mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2024-11-21 09:36:35 +00:00
309 lines
8.5 KiB
TypeScript
309 lines
8.5 KiB
TypeScript
import fs from 'fs';
|
||
import fsPromise, { stat } from 'fs/promises';
|
||
import crypto from 'crypto';
|
||
import util from 'util';
|
||
import path from 'node:path';
|
||
import { log, logError } from './log';
|
||
import { dbUtil } from '@/common/utils/db';
|
||
import * as fileType from 'file-type';
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
import { napCatCore } from '@/core';
|
||
|
||
export const getNapCatDir = () => {
|
||
const p = path.join(napCatCore.dataPath, 'NapCat');
|
||
fs.mkdirSync(p, { recursive: true });
|
||
return p;
|
||
};
|
||
export const getTempDir = () => {
|
||
const p = path.join(getNapCatDir(), 'temp');
|
||
// 创建临时目录
|
||
if (!fs.existsSync(p)) {
|
||
fs.mkdirSync(p, { recursive: true });
|
||
}
|
||
return p;
|
||
};
|
||
|
||
|
||
export function isGIF(path: string) {
|
||
const buffer = Buffer.alloc(4);
|
||
const fd = fs.openSync(path, 'r');
|
||
fs.readSync(fd, buffer, 0, 4, 0);
|
||
fs.closeSync(fd);
|
||
return buffer.toString() === 'GIF8';
|
||
}
|
||
|
||
// 定义一个异步函数来检查文件是否存在
|
||
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const startTime = Date.now();
|
||
|
||
function check() {
|
||
if (fs.existsSync(path)) {
|
||
resolve();
|
||
} else if (Date.now() - startTime > timeout) {
|
||
reject(new Error(`文件不存在: ${path}`));
|
||
} else {
|
||
setTimeout(check, 100);
|
||
}
|
||
}
|
||
|
||
check();
|
||
});
|
||
}
|
||
// 定义一个异步函数来检查文件是否存在
|
||
export async function checkFileReceived2(path: string, timeout: number = 3000): Promise<void> {
|
||
// 使用 Promise.race 来同时进行文件状态检查和超时计时
|
||
// Promise.race 会返回第一个解决(resolve)或拒绝(reject)的 Promise
|
||
await Promise.race([
|
||
checkFile(path),
|
||
timeoutPromise(timeout, `文件不存在: ${path}`),
|
||
]);
|
||
}
|
||
|
||
// 转换超时时间至 Promise
|
||
function timeoutPromise(timeout: number, errorMsg: string): Promise<void> {
|
||
return new Promise((_, reject) => {
|
||
setTimeout(() => {
|
||
reject(new Error(errorMsg));
|
||
}, timeout);
|
||
});
|
||
}
|
||
|
||
// 异步检查文件是否存在
|
||
async function checkFile(path: string): Promise<void> {
|
||
try {
|
||
await stat(path);
|
||
} catch (error: any) {
|
||
if (error.code === 'ENOENT') {
|
||
// 如果文件不存在,则抛出一个错误
|
||
throw new Error(`文件不存在: ${path}`);
|
||
} else {
|
||
// 对于 stat 调用的其他错误,重新抛出
|
||
throw error;
|
||
}
|
||
}
|
||
// 如果文件存在,则无需做任何事情,Promise 解决(resolve)自身
|
||
}
|
||
export async function file2base64(path: string) {
|
||
const readFile = util.promisify(fs.readFile);
|
||
const result = {
|
||
err: '',
|
||
data: ''
|
||
};
|
||
try {
|
||
// 读取文件内容
|
||
// if (!fs.existsSync(path)){
|
||
// path = path.replace("\\Ori\\", "\\Thumb\\");
|
||
// }
|
||
try {
|
||
await checkFileReceived(path, 5000);
|
||
} catch (e: any) {
|
||
result.err = e.toString();
|
||
return result;
|
||
}
|
||
const data = await readFile(path);
|
||
// 转换为Base64编码
|
||
result.data = data.toString('base64');
|
||
} catch (err: any) {
|
||
result.err = err.toString();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
|
||
export function calculateFileMD5(filePath: string): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
// 创建一个流式读取器
|
||
const stream = fs.createReadStream(filePath);
|
||
const hash = crypto.createHash('md5');
|
||
|
||
stream.on('data', (data: Buffer) => {
|
||
// 当读取到数据时,更新哈希对象的状态
|
||
hash.update(data);
|
||
});
|
||
|
||
stream.on('end', () => {
|
||
// 文件读取完成,计算哈希
|
||
const md5 = hash.digest('hex');
|
||
resolve(md5);
|
||
});
|
||
|
||
stream.on('error', (err: Error) => {
|
||
// 处理可能的读取错误
|
||
reject(err);
|
||
});
|
||
});
|
||
}
|
||
|
||
export interface HttpDownloadOptions {
|
||
url: string;
|
||
headers?: Record<string, string> | string;
|
||
}
|
||
|
||
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
|
||
const chunks: Buffer[] = [];
|
||
let url: string;
|
||
let headers: Record<string, string> = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36'
|
||
};
|
||
if (typeof options === 'string') {
|
||
url = options;
|
||
const host = new URL(url).hostname;
|
||
headers['Host'] = host;
|
||
} else {
|
||
url = options.url;
|
||
if (options.headers) {
|
||
if (typeof options.headers === 'string') {
|
||
headers = JSON.parse(options.headers);
|
||
} else {
|
||
headers = options.headers;
|
||
}
|
||
}
|
||
}
|
||
const fetchRes = await fetch(url, { headers });
|
||
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`);
|
||
|
||
const blob = await fetchRes.blob();
|
||
const buffer = await blob.arrayBuffer();
|
||
return Buffer.from(buffer);
|
||
}
|
||
|
||
type Uri2LocalRes = {
|
||
success: boolean,
|
||
errMsg: string,
|
||
fileName: string,
|
||
ext: string,
|
||
path: string,
|
||
isLocal: boolean
|
||
}
|
||
|
||
export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> {
|
||
const res = {
|
||
success: false,
|
||
errMsg: '',
|
||
fileName: '',
|
||
ext: '',
|
||
path: '',
|
||
isLocal: false
|
||
};
|
||
if (!fileName) {
|
||
fileName = uuidv4();
|
||
}
|
||
let filePath = path.join(getTempDir(), fileName);
|
||
let url = null;
|
||
try {
|
||
url = new URL(uri);
|
||
} catch (e: any) {
|
||
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`;
|
||
return res;
|
||
}
|
||
|
||
// log("uri protocol", url.protocol, uri);
|
||
if (url.protocol == 'base64:') {
|
||
// base64转成文件
|
||
const base64Data = uri.split('base64://')[1];
|
||
try {
|
||
const buffer = Buffer.from(base64Data, 'base64');
|
||
fs.writeFileSync(filePath, buffer);
|
||
|
||
} catch (e: any) {
|
||
res.errMsg = 'base64文件下载失败,' + e.toString();
|
||
return res;
|
||
}
|
||
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
|
||
// 下载文件
|
||
let buffer: Buffer | null = null;
|
||
try {
|
||
buffer = await httpDownload(uri);
|
||
} catch (e: any) {
|
||
res.errMsg = `${url}下载失败,` + e.toString();
|
||
return res;
|
||
}
|
||
try {
|
||
const pathInfo = path.parse(decodeURIComponent(url.pathname));
|
||
if (pathInfo.name) {
|
||
fileName = pathInfo.name;
|
||
if (pathInfo.ext) {
|
||
fileName += pathInfo.ext;
|
||
// res.ext = pathInfo.ext
|
||
}
|
||
}
|
||
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_');
|
||
res.fileName = fileName;
|
||
filePath = path.join(getTempDir(), uuidv4() + fileName);
|
||
fs.writeFileSync(filePath, buffer);
|
||
} catch (e: any) {
|
||
res.errMsg = `${url}下载失败,` + e.toString();
|
||
return res;
|
||
}
|
||
} else {
|
||
let pathname: string;
|
||
if (url.protocol === 'file:') {
|
||
// await fs.copyFile(url.pathname, filePath);
|
||
pathname = decodeURIComponent(url.pathname);
|
||
if (process.platform === 'win32') {
|
||
filePath = pathname.slice(1);
|
||
} else {
|
||
filePath = pathname;
|
||
}
|
||
} else {
|
||
const cache = await dbUtil.getFileCacheByName(uri);
|
||
if (cache) {
|
||
filePath = cache.path;
|
||
} else {
|
||
filePath = uri;
|
||
}
|
||
}
|
||
|
||
res.isLocal = true;
|
||
}
|
||
// else{
|
||
// res.errMsg = `不支持的file协议,` + url.protocol
|
||
// return res
|
||
// }
|
||
// if (isGIF(filePath) && !res.isLocal) {
|
||
// await fs.rename(filePath, filePath + ".gif");
|
||
// filePath += ".gif";
|
||
// }
|
||
if (!res.isLocal && !res.ext) {
|
||
try {
|
||
const ext: string | undefined = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||
if (ext) {
|
||
log('获取文件类型', ext, filePath);
|
||
fs.renameSync(filePath, filePath + `.${ext}`);
|
||
filePath += `.${ext}`;
|
||
res.fileName += `.${ext}`;
|
||
res.ext = ext;
|
||
}
|
||
} catch (e) {
|
||
// log("获取文件类型失败", filePath,e.stack)
|
||
}
|
||
}
|
||
res.success = true;
|
||
res.path = filePath;
|
||
return res;
|
||
}
|
||
|
||
export async function copyFolder(sourcePath: string, destPath: string) {
|
||
try {
|
||
const entries = await fsPromise.readdir(sourcePath, { withFileTypes: true });
|
||
await fsPromise.mkdir(destPath, { recursive: true });
|
||
for (const entry of entries) {
|
||
const srcPath = path.join(sourcePath, entry.name);
|
||
const dstPath = path.join(destPath, entry.name);
|
||
if (entry.isDirectory()) {
|
||
await copyFolder(srcPath, dstPath);
|
||
} else {
|
||
try {
|
||
await fsPromise.copyFile(srcPath, dstPath);
|
||
} catch (error) {
|
||
logError(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`);
|
||
// 这里可以决定是否要继续复制其他文件
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
logError('复制文件夹时出错:', error);
|
||
}
|
||
}
|