import fs from 'fs' import { isEmpty } from 'lodash' import path from 'path' import util from 'util' import { exec } from 'child_process' const execAsync = util.promisify(exec) const fspromises = fs.promises /** * 判断文件或目录是否存在 * @param {*} path 文件或目录的路径 * @returns true表示存在,false表示不存在 */ export async function CheckFileOrDirExist(filePath) { try { let newFilePath = path.resolve(filePath) await fspromises.access(newFilePath) return true // 文件或目录存在 } catch (error) { return false // 文件或目录不存在 } } // 检查文件夹是不是存在,不存在的话,创建 export async function CheckFolderExistsOrCreate(folderPath) { try { if (!(await CheckFileOrDirExist(folderPath))) { await fspromises.mkdir(folderPath, { recursive: true }) } } catch (error) { throw error } } /** * 拼接两个地址,返回拼接后的地址 * @param {*} rootPath 根目录的消息 * @param {*} subPath 子目录的消息 * @returns */ export function JoinPath(rootPath: string, subPath: string | null): string | undefined { // 判断第二个地址是不是存在,不存在返回null,存在返回拼接后的地址 if (subPath && !isEmpty(subPath)) { return path.resolve(rootPath, subPath) } else { return undefined } } /** * 删除指定的文件中里面所有的文件和文件夹 * @param {*} folderPath 文件夹地址 * @param {*} isDeleteOut 是否删除最外层的文件夹,默认false,不删除 */ export async function DeleteFolderAllFile( folderPath: string, isDeleteOut: boolean = false ): Promise { try { let folderIsExist = await CheckFileOrDirExist(folderPath) if (!folderIsExist) { throw new Error('目的文件夹不存在,' + folderPath) } // 开始删除 let files = await fspromises.readdir(folderPath) for (const file of files) { const curPath = path.join(folderPath, file) const stat = await fspromises.stat(curPath) if (stat.isDirectory()) { // 判断是不是文件夹 await DeleteFolderAllFile(curPath) // 递归删除文件夹内容 await fspromises.rmdir(curPath) // 删除空文件夹 } else { // 删除文件 await fspromises.unlink(curPath) } } // 判断是不是要删除最外部的文件夹 if (isDeleteOut) { await fspromises.rmdir(folderPath) } } catch (error) { throw error } } /** * 拷贝一个文件或者是文件夹到指定的目标地址 * @param {*} source 源文件或文件夹地址 * @param {*} target 目标文件或文件夹地址 * @param {*} checkParent 是否检查父文件夹是否存在,不存在的话创建,默认false,不检查,不存在直接创建 */ export async function CopyFileOrFolder(source, target, checkParent = false) { try { // 判断源文件或文件夹是不是存在 if (!(await CheckFileOrDirExist(source))) { throw new Error(`源文件或文件夹不存在: ${source}`) } // 判断父文件夹是否存在,不存在创建 const parent_path = path.dirname(target) let parentIsExist = await CheckFileOrDirExist(parent_path) if (!parentIsExist) { if (checkParent) { throw new Error(`目的文件或文件夹的父文件夹不存在: ${parent_path}`) } else { await fspromises.mkdir(parent_path, { recursive: true }) } } // 判断是不是文件夹 const isDirectory = await IsDirectory(source) // 复制文件夹的逻辑 async function copyDirectory(source, target) { // 创建目标文件夹 await fspromises.mkdir(target, { recursive: true }) let entries = await fspromises.readdir(source, { withFileTypes: true }) for (let entry of entries) { let srcPath = path.join(source, entry.name) let tgtPath = path.join(target, entry.name) if (entry.isDirectory()) { await copyDirectory(srcPath, tgtPath) } else { await fspromises.copyFile(srcPath, tgtPath) } } } if (isDirectory) { // 创建目标文件夹 await copyDirectory(source, target) } else { // 复制文件 await fspromises.copyFile(source, target) } } catch (error) { throw error } } /** * 判断一个文件地址是不是文件夹 * @param {*} path 输入的文件地址 * @returns true 是 false 不是 */ export async function IsDirectory(path) { try { const stat = await fspromises.stat(path) return stat.isDirectory() } catch (error) { throw new Error(`获取文件夹信息失败: ${path}`) } } /** * 将文件或者是文件夹备份到指定的文职 * @param {*} source_path 源文件/文件夹地址 * @param {*} target_path 目标文件/文件夹地址 */ export async function BackupFileOrFolder(source_path: string, target_path: string): Promise { try { // 判断父文件夹是否存在,不存在创建 const parent_path = path.dirname(target_path) if (!(await CheckFileOrDirExist(parent_path))) { await fspromises.mkdir(parent_path, { recursive: true }) } // 判断是不是文件夹 const isDirectory = await IsDirectory(source_path) if (isDirectory) { // 复制文件夹 await fspromises.rename(source_path, target_path) } else { // 复制文件 await fspromises.copyFile(source_path, target_path) } } catch (error) { throw error } } /** * 获取指定的文件夹下面的所有的指定的拓展名的文件 * @param {*} folderPath 文件夹地址 * @param {*} extensions 拓展地址 * @returns 返回文件中指定的后缀文件地址(绝对地址) */ export async function GetFilesWithExtensions( folderPath: string, extensions: string[] ): Promise { try { // 判断当前是不是文件夹 if (!(await IsDirectory(folderPath))) { throw new Error('输入的不是有效的文件夹地址') } let entries = await fspromises.readdir(folderPath, { withFileTypes: true }) let files = [] as any // 使用Promise.all来并行处理所有的stat调用 const fileStats = await Promise.all( entries.map(async (entry) => { const entryPath = path.join(folderPath, entry.name) if (entry.isFile()) { return { name: entry.name, path: entryPath, isFile: true } } else { return { isFile: false } } }) ) // 过滤出文件并且满足扩展名要求的文件 files = fileStats.filter( (fileStat) => fileStat.isFile && extensions.includes(path.extname(fileStat.name ?? '').toLowerCase()) ) // 对files数组进行排序,基于文件名 files.sort((a: any, b: any) => a.name.localeCompare(b.name)) // 返回文件名数组(完整的) return files.map((fileStat) => path.join(folderPath, fileStat.name)) } catch (error) { throw error } } /** * 获取文件的大小 * @param filePath 文件的地址 * @returns 返回的文件大小为 kb单位 */ export async function GetFileSize(filePath: string): Promise { try { if (!(await CheckFileOrDirExist(filePath))) { throw new Error('获取文件大小,指定的文件不存在') } const stats = await fspromises.stat(filePath) return stats.size / 1024 } catch (error) { throw error } } /** * 获取文件夹下的所有子文件夹信息,按创建时间排序 * @param folderPath 文件夹的路径 * @returns 返回包含子文件夹名称和完整路径的对象数组,按创建时间排序(最新的在前) */ export async function GetSubdirectoriesWithInfo( folderPath: string ): Promise> { try { const filesAndDirectories = await fs.promises.readdir(folderPath, { withFileTypes: true }) // 过滤出文件夹 const directories = filesAndDirectories.filter((dirent) => dirent.isDirectory()) // 并行获取所有文件夹的状态信息 const directoryStatsPromises = directories.map((dirent) => fs.promises.stat(path.join(folderPath, dirent.name)) ) const directoryStats = await Promise.all(directoryStatsPromises) // 将目录信息和状态对象组合 const directoriesWithInfo = directories.map((dirent, index) => ({ name: dirent.name, fullPath: path.join(folderPath, dirent.name), ctime: directoryStats[index].ctime })) // 按创建时间排序,最新的在前 directoriesWithInfo.sort((a, b) => b.ctime.getTime() - a.ctime.getTime()) return directoriesWithInfo } catch (error) { throw error } } /** * 删除目标图片,然后将原图片的exif信息删除,然后将原图片复制到目标图片地址 * @param {*} exiftoolPath exiftool的地址 * @param {*} source 原图片地址 * @param {*} target 目标图片地址 */ export async function DeleteFileExifData(exiftoolPath: string, source: string, target: string) { try { if (await CheckFileOrDirExist(target)) { await fspromises.unlink(target) } let script = `"${exiftoolPath}" -all= -overwrite_original "${source}" -o "${target}"` await execAsync(script, { maxBuffer: 1024 * 1024 * 10, encoding: 'utf-8' }) } catch (error) { throw error } } /** * 下载网络图片到本地 * * 该方法从指定的URL下载图片文件,并将其保存到本地指定路径。 * 如果目标文件夹不存在,会自动创建。如果指定路径已存在文件,则会覆盖。 * 支持重试机制和详细的错误处理。 * * @param {string} imageUrl - 图片的网络URL地址 * @param {string} localPath - 保存到本地的完整路径,包含文件名和扩展名 * @param {number} maxRetries - 最大重试次数,默认为3次 * @param {number} timeout - 超时时间(毫秒),默认为60秒 * @returns {Promise} 成功时返回保存的本地文件路径 * @throws {Error} 当网络请求失败、写入失败或其他错误时抛出异常 * * @example * // 下载图片到指定路径 * try { * const savedPath = await DownloadImageFromUrl( * 'https://example.com/image.jpg', * 'd:/images/downloaded.jpg' * ); * console.log('图片已保存至:', savedPath); * } catch (error) { * console.error('下载图片失败:', error.message); * } */ export async function DownloadImageFromUrl( imageUrl: string, localPath: string, maxRetries: number = 3, timeout: number = 60000 ): Promise { // 确保目标文件夹存在 const dirPath = path.dirname(localPath) await CheckFolderExistsOrCreate(dirPath) let lastError: Error | null = null for (let attempt = 1; attempt <= maxRetries; attempt++) { try { // 使用fetch获取图片数据,设置超时和重试友好的配置 const response = await fetch(imageUrl, { signal: AbortSignal.timeout(timeout), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', Accept: 'image/*,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', Connection: 'keep-alive', 'Cache-Control': 'no-cache' } }) if (!response.ok) { throw new Error(`HTTP错误: ${response.status} ${response.statusText}`) } // 获取图片的二进制数据 const arrayBuffer = await response.arrayBuffer() const buffer = Buffer.from(arrayBuffer) // 验证下载的数据是否有效 if (buffer.length === 0) { throw new Error('下载的文件为空') } // 将图片数据写入本地文件 await fspromises.writeFile(localPath, buffer) console.log(`图片下载成功: ${localPath} (大小: ${(buffer.length / 1024).toFixed(2)} KB)`) return localPath } catch (error) { lastError = error instanceof Error ? error : new Error('未知错误') console.error(`第${attempt}次下载失败:`, lastError.message) // 如果不是最后一次尝试,等待一段时间再重试 if (attempt < maxRetries) { const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 5000) // 指数退避,最大5秒 console.log(`等待 ${waitTime}ms 后重试...`) await new Promise((resolve) => setTimeout(resolve, waitTime)) } else { throw lastError } } } // 所有重试都失败了,抛出最后一个错误 const errorMessage = lastError?.message || '未知错误' if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) { throw new Error(`下载图片超时 (${timeout / 1000}秒),已重试${maxRetries}次: ${errorMessage}`) } else if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('ECONNREFUSED')) { throw new Error(`网络连接失败,无法访问图片地址,已重试${maxRetries}次: ${errorMessage}`) } else if (errorMessage.includes('Connect Timeout Error')) { throw new Error(`连接超时,服务器响应缓慢,已重试${maxRetries}次: ${errorMessage}`) } else { throw new Error(`下载图片失败,已重试${maxRetries}次: ${errorMessage}`) } }