Feature(custom): add vitest and optimize performance of aeshelper

This commit is contained in:
Kuingsmile
2025-08-11 12:24:25 +08:00
parent ed02d00f05
commit 112c08d92c
7 changed files with 830 additions and 449 deletions

View File

@@ -1,3 +1,3 @@
import { bootstrap } from '~/lifeCycle'
bootstrap.launchApp()
import { lifeCycle } from '~/lifeCycle'
lifeCycle.launchApp()

View File

@@ -1,350 +1,350 @@
import '~/lifeCycle/errorHandler'
import path from 'node:path'
import bus from '@core/bus'
import db from '@core/datastore'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import { remoteNoticeHandler } from 'apis/app/remoteNotice'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import { createTray, setDockMenu } from 'apis/app/system'
import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
import windowManager from 'apis/app/window/windowManager'
import axios from 'axios'
import { app, dialog, globalShortcut, Notification, protocol, screen, shell } from 'electron'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import updater from 'electron-updater'
import fs from 'fs-extra'
import busEventList from '~/events/busEventList'
import { rpcServer } from '~/events/rpc'
import { startFileServer, stopFileServer } from '~/fileServer'
import { T as $t } from '~/i18n'
import fixPath from '~/lifeCycle/fixPath'
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
import getManageApi from '~/manage/Main'
import { clearTempFolder } from '~/manage/utils/common'
import server from '~/server/index'
import webServer from '~/server/webServer'
import beforeOpen from '~/utils/beforeOpen'
import clipboardPoll from '~/utils/clipboardPoll'
import { configPaths } from '~/utils/configPaths'
import { II18nLanguage, IRemoteNoticeTriggerHook, ISartMode, IWindowList } from '~/utils/enum'
import { getUploadFiles } from '~/utils/handleArgv'
import { initI18n } from '~/utils/handleI18n'
import { notificationList } from '~/utils/notification'
import { CLIPBOARD_IMAGE_FOLDER } from '~/utils/static'
import updateChecker from '~/utils/updateChecker'
const isDevelopment = process.env.NODE_ENV !== 'production'
const handleStartUpFiles = (argv: string[], cwd: string) => {
const files = getUploadFiles(argv, cwd, logger)
if (files === null) {
logger.info('cli -> uploading file from clipboard')
uploadClipboardFiles()
return true
}
if (files.length > 0) {
logger.info('cli -> uploading files from cli', ...files.map(file => file.path))
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
return true
}
return false
}
updater.autoUpdater.setFeedURL({
provider: 'generic',
url: 'https://release.piclist.cn/latest',
channel: 'latest'
})
updater.autoUpdater.autoDownload = false
updater.autoUpdater.on('update-available', async (info: updater.UpdateInfo) => {
const lang = db.get(configPaths.settings.language) || II18nLanguage.ZH_CN
let updateLog = ''
try {
const url =
lang === II18nLanguage.ZH_CN
? 'https://release.piclist.cn/currentVersion.md'
: 'https://release.piclist.cn/currentVersion_en.md'
const res = await axios.get(url)
updateLog = res.data
} catch (e: any) {
logger.error(e)
}
const maxLogLength = 800
let displayLog = updateLog
let truncatedNote = ''
if (updateLog.length > maxLogLength) {
const truncatePoint = updateLog.lastIndexOf('\n', maxLogLength)
displayLog = updateLog.substring(0, truncatePoint > 0 ? truncatePoint : maxLogLength)
truncatedNote =
lang === II18nLanguage.ZH_CN
? '\n\n... (更多详情请查看完整更新日志)'
: '\n\n... (See full changelog for more details)'
}
dialog
.showMessageBox({
type: 'info',
title: $t('FIND_NEW_VERSION'),
buttons: ['Yes', 'Go to download page'],
message:
$t('TIPS_FIND_NEW_VERSION', {
v: info.version
}) +
'\n\n' +
displayLog +
truncatedNote,
checkboxLabel: $t('NO_MORE_NOTICE'),
checkboxChecked: false
})
.then(result => {
if (result.response === 0) {
updater.autoUpdater.downloadUpdate()
} else {
shell.openExternal('https://github.com/Kuingsmile/PicList/releases/latest')
}
db.set(configPaths.settings.showUpdateTip, !result.checkboxChecked)
})
.catch(err => {
logger.error(err)
})
})
updater.autoUpdater.on('download-progress', progressObj => {
const percent = {
progress: progressObj.percent
}
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send('updateProgress', percent)
})
updater.autoUpdater.on('update-downloaded', () => {
dialog
.showMessageBox({
type: 'info',
title: $t('UPDATE_DOWNLOADED'),
buttons: ['Yes', 'No'],
message: $t('TIPS_UPDATE_DOWNLOADED')
})
.then(result => {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send('updateProgress', { progress: 100 })
if (result.response === 0) {
updater.autoUpdater.quitAndInstall()
}
})
.catch(err => {
logger.error(err)
})
})
updater.autoUpdater.on('error', err => {
console.log(err)
})
class LifeCycle {
async #beforeReady () {
protocol.registerSchemesAsPrivileged([{ scheme: 'picgo', privileges: { secure: true, standard: true } }])
// fix the $PATH in macOS & linux
fixPath()
beforeOpen()
getManageApi()
UpDownTaskQueue.getInstance()
initI18n()
rpcServer.start()
busEventList.listen()
}
#onReady () {
const readyFunction = async () => {
if (process.env.NODE_ENV !== 'production') {
installExtension(VUEJS_DEVTOOLS).then(name => {
console.log(`Added Extension: ${JSON.stringify(name)}`)
}).catch(err => {
console.log('An error occurred: ', err)
})
}
windowManager.create(IWindowList.TRAY_WINDOW)
windowManager.create(IWindowList.SETTING_WINDOW)
const isAutoListenClipboard = db.get(configPaths.settings.isAutoListenClipboard) || false
const ClipboardWatcher = clipboardPoll
if (isAutoListenClipboard) {
db.set(configPaths.settings.isListeningClipboard, true)
ClipboardWatcher.startListening()
ClipboardWatcher.on('change', () => {
picgo.log.info('clipboard changed')
uploadClipboardFiles()
})
} else {
db.set(configPaths.settings.isListeningClipboard, false)
}
const isHideDock = db.get(configPaths.settings.isHideDock) || false
let startMode = db.get(configPaths.settings.startMode) || ISartMode.QUIET
if (process.platform === 'darwin' && startMode === ISartMode.MINI) {
startMode = ISartMode.QUIET
}
const currentPicBed = db.get(configPaths.picBed.uploader) || db.get(configPaths.picBed.current) || 'smms'
const currentPicBedConfig = db.get(`picBed.${currentPicBed}`)?._configName || 'Default'
const tooltip = `${currentPicBed} ${currentPicBedConfig}`
if (process.platform === 'darwin') {
isHideDock ? app.dock?.hide() : setDockMenu()
startMode !== ISartMode.NO_TRAY && createTray(tooltip)
} else {
createTray(tooltip)
}
db.set(configPaths.needReload, false)
updateChecker()
// 不需要阻塞
process.nextTick(() => {
shortKeyHandler.init()
})
server.startup()
webServer.start()
startFileServer()
if (process.env.NODE_ENV !== 'development') {
handleStartUpFiles(process.argv, process.cwd())
}
if (notificationList && notificationList.length > 0) {
while (notificationList.length) {
const option = notificationList.pop()
const notice = new Notification(option!)
notice.show()
}
}
await remoteNoticeHandler.init()
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.APP_START)
if (startMode === ISartMode.MINI && process.platform !== 'darwin') {
windowManager.create(IWindowList.MINI_WINDOW)
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!
miniWindow.removeAllListeners()
if (db.get(configPaths.settings.miniWindowOntop)) {
miniWindow.setAlwaysOnTop(true)
}
const { width, height } = screen.getPrimaryDisplay().workAreaSize
const lastPosition = db.get(configPaths.settings.miniWindowPosition)
if (lastPosition) {
if (lastPosition[0] < 0 || lastPosition[0] > width || lastPosition[1] < 0 || lastPosition[1] > height) {
miniWindow.setPosition(width - 100, height - 100)
db.set(configPaths.settings.miniWindowPosition, [width - 100, height - 100])
} else if (lastPosition[0] + miniWindow.getSize()[0] > width || lastPosition[1] + miniWindow.getSize()[1] > height) {
miniWindow.setPosition(width - miniWindow.getSize()[0], height - miniWindow.getSize()[1])
db.set(configPaths.settings.miniWindowPosition, [width - miniWindow.getSize()[0], height - miniWindow.getSize()[1]])
} else {
miniWindow.setPosition(lastPosition[0], lastPosition[1])
}
} else {
miniWindow.setPosition(width - 100, height - 100)
}
const setPositionFunc = () => {
const position = miniWindow.getPosition()
db.set(configPaths.settings.miniWindowPosition, position)
}
miniWindow.on('close', setPositionFunc)
miniWindow.on('move', setPositionFunc)
miniWindow.show()
miniWindow.focus()
} else if (startMode === ISartMode.MAIN) {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
settingWindow.show()
settingWindow.focus()
}
const clipboardDir = path.join(picgo.baseDir, CLIPBOARD_IMAGE_FOLDER)
fs.emptyDir(clipboardDir)
}
app.whenReady().then(readyFunction)
}
#onRunning () {
app.on('second-instance', (_, commandLine, workingDirectory) => {
logger.info('detect second instance')
const result = handleStartUpFiles(commandLine, workingDirectory)
if (!result) {
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
if (settingWindow.isMinimized()) {
settingWindow.restore()
}
settingWindow.focus()
}
}
})
app.on('activate', () => {
if (!windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.create(IWindowList.TRAY_WINDOW)
}
if (!windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.create(IWindowList.SETTING_WINDOW)
}
})
app.setLoginItemSettings({
openAtLogin: db.get(configPaths.settings.autoStart) || false
})
if (process.platform === 'win32') {
app.setAppUserModelId('com.kuingsmile.piclist')
}
if (process.env.XDG_CURRENT_DESKTOP && process.env.XDG_CURRENT_DESKTOP.includes('Unity')) {
process.env.XDG_CURRENT_DESKTOP = 'Unity'
}
}
#onQuit () {
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('will-quit', () => {
UpDownTaskQueue.getInstance().persist()
clearTempFolder()
globalShortcut.unregisterAll()
bus.removeAllListeners()
server.shutdown()
webServer.stop()
stopFileServer()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
}
async launchApp () {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
await this.#beforeReady()
this.#onReady()
this.#onRunning()
this.#onQuit()
}
}
}
const bootstrap = new LifeCycle()
export { bootstrap }
import '~/lifeCycle/errorHandler'
import path from 'node:path'
import bus from '@core/bus'
import db from '@core/datastore'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import { remoteNoticeHandler } from 'apis/app/remoteNotice'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import { createTray, setDockMenu } from 'apis/app/system'
import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
import windowManager from 'apis/app/window/windowManager'
import axios from 'axios'
import { app, dialog, globalShortcut, Notification, protocol, screen, shell } from 'electron'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import updater from 'electron-updater'
import fs from 'fs-extra'
import busEventList from '~/events/busEventList'
import { rpcServer } from '~/events/rpc'
import { startFileServer, stopFileServer } from '~/fileServer'
import { T as $t } from '~/i18n'
import fixPath from '~/lifeCycle/fixPath'
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
import getManageApi from '~/manage/Main'
import { clearTempFolder } from '~/manage/utils/common'
import server from '~/server/index'
import webServer from '~/server/webServer'
import beforeOpen from '~/utils/beforeOpen'
import clipboardPoll from '~/utils/clipboardPoll'
import { configPaths } from '~/utils/configPaths'
import { II18nLanguage, IRemoteNoticeTriggerHook, ISartMode, IWindowList } from '~/utils/enum'
import { getUploadFiles } from '~/utils/handleArgv'
import { initI18n } from '~/utils/handleI18n'
import { notificationList } from '~/utils/notification'
import { CLIPBOARD_IMAGE_FOLDER } from '~/utils/static'
import updateChecker from '~/utils/updateChecker'
const isDevelopment = process.env.NODE_ENV !== 'production'
const handleStartUpFiles = (argv: string[], cwd: string) => {
const files = getUploadFiles(argv, cwd, logger)
if (files === null) {
logger.info('cli -> uploading file from clipboard')
uploadClipboardFiles()
return true
}
if (files.length > 0) {
logger.info('cli -> uploading files from cli', ...files.map(file => file.path))
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
return true
}
return false
}
updater.autoUpdater.setFeedURL({
provider: 'generic',
url: 'https://release.piclist.cn/latest',
channel: 'latest'
})
updater.autoUpdater.autoDownload = false
updater.autoUpdater.on('update-available', async (info: updater.UpdateInfo) => {
const lang = db.get(configPaths.settings.language) || II18nLanguage.ZH_CN
let updateLog = ''
try {
const url =
lang === II18nLanguage.ZH_CN
? 'https://release.piclist.cn/currentVersion.md'
: 'https://release.piclist.cn/currentVersion_en.md'
const res = await axios.get(url)
updateLog = res.data
} catch (e: any) {
logger.error(e)
}
const maxLogLength = 800
let displayLog = updateLog
let truncatedNote = ''
if (updateLog.length > maxLogLength) {
const truncatePoint = updateLog.lastIndexOf('\n', maxLogLength)
displayLog = updateLog.substring(0, truncatePoint > 0 ? truncatePoint : maxLogLength)
truncatedNote =
lang === II18nLanguage.ZH_CN
? '\n\n... (更多详情请查看完整更新日志)'
: '\n\n... (See full changelog for more details)'
}
dialog
.showMessageBox({
type: 'info',
title: $t('FIND_NEW_VERSION'),
buttons: ['Yes', 'Go to download page'],
message:
$t('TIPS_FIND_NEW_VERSION', {
v: info.version
}) +
'\n\n' +
displayLog +
truncatedNote,
checkboxLabel: $t('NO_MORE_NOTICE'),
checkboxChecked: false
})
.then(result => {
if (result.response === 0) {
updater.autoUpdater.downloadUpdate()
} else {
shell.openExternal('https://github.com/Kuingsmile/PicList/releases/latest')
}
db.set(configPaths.settings.showUpdateTip, !result.checkboxChecked)
})
.catch(err => {
logger.error(err)
})
})
updater.autoUpdater.on('download-progress', progressObj => {
const percent = {
progress: progressObj.percent
}
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send('updateProgress', percent)
})
updater.autoUpdater.on('update-downloaded', () => {
dialog
.showMessageBox({
type: 'info',
title: $t('UPDATE_DOWNLOADED'),
buttons: ['Yes', 'No'],
message: $t('TIPS_UPDATE_DOWNLOADED')
})
.then(result => {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send('updateProgress', { progress: 100 })
if (result.response === 0) {
updater.autoUpdater.quitAndInstall()
}
})
.catch(err => {
logger.error(err)
})
})
updater.autoUpdater.on('error', err => {
console.log(err)
})
class LifeCycle {
async #beforeReady () {
protocol.registerSchemesAsPrivileged([{ scheme: 'picgo', privileges: { secure: true, standard: true } }])
// fix the $PATH in macOS & linux
fixPath()
beforeOpen()
getManageApi()
UpDownTaskQueue.getInstance()
initI18n()
rpcServer.start()
busEventList.listen()
}
#onReady () {
const readyFunction = async () => {
if (process.env.NODE_ENV !== 'production') {
installExtension(VUEJS_DEVTOOLS).then(name => {
console.log(`Added Extension: ${JSON.stringify(name)}`)
}).catch(err => {
console.log('An error occurred: ', err)
})
}
windowManager.create(IWindowList.TRAY_WINDOW)
windowManager.create(IWindowList.SETTING_WINDOW)
const isAutoListenClipboard = db.get(configPaths.settings.isAutoListenClipboard) || false
const ClipboardWatcher = clipboardPoll
if (isAutoListenClipboard) {
db.set(configPaths.settings.isListeningClipboard, true)
ClipboardWatcher.startListening()
ClipboardWatcher.on('change', () => {
picgo.log.info('clipboard changed')
uploadClipboardFiles()
})
} else {
db.set(configPaths.settings.isListeningClipboard, false)
}
const isHideDock = db.get(configPaths.settings.isHideDock) || false
let startMode = db.get(configPaths.settings.startMode) || ISartMode.QUIET
if (process.platform === 'darwin' && startMode === ISartMode.MINI) {
startMode = ISartMode.QUIET
}
const currentPicBed = db.get(configPaths.picBed.uploader) || db.get(configPaths.picBed.current) || 'smms'
const currentPicBedConfig = db.get(`picBed.${currentPicBed}`)?._configName || 'Default'
const tooltip = `${currentPicBed} ${currentPicBedConfig}`
if (process.platform === 'darwin') {
isHideDock ? app.dock?.hide() : setDockMenu()
startMode !== ISartMode.NO_TRAY && createTray(tooltip)
} else {
createTray(tooltip)
}
db.set(configPaths.needReload, false)
updateChecker()
// 不需要阻塞
process.nextTick(() => {
shortKeyHandler.init()
})
server.startup()
webServer.start()
startFileServer()
if (process.env.NODE_ENV !== 'development') {
handleStartUpFiles(process.argv, process.cwd())
}
if (notificationList && notificationList.length > 0) {
while (notificationList.length) {
const option = notificationList.pop()
const notice = new Notification(option!)
notice.show()
}
}
await remoteNoticeHandler.init()
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.APP_START)
if (startMode === ISartMode.MINI && process.platform !== 'darwin') {
windowManager.create(IWindowList.MINI_WINDOW)
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!
miniWindow.removeAllListeners()
if (db.get(configPaths.settings.miniWindowOntop)) {
miniWindow.setAlwaysOnTop(true)
}
const { width, height } = screen.getPrimaryDisplay().workAreaSize
const lastPosition = db.get(configPaths.settings.miniWindowPosition)
if (lastPosition) {
if (lastPosition[0] < 0 || lastPosition[0] > width || lastPosition[1] < 0 || lastPosition[1] > height) {
miniWindow.setPosition(width - 100, height - 100)
db.set(configPaths.settings.miniWindowPosition, [width - 100, height - 100])
} else if (lastPosition[0] + miniWindow.getSize()[0] > width || lastPosition[1] + miniWindow.getSize()[1] > height) {
miniWindow.setPosition(width - miniWindow.getSize()[0], height - miniWindow.getSize()[1])
db.set(configPaths.settings.miniWindowPosition, [width - miniWindow.getSize()[0], height - miniWindow.getSize()[1]])
} else {
miniWindow.setPosition(lastPosition[0], lastPosition[1])
}
} else {
miniWindow.setPosition(width - 100, height - 100)
}
const setPositionFunc = () => {
const position = miniWindow.getPosition()
db.set(configPaths.settings.miniWindowPosition, position)
}
miniWindow.on('close', setPositionFunc)
miniWindow.on('move', setPositionFunc)
miniWindow.show()
miniWindow.focus()
} else if (startMode === ISartMode.MAIN) {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
settingWindow.show()
settingWindow.focus()
}
const clipboardDir = path.join(picgo.baseDir, CLIPBOARD_IMAGE_FOLDER)
fs.emptyDir(clipboardDir)
}
app.whenReady().then(readyFunction)
}
#onRunning () {
app.on('second-instance', (_, commandLine, workingDirectory) => {
logger.info('detect second instance')
const result = handleStartUpFiles(commandLine, workingDirectory)
if (!result) {
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
if (settingWindow.isMinimized()) {
settingWindow.restore()
}
settingWindow.focus()
}
}
})
app.on('activate', () => {
if (!windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.create(IWindowList.TRAY_WINDOW)
}
if (!windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.create(IWindowList.SETTING_WINDOW)
}
})
app.setLoginItemSettings({
openAtLogin: db.get(configPaths.settings.autoStart) || false
})
if (process.platform === 'win32') {
app.setAppUserModelId('com.kuingsmile.piclist')
}
if (process.env.XDG_CURRENT_DESKTOP && process.env.XDG_CURRENT_DESKTOP.includes('Unity')) {
process.env.XDG_CURRENT_DESKTOP = 'Unity'
}
}
#onQuit () {
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('will-quit', () => {
UpDownTaskQueue.getInstance().persist()
clearTempFolder()
globalShortcut.unregisterAll()
bus.removeAllListeners()
server.shutdown()
webServer.stop()
stopFileServer()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
}
async launchApp () {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
await this.#beforeReady()
this.#onReady()
this.#onRunning()
this.#onQuit()
}
}
}
const lifeCycle = new LifeCycle()
export { lifeCycle }

View File

@@ -1,33 +1,75 @@
import crypto from 'node:crypto'
import picgo from '@core/picgo'
import { configPaths } from '~/utils/configPaths'
export class AESHelper {
private key: Buffer = crypto.pbkdf2Sync(
picgo.getConfig<string>(configPaths.settings.aesPassword) || 'aesPassword',
Buffer.from('a8b3c4d2e4f5098712345678feedc0de', 'hex'),
100000,
32,
'sha512'
)
encrypt (plainText: string) {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv)
let encrypted = cipher.update(plainText, 'utf8', 'hex')
encrypted += cipher.final('hex')
return `${iv.toString('hex')}:${encrypted}`
}
decrypt (encryptedData: string) {
const [ivHex, encryptedText] = encryptedData.split(':')
if (!ivHex || !encryptedText) return '{}'
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, Buffer.from(ivHex, 'hex'))
let decrypted = decipher.update(encryptedText, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
}
import crypto from 'node:crypto'
import picgo from '@core/picgo'
import { configPaths } from '~/utils/configPaths'
export class AESHelper {
static readonly #SALT = Buffer.from('a8b3c4d2e4f5098712345678feedc0de', 'hex')
static readonly #ITERATIONS = 100_000
static readonly #KEYLEN = 32
static readonly #DIGEST = 'sha512' as const
static readonly #ALGO = 'aes-256-cbc'
static readonly #IV_LENGTH = 16
static readonly #SEP = ':'
static #keyCache = new Map<string, Buffer>()
readonly key: Buffer
constructor (password?: string) {
const pwd =
password ??
picgo.getConfig<string>(configPaths.settings.aesPassword) ??
'aesPassword'
this.key = AESHelper.#deriveKey(pwd)
}
static #deriveKey (password: string): Buffer {
const cached = this.#keyCache.get(password)
if (cached) return cached
const key = crypto.pbkdf2Sync(
password,
this.#SALT,
this.#ITERATIONS,
this.#KEYLEN,
this.#DIGEST
)
this.#keyCache.set(password, key)
return key
}
encrypt (plainText: string): string {
const iv = crypto.randomBytes(AESHelper.#IV_LENGTH)
const cipher = crypto.createCipheriv(AESHelper.#ALGO, this.key, iv)
const encrypted = Buffer.concat([
cipher.update(plainText, 'utf8'),
cipher.final()
])
return `${iv.toString('hex')}${AESHelper.#SEP}${encrypted.toString('hex')}`
}
decrypt (encryptedData: string): string {
if (!encryptedData) return '{}'
const sepIndex = encryptedData.indexOf(AESHelper.#SEP)
if (sepIndex <= 0) return '{}'
const ivHex = encryptedData.slice(0, sepIndex)
const encryptedHex = encryptedData.slice(sepIndex + 1)
try {
const iv = Buffer.from(ivHex, 'hex')
if (iv.length !== AESHelper.#IV_LENGTH) return '{}'
const decipher = crypto.createDecipheriv(AESHelper.#ALGO, this.key, iv)
const decrypted = Buffer.concat([
decipher.update(Buffer.from(encryptedHex, 'hex')),
decipher.final()
])
return decrypted.toString('utf8')
} catch {
return '{}'
}
}
}