2025-09-04 16:58:42 +08:00

404 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void> {
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<void> {
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<string[]> {
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<number> {
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<Array<{ name: string; fullPath: string; ctime: Date }>> {
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<string>} 成功时返回保存的本地文件路径
* @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<string> {
// 确保目标文件夹存在
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}`)
}
}