返回 AiToEarn
file.ts
1 import fs from 'fs';
2 import mimeTypes from 'mime-types';
3 import ffmpeg from 'fluent-ffmpeg';
4 import path from 'path';
5 import { net } from 'electron';
6 import { v4 as uuidv4 } from 'uuid';
7
8 export interface FileInfo {
9 streams: Array<{
10 codec_type: string;
11 width?: number;
12 height?: number;
13 duration?: number;
14 bit_rate?: string;
15 color_primaries?: string;
16 r_frame_rate?: string;
17 codec_name?: string;
18 codec_long_name?: string;
19 sample_rate?: string;
20 channels?: number;
21 }>;
22 format: {
23 size: number;
24 };
25 mimeType: string;
26 }
27
28 export interface FilePartInfo {
29 fileSize: number;
30 blockInfo: number[];
31 }
32
33 export class FileUtils {
34 /**
35 * 判断目录是否存在,若不存在则创建目录
36 * @param catalogue
37 * @returns {Promise<boolean>}
38 */
39 static async checkDirectories(catalogue: string): Promise<boolean> {
40 return new Promise((resolve, reject) => {
41 fs.access(catalogue, fs.constants.F_OK, (err) => {
42 if (err) {
43 // 创建目录
44 fs.mkdir(catalogue, { recursive: true }, (err) => {
45 if (err) {
46 reject(false);
47 } else {
48 resolve(true);
49 }
50 });
51 } else {
52 resolve(true);
53 }
54 });
55 });
56 }
57
58 /**
59 * 获取文件信息
60 * @param filePath
61 */
62 static async getFileInfo(filePath: string): Promise<FileInfo> {
63 return new Promise((resolve, reject) => {
64 try {
65 console.log('开始获取文件信息, 文件路径:', filePath);
66
67 // 规范化文件路径
68 const normalizedPath = path.normalize(filePath);
69 console.log('规范化后的文件路径:', normalizedPath);
70
71 // 获取文件的绝对路径
72 const absolutePath = path.resolve(normalizedPath);
73 console.log('绝对文件路径:', absolutePath);
74
75 // 检查文件是否存在
76 if (!fs.existsSync(absolutePath)) {
77 console.error('文件不存在:', absolutePath);
78 reject('获取文件信息失败,失败原因:文件不存在');
79 return;
80 }
81
82 // 检查文件是否可读
83 try {
84 fs.accessSync(absolutePath, fs.constants.R_OK);
85 } catch (err) {
86 console.error('文件不可读:', err);
87 reject('获取文件信息失败,失败原因:文件不可读');
88 return;
89 }
90
91 // 获取文件状态
92 const stats = fs.statSync(absolutePath);
93 if (!stats.isFile()) {
94 console.error('路径不是文件:', absolutePath);
95 reject('获取文件信息失败,失败原因:路径不是文件');
96 return;
97 }
98
99 // 获取文件MimeType
100 const fileMimeType = mimeTypes.lookup(absolutePath);
101 console.log('文件 MimeType:', fileMimeType);
102
103 if (
104 !fileMimeType ||
105 typeof fileMimeType !== 'string' ||
106 !fileMimeType.includes('video')
107 ) {
108 console.error('不支持的文件格式:', fileMimeType);
109 reject('获取文件信息失败,失败原因:不支持的文件格式');
110 return;
111 }
112
113 // 获取视频文件信息
114 console.log(`开始获取视频文件信息... [${new Date().toLocaleString()}]`);
115 console.log('要分析的文件路径:', absolutePath);
116
117 // 直接使用 ffmpeg.ffprobe 静态方法
118 ffmpeg.ffprobe(absolutePath, (err: Error | null, metadata: any) => {
119 if (err) {
120 console.error(
121 `ffprobe 错误 [${new Date().toLocaleString()}]:`,
122 err,
123 );
124 reject('获取文件信息失败,失败原因:' + (err.message ?? '未知'));
125 return;
126 }
127 console.log(
128 `获取到的视频元数据 [${new Date().toLocaleString()}]:`,
129 metadata,
130 );
131 const result: FileInfo = {
132 ...metadata,
133 mimeType: fileMimeType,
134 };
135 resolve(result);
136 });
137 } catch (err: any) {
138 console.error('获取文件信息时发生错误:', err);
139 reject('获取文件信息失败,失败原因:' + (err.message || '未知错误'));
140 }
141 });
142 }
143
144 /**
145 * 获取文件大小及分片信息
146 * @param filePath
147 * @param blockSize
148 * @returns {Promise<FilePartInfo>}
149 */
150 static async getFilePartInfo(
151 filePath: string,
152 blockSize: number,
153 ): Promise<FilePartInfo> {
154 return new Promise((resolve, reject) => {
155 try {
156 console.log(
157 '开始获取文件分片信息, 文件路径:',
158 filePath,
159 '块大小:',
160 blockSize,
161 );
162 const fileInfo = fs.statSync(filePath);
163 const fileSize = fileInfo.size;
164 console.log('文件大小:', fileSize);
165
166 const blockInfo: number[] = [];
167 for (let i = 1; i <= Math.ceil(fileSize / blockSize); i++) {
168 if (i === Math.ceil(fileSize / blockSize)) {
169 blockInfo.push(fileSize);
170 } else {
171 blockInfo.push(blockSize * i);
172 }
173 }
174 console.log('分片信息:', blockInfo);
175 resolve({
176 fileSize,
177 blockInfo,
178 });
179 } catch (err: any) {
180 console.error('获取分片信息错误:', err);
181 reject('获取分片信息失败,失败原因:' + err.message);
182 }
183 });
184 }
185
186 /**
187 * 获取文件分片内容
188 * @param filePath
189 * @param start
190 * @param end
191 */
192 static async getFilePartContent(
193 filePath: string,
194 start: number,
195 end: number,
196 ): Promise<Buffer> {
197 return new Promise((resolve, reject) => {
198 try {
199 const readStream = fs.createReadStream(filePath, {
200 start: start,
201 end: end,
202 });
203 const chunks: Buffer[] = [];
204 // @ts-ignore
205 readStream.on('data', (chunk: Buffer) => {
206 chunks.push(chunk);
207 });
208 readStream.on('end', () => {
209 readStream.close();
210 resolve(Buffer.concat(chunks));
211 });
212 } catch (err: any) {
213 reject('获取文件分片内容失败,失败原因:' + err.message);
214 }
215 });
216 }
217
218 /**
219 * 获取项目根目录
220 */
221 static getAppRootDir(): string {
222 // 在开发环境中
223 if (process.env.NODE_ENV === 'development') {
224 return path.join(process.cwd(), 'node_modules', 'ffprobe-static', 'bin');
225 }
226 // 在生产环境中
227 if (process.type === 'renderer') {
228 return path.join(process.resourcesPath, 'app.asar');
229 }
230 return path.join(process.resourcesPath, 'app.asar.unpacked');
231 }
232
233 /**
234 * 获取文件数据目录
235 * @returns
236 */
237 static getAppDataPath() {
238 switch (process.platform) {
239 case 'darwin': {
240 return path.join(
241 process.env.HOME || '',
242 'Library',
243 'Application Support',
244 'att',
245 );
246 }
247 case 'win32': {
248 return path.join(process.env.APPDATA || '', 'att');
249 }
250 case 'linux': {
251 return path.join(process.env.HOME || '', 'att');
252 }
253 default: {
254 console.log('Unsupported platform!');
255 process.exit(1);
256 }
257 }
258 }
259
260 /**
261 * 下载文件
262 * @param url
263 * @param name
264 * @returns
265 */
266 static async downFile(url: string, name?: string): Promise<string> {
267 const dirPath = this.getAppDataPath();
268 await this.checkDirectories(dirPath);
269
270 return new Promise((resolve, reject) => {
271 const request = net.request(url);
272 let fileStream: NodeJS.WritableStream | null = null;
273 let filePath: string;
274 const fileExt: string = path.extname(url);
275
276 request.on('response', (response) => {
277 if (response.statusCode !== 200) {
278 return reject(new Error(`下载失败: 状态码 ${response.statusCode}`));
279 }
280
281 filePath = path.join(dirPath, (name || uuidv4()) + fileExt);
282 fileStream = fs.createWriteStream(filePath);
283
284 response.on('data', (chunk) => {
285 if (fileStream) fileStream.write(chunk);
286 });
287
288 fileStream.on('finish', () => {
289 resolve(filePath);
290 });
291
292 // 处理文件流错误
293 fileStream.on('error', (err) => {
294 reject(err);
295 });
296
297 response.on('end', () => {
298 if (fileStream) {
299 fileStream.end();
300 resolve(filePath); // 确保filePath已定义
301 }
302 });
303 });
304
305 request.on('error', (err) => {
306 reject(err);
307 });
308
309 // 必须调用request.end()来发送请求
310 request.end();
311 });
312 }
313
314 // 获取路径的文件到前端
315 static async getFileBuffer(filePath: string): Promise<any> {
316 return new Promise((resolve, reject) => {
317 fs.readFile(filePath, (err, data) => {
318 if (err) {
319 reject(err);
320 } else {
321 resolve(data);
322 }
323 });
324 });
325 }
326 }
327
327 lines TYPESCRIPT