improvement for the backend api
This commit is contained in:
191
lib/utils/file-parser.ts
Normal file
191
lib/utils/file-parser.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
// ============================================================================
|
||||
// File Parser Utilities
|
||||
// ============================================================================
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
import Papa from "papaparse";
|
||||
|
||||
export interface ParsedChannel {
|
||||
telegram_id?: number;
|
||||
title: string;
|
||||
username?: string;
|
||||
description?: string;
|
||||
subscribers_count?: number;
|
||||
row: number; // Номер строки для отчетов об ошибках
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
channels: ParsedChannel[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит Excel файл (xlsx)
|
||||
*/
|
||||
export const parseExcelFile = async (file: File): Promise<ParseResult> => {
|
||||
const errors: string[] = [];
|
||||
const channels: ParsedChannel[] = [];
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const workbook = XLSX.read(arrayBuffer, { type: "array" });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||
|
||||
// Пропускаем заголовок (первая строка)
|
||||
const rows = jsonData.slice(1) as any[][];
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
const rowNumber = index + 2; // +2 потому что пропустили заголовок и индекс с 0
|
||||
|
||||
// Пропускаем пустые строки
|
||||
if (!row || row.length === 0 || !row[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const channel: ParsedChannel = {
|
||||
row: rowNumber,
|
||||
title: String(row[0] || "").trim(),
|
||||
username: row[1] ? String(row[1]).trim().replace("@", "") : undefined,
|
||||
telegram_id: row[2] ? parseInt(String(row[2])) : undefined,
|
||||
subscribers_count: row[3] ? parseInt(String(row[3])) : undefined,
|
||||
description: row[4] ? String(row[4]).trim() : undefined,
|
||||
};
|
||||
|
||||
if (!channel.title) {
|
||||
errors.push(`Строка ${rowNumber}: отсутствует название канала`);
|
||||
return;
|
||||
}
|
||||
|
||||
channels.push(channel);
|
||||
} catch (err) {
|
||||
errors.push(
|
||||
`Строка ${rowNumber}: ошибка парсинга - ${
|
||||
err instanceof Error ? err.message : "неизвестная ошибка"
|
||||
}`
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
errors.push(
|
||||
`Ошибка чтения файла: ${
|
||||
err instanceof Error ? err.message : "неизвестная ошибка"
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
return { channels, errors };
|
||||
};
|
||||
|
||||
/**
|
||||
* Парсит CSV файл
|
||||
*/
|
||||
export const parseCsvFile = async (file: File): Promise<ParseResult> => {
|
||||
return new Promise((resolve) => {
|
||||
const errors: string[] = [];
|
||||
const channels: ParsedChannel[] = [];
|
||||
|
||||
Papa.parse(file, {
|
||||
header: false,
|
||||
skipEmptyLines: true,
|
||||
complete: (results) => {
|
||||
const rows = results.data as string[][];
|
||||
|
||||
// Пропускаем заголовок
|
||||
rows.slice(1).forEach((row, index) => {
|
||||
const rowNumber = index + 2;
|
||||
|
||||
if (!row || row.length === 0 || !row[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const channel: ParsedChannel = {
|
||||
row: rowNumber,
|
||||
title: (row[0] || "").trim(),
|
||||
username: row[1] ? row[1].trim().replace("@", "") : undefined,
|
||||
telegram_id: row[2] ? parseInt(row[2]) : undefined,
|
||||
subscribers_count: row[3] ? parseInt(row[3]) : undefined,
|
||||
description: row[4] ? row[4].trim() : undefined,
|
||||
};
|
||||
|
||||
if (!channel.title) {
|
||||
errors.push(`Строка ${rowNumber}: отсутствует название канала`);
|
||||
return;
|
||||
}
|
||||
|
||||
channels.push(channel);
|
||||
} catch (err) {
|
||||
errors.push(
|
||||
`Строка ${rowNumber}: ошибка парсинга - ${
|
||||
err instanceof Error ? err.message : "неизвестная ошибка"
|
||||
}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
resolve({ channels, errors });
|
||||
},
|
||||
error: (err) => {
|
||||
errors.push(`Ошибка парсинга CSV: ${err.message}`);
|
||||
resolve({ channels: [], errors });
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Универсальная функция парсинга файла
|
||||
*/
|
||||
export const parseChannelsFile = async (file: File): Promise<ParseResult> => {
|
||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (extension === "xlsx" || extension === "xls") {
|
||||
return parseExcelFile(file);
|
||||
} else if (extension === "csv") {
|
||||
return parseCsvFile(file);
|
||||
} else {
|
||||
return {
|
||||
channels: [],
|
||||
errors: [
|
||||
"Неподдерживаемый формат файла. Поддерживаются только .xlsx, .xls и .csv",
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Скачать шаблон для импорта
|
||||
*/
|
||||
export const downloadTemplate = (format: "xlsx" | "csv") => {
|
||||
const headers = [
|
||||
"Название канала*",
|
||||
"Username (без @)",
|
||||
"Telegram ID",
|
||||
"Подписчиков",
|
||||
"Описание",
|
||||
];
|
||||
const example = [
|
||||
"Пример канала",
|
||||
"example_channel",
|
||||
"-1001234567890",
|
||||
"10000",
|
||||
"Описание канала",
|
||||
];
|
||||
|
||||
if (format === "xlsx") {
|
||||
const ws = XLSX.utils.aoa_to_sheet([headers, example]);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Channels");
|
||||
XLSX.writeFile(wb, "channels_template.xlsx");
|
||||
} else {
|
||||
const csv = Papa.unparse([headers, example]);
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = "channels_template.csv";
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user