mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-06-03 14:50:57 +08:00
@@ -26,6 +26,7 @@ import { configPaths } from '~/utils/configPaths'
|
||||
import { IPasteStyle, IWindowList } from '~/utils/enum'
|
||||
import { isMacOSVersionGreaterThanOrEqualTo } from '~/utils/getMacOSVersion'
|
||||
import pasteTemplate from '~/utils/pasteTemplate'
|
||||
import { runScriptInStage } from '~/utils/runScript'
|
||||
import { hideMiniWindow, openMainWindow, openMiniWindow } from '~/utils/windowHelper'
|
||||
|
||||
import menubarPng from '../../../../../resources/menubar.png?asset&asarUnpack'
|
||||
@@ -308,8 +309,8 @@ export function createTray(tooltip: string) {
|
||||
const rawInput = cloneDeep(files)
|
||||
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)
|
||||
const res = await uploader.setWebContents(trayWindow?.webContents).uploadReturnCtx(files)
|
||||
const imgs = res[0] ? res[0] : false
|
||||
const backImgs = res[1] ? res[1] : false
|
||||
const imgs = res.ctx?.output ? res.ctx.output : false
|
||||
const backImgs = res.backupCtx?.output ? res.backupCtx.output : false
|
||||
const deleteLocalFile = allConfig.settings?.deleteLocalFile || false
|
||||
if (imgs !== false) {
|
||||
const pasteText: string[] = []
|
||||
@@ -334,7 +335,8 @@ export function createTray(tooltip: string) {
|
||||
notification.show()
|
||||
}, i * 100)
|
||||
}
|
||||
await GalleryDB.getInstance().insert(imgs[i])
|
||||
const inserted = await GalleryDB.getInstance().insert(imgs[i])
|
||||
runScriptInStage('onUploadSuccess', res.ctx || picgo, { galleryItem: inserted })
|
||||
}
|
||||
handleCopyUrl(pasteText.join('\n'))
|
||||
trayWindow?.webContents.send('dragFiles', imgs)
|
||||
|
||||
@@ -1,46 +1,56 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { themesDir } from '@core/datastore/dirs'
|
||||
import * as fsWalk from '@nodelib/fs.walk'
|
||||
import AdmZip from 'adm-zip'
|
||||
import axios from 'axios'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
import { randomStringGenerator } from '@/manage/utils/common'
|
||||
import logger from '~/apis/core/picgo/logger'
|
||||
import { IWindowList } from '~/utils/enum'
|
||||
|
||||
import windowManager from '../window/windowManager'
|
||||
|
||||
export async function resolveThemes(): Promise<{ key: string; label: string }[]> {
|
||||
const files = fsWalk.walkSync(themesDir(), {
|
||||
followSymbolicLinks: true,
|
||||
fs,
|
||||
stats: true,
|
||||
throwErrorOnBrokenSymbolicLink: false,
|
||||
})
|
||||
const result: string[] = []
|
||||
files.forEach(item => {
|
||||
if (item.stats?.isFile()) {
|
||||
result.push(path.basename(item.path))
|
||||
}
|
||||
})
|
||||
const themes = await Promise.all(
|
||||
result
|
||||
.filter(file => file.endsWith('.css'))
|
||||
.map(async file => {
|
||||
const css = (await fs.readFile(path.join(themesDir(), file), 'utf-8')) || ''
|
||||
let name = file
|
||||
if (css.startsWith('/*')) {
|
||||
name = css.split('\n')[0].replace('/*', '').replace('*/', '').trim() || file
|
||||
const dir = themesDir()
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const themePromises = entries
|
||||
.filter(entry => entry.isFile() && entry.name.endsWith('.css'))
|
||||
.map(async entry => {
|
||||
const filePath = path.join(dir, entry.name)
|
||||
|
||||
let label = entry.name
|
||||
let fd
|
||||
try {
|
||||
fd = await fs.open(filePath, 'r')
|
||||
const buffer = Buffer.alloc(256)
|
||||
const { bytesRead } = await fs.read(fd, buffer, 0, 256, 0)
|
||||
const content = buffer.toString('utf8', 0, bytesRead)
|
||||
const match = content.match(/^\/\*\s*(.*?)\s*\*\//)
|
||||
if (match && match[1]) {
|
||||
label = match[1].trim()
|
||||
}
|
||||
return { key: file, label: name }
|
||||
}),
|
||||
)
|
||||
if (themes.find(theme => theme.key === 'default.css')) {
|
||||
return themes
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
} finally {
|
||||
if (fd !== undefined) await fs.close(fd)
|
||||
}
|
||||
|
||||
return { key: entry.name, label }
|
||||
})
|
||||
|
||||
const themes = await Promise.all(themePromises)
|
||||
|
||||
const hasDefault = themes.some(t => t.key === 'default.css')
|
||||
if (!hasDefault) {
|
||||
themes.unshift({ key: 'default.css', label: '默认' })
|
||||
} else {
|
||||
return [{ key: 'default.css', label: '默认' }, ...themes]
|
||||
const idx = themes.findIndex(t => t.key === 'default.css')
|
||||
const [defaultTheme] = themes.splice(idx, 1)
|
||||
themes.unshift(defaultTheme)
|
||||
}
|
||||
|
||||
return themes
|
||||
}
|
||||
|
||||
export async function fetchThemes(): Promise<boolean> {
|
||||
|
||||
@@ -11,8 +11,9 @@ import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/utils/common'
|
||||
import { configPaths } from '~/utils/configPaths'
|
||||
import { IPasteStyle, IWindowList } from '~/utils/enum'
|
||||
import pasteTemplate from '~/utils/pasteTemplate'
|
||||
import { runScriptInStage } from '~/utils/runScript'
|
||||
|
||||
const handleClipboardUploadingReturnCtx = async (img?: IUploadOption): Promise<(ImgInfo[] | false)[]> => {
|
||||
const handleClipboardUploadingReturnCtx = async (img?: IUploadOption): Promise<IuploadReturnCtxResult> => {
|
||||
const useBuiltinClipboardConfig = picgo.getConfig<boolean | undefined>(configPaths.settings.useBuiltinClipboard)
|
||||
const useBuiltinClipboard = useBuiltinClipboardConfig === undefined ? true : !!useBuiltinClipboardConfig
|
||||
const win = windowManager.getAvailableWindow()
|
||||
@@ -26,8 +27,8 @@ export const uploadClipboardFiles = async (): Promise<IStringKeyMap> => {
|
||||
let img: ImgInfo[] | false = false
|
||||
let backImg: ImgInfo[] | false = false
|
||||
const res = await handleClipboardUploadingReturnCtx()
|
||||
img = res[0] ? res[0] : false
|
||||
backImg = res[1] ? res[1] : false
|
||||
img = res.ctx?.output ? res.ctx.output : false
|
||||
backImg = res.backupCtx?.output ? res.backupCtx.output : false
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
if (img !== false) {
|
||||
if (img.length > 0) {
|
||||
@@ -50,6 +51,7 @@ export const uploadClipboardFiles = async (): Promise<IStringKeyMap> => {
|
||||
}, 100)
|
||||
}
|
||||
const inserted = await GalleryDB.getInstance().insert(img[0])
|
||||
runScriptInStage('onUploadSuccess', res.ctx || picgo, { galleryItem: inserted })
|
||||
// trayWindow just be created in mac/windows, not in linux
|
||||
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)
|
||||
trayWindow?.webContents?.send('clipboardFiles', [])
|
||||
@@ -93,8 +95,8 @@ export const uploadChoosedFiles = async (
|
||||
let imgs: ImgInfo[] | false = false
|
||||
let backImgs: ImgInfo[] | false = false
|
||||
const res = await uploader.setWebContents(webContents).uploadReturnCtx(input)
|
||||
imgs = res[0] ? res[0] : false
|
||||
backImgs = res[1] ? res[1] : false
|
||||
imgs = res.ctx?.output ? res.ctx.output : false
|
||||
backImgs = res.backupCtx?.output ? res.backupCtx.output : false
|
||||
const result = []
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
if (imgs !== false) {
|
||||
@@ -140,6 +142,7 @@ export const uploadChoosedFiles = async (
|
||||
}
|
||||
}
|
||||
const inserted = await GalleryDB.getInstance().insert(imgs[i])
|
||||
runScriptInStage('onUploadSuccess', res.ctx || picgo, { galleryItem: inserted })
|
||||
result.push({
|
||||
url: handleUrlEncodeWithSetting(inserted.imgUrl!),
|
||||
fullResult: inserted,
|
||||
|
||||
@@ -118,15 +118,15 @@ class Uploader {
|
||||
return filePath
|
||||
}
|
||||
|
||||
async uploadWithBuildInClipboardReturnCtx(img?: IUploadOption): Promise<(ImgInfo[] | false)[]> {
|
||||
async uploadWithBuildInClipboardReturnCtx(img?: IUploadOption): Promise<IuploadReturnCtxResult> {
|
||||
let imgPath: string | false = false
|
||||
try {
|
||||
imgPath = await this.getClipboardImagePath()
|
||||
if (!imgPath) return [false, false]
|
||||
if (!imgPath) return { ctx: undefined, backupCtx: undefined }
|
||||
return await this.uploadReturnCtx(img ?? [imgPath])
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
return [false, false]
|
||||
return { ctx: undefined, backupCtx: undefined }
|
||||
} finally {
|
||||
if (imgPath && imgPath.startsWith(path.join(picgo.baseDir, CLIPBOARD_IMAGE_FOLDER))) {
|
||||
fs.remove(imgPath)
|
||||
@@ -134,24 +134,24 @@ class Uploader {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadReturnCtx(img?: IUploadOption): Promise<(ImgInfo[] | false)[]> {
|
||||
async uploadReturnCtx(img?: IUploadOption): Promise<IuploadReturnCtxResult> {
|
||||
try {
|
||||
const result = [false, false] as (ImgInfo[] | false)[]
|
||||
const result = { ctx: undefined, backupCtx: undefined } as IuploadReturnCtxResult
|
||||
const res = await picgo.uploadReturnCtx(img)
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
|
||||
if (Array.isArray(res.output) && res.output.some((item: ImgInfo) => item.imgUrl)) {
|
||||
res.output.forEach((item: ImgInfo) => {
|
||||
if (Array.isArray(res.ctx?.output) && res.ctx?.output.some((item: ImgInfo) => item.imgUrl)) {
|
||||
res.ctx.output.forEach((item: ImgInfo) => {
|
||||
item.config = JSON.parse(JSON.stringify(allConfig.picBed?.[item.type!]))
|
||||
})
|
||||
result[0] = res.output
|
||||
result.ctx = res.ctx
|
||||
}
|
||||
|
||||
if (Array.isArray(res.backupOutput) && res.backupOutput.some((item: ImgInfo) => item.imgUrl)) {
|
||||
res.backupOutput.forEach((item: ImgInfo) => {
|
||||
if (Array.isArray(res.backupCtx?.output) && res.backupCtx?.output.some((item: ImgInfo) => item.imgUrl)) {
|
||||
res.backupCtx.output.forEach((item: ImgInfo) => {
|
||||
item.config = JSON.parse(JSON.stringify(allConfig.picBed?.[item.type!]))
|
||||
})
|
||||
result[1] = res.backupOutput
|
||||
result.backupCtx = res.backupCtx
|
||||
}
|
||||
return result
|
||||
} catch (e: any) {
|
||||
@@ -163,7 +163,7 @@ class Uploader {
|
||||
clickToCopy: true,
|
||||
})
|
||||
}, 500)
|
||||
return [false, false]
|
||||
return { ctx: undefined, backupCtx: undefined } as IuploadReturnCtxResult
|
||||
} finally {
|
||||
ipcMain.removeAllListeners(GET_RENAME_FILE_NAME)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ export function defaultDir() {
|
||||
return userDataDir()
|
||||
}
|
||||
|
||||
export function scriptsDir() {
|
||||
return path.join(dataDir(), 'scripts')
|
||||
}
|
||||
|
||||
export function defaultConfigPath() {
|
||||
if (isPortable()) {
|
||||
return path.join(exeDir(), 'data', 'data.json')
|
||||
|
||||
@@ -13,6 +13,7 @@ import { T as $t } from '~/i18n'
|
||||
import { handleCopyUrl } from '~/utils/common'
|
||||
import { IPasteStyle } from '~/utils/enum'
|
||||
import pasteTemplate from '~/utils/pasteTemplate'
|
||||
import { runScriptInStage } from '~/utils/runScript'
|
||||
|
||||
// Cross-process support may be required in the future
|
||||
class GuiApi implements IGuiApi {
|
||||
@@ -74,8 +75,8 @@ class GuiApi implements IGuiApi {
|
||||
const webContents = this.getWebcontentsByWindowId(this.windowId)
|
||||
const rawInput = cloneDeep(input)
|
||||
const res = await uploader.setWebContents(webContents!).uploadReturnCtx(input)
|
||||
const imgs = res[0] ? res[0] : false
|
||||
const backImgs = res[1] ? res[1] : false
|
||||
const imgs = res.ctx?.output ? res.ctx.output : false
|
||||
const backImgs = res.backupCtx?.output ? res.backupCtx.output : false
|
||||
let result: ImgInfo[] = []
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
if (imgs !== false) {
|
||||
@@ -103,7 +104,8 @@ class GuiApi implements IGuiApi {
|
||||
notification.show()
|
||||
}, i * 100)
|
||||
}
|
||||
await GalleryDB.getInstance().insert(imgs[i])
|
||||
const inserted = await GalleryDB.getInstance().insert(imgs[i])
|
||||
runScriptInStage('onUploadSuccess', res.ctx || picgo, { galleryItem: inserted })
|
||||
}
|
||||
handleCopyUrl(pasteText.join('\n'))
|
||||
webContents?.send('uploadFiles')
|
||||
|
||||
@@ -6,6 +6,7 @@ import { clipboard } from 'electron'
|
||||
import { RPCRouter } from '~/events/rpc/router'
|
||||
import { ICOREBuildInEvent, IPasteStyle, IRPCActionType, IRPCType } from '~/utils/enum'
|
||||
import pasteTemplate from '~/utils/pasteTemplate'
|
||||
import { runScriptInStage } from '~/utils/runScript'
|
||||
interface IFilter {
|
||||
orderBy?: 'asc' | 'desc'
|
||||
limit?: number
|
||||
@@ -38,6 +39,7 @@ const galleryRoutes = [
|
||||
action: IRPCActionType.GALLERY_REMOVE_FILES,
|
||||
handler: async (_: IIPCEvent, args: [files: ImgInfo[]]) => {
|
||||
setTimeout(() => {
|
||||
runScriptInStage('onGalleryRemove', picgo, { galleryItem: args[0] })
|
||||
picgo.emit(ICOREBuildInEvent.REMOVE, args[0], GuiApi.getInstance())
|
||||
}, 500)
|
||||
},
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { dataDir } from '@core/datastore/dirs'
|
||||
import { dataDir, scriptsDir } from '@core/datastore/dirs'
|
||||
import picgo from '@core/picgo'
|
||||
import { IpcMainEvent, shell } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
import logger from '~/apis/core/picgo/logger'
|
||||
import { isAutoStartEnabled, setAutoStart } from '~/utils/autoStart'
|
||||
import { getDirectoryTree } from '~/utils/common'
|
||||
import { IRPCActionType, IRPCType } from '~/utils/enum'
|
||||
import { runScript } from '~/utils/runScript'
|
||||
|
||||
const STORE_PATH = dataDir()
|
||||
|
||||
@@ -57,9 +60,110 @@ export default [
|
||||
action: IRPCActionType.WRITE_FILE_CONTENT,
|
||||
handler: async (_: IIPCEvent, args: [fileName: string, content: string]) => {
|
||||
const abFilePath = path.join(STORE_PATH, args[0])
|
||||
fs.ensureDirSync(path.dirname(abFilePath))
|
||||
fs.writeFileSync(abFilePath, args[1], 'utf-8')
|
||||
},
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.RUN_SCRIPT_FILE,
|
||||
handler: async (_: IIPCEvent, args: [fileName: string[]]) => {
|
||||
const abFilePath = path.join(scriptsDir(), ...args[0])
|
||||
if (!fs.existsSync(abFilePath)) {
|
||||
throw new Error('Script file does not exist')
|
||||
}
|
||||
const scriptContent = fs.readFileSync(abFilePath, 'utf-8')
|
||||
try {
|
||||
await runScript(picgo, scriptContent, {})
|
||||
logger.info(`Script ${args[0].join('/')} executed successfully`)
|
||||
return 'Script executed successfully'
|
||||
} catch (e) {
|
||||
return Error(`Script execution failed: ${e}`)
|
||||
}
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.CREATE_SCRIPTS_FILE,
|
||||
handler: async (_: IIPCEvent, args: [fileName: string[], content: string]) => {
|
||||
const abFilePath = path.join(scriptsDir(), ...args[0])
|
||||
fs.ensureDirSync(path.dirname(abFilePath))
|
||||
fs.writeFileSync(abFilePath, args[1], 'utf-8')
|
||||
},
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.READ_SCRIPTS_FILE,
|
||||
handler: async (_: IIPCEvent, args: [fileName: string[]]) => {
|
||||
const abFilePath = path.join(scriptsDir(), ...args[0])
|
||||
if (!fs.existsSync(abFilePath)) {
|
||||
return null
|
||||
}
|
||||
return fs.readFileSync(abFilePath, 'utf-8')
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.LIST_SCRIPTS_FILES,
|
||||
handler: async (_: IIPCEvent, args: [dirPath: string[]]) => {
|
||||
const dir = scriptsDir()
|
||||
const targetDir = path.join(dir, ...(args[0] || []))
|
||||
|
||||
if (!(await fs.pathExists(targetDir))) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
return await getDirectoryTree(targetDir)
|
||||
} catch (error) {
|
||||
console.error('Failed to list scripts:', error)
|
||||
return {}
|
||||
}
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.WRITE_SCRIPT_FILE,
|
||||
handler: async (_: IIPCEvent, args: [fileName: string[], content: string]) => {
|
||||
const abFilePath = path.join(scriptsDir(), ...args[0])
|
||||
fs.ensureDirSync(path.dirname(abFilePath))
|
||||
fs.writeFileSync(abFilePath, args[1], 'utf-8')
|
||||
},
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.DELETE_SCRIPTS_FILE,
|
||||
handler: async (_: IIPCEvent, args: [fileName: string[]]) => {
|
||||
const abFilePath = path.join(scriptsDir(), ...args[0])
|
||||
if (fs.existsSync(abFilePath)) {
|
||||
fs.unlinkSync(abFilePath)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.GET_FILES_STAT,
|
||||
handler: async (_: IIPCEvent, [filePaths, type]: [string[][], 'scripts' | 'config']) => {
|
||||
const basePath = type === 'scripts' ? scriptsDir() : STORE_PATH
|
||||
const result: IObj[] = []
|
||||
const statPromises = filePaths.map(async filePath => {
|
||||
const absolutePath = path.join(basePath, ...filePath)
|
||||
try {
|
||||
return await fs.promises.stat(absolutePath)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
const statsResults = await Promise.all(statPromises)
|
||||
|
||||
for (const [index, item] of filePaths.entries()) {
|
||||
result.push({
|
||||
fileName: item[item.length - 1],
|
||||
stats: statsResults[index],
|
||||
category: item.length > 1 ? item.slice(0, -1).join('.') : '',
|
||||
filePath: item,
|
||||
})
|
||||
}
|
||||
return result
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.PICLIST_OPEN_DIRECTORY,
|
||||
handler: async (_: IIPCEvent, args: [dirPath?: string, inStorePath?: boolean]) => {
|
||||
|
||||
@@ -32,8 +32,8 @@ const trayRoutes = [
|
||||
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)
|
||||
// macOS use builtin clipboard is OK
|
||||
const res = await uploader.setWebContents(trayWindow?.webContents).uploadWithBuildInClipboardReturnCtx()
|
||||
const img = res[0] ? res[0] : false
|
||||
const backupImgs = res[1] ? res[1] : false
|
||||
const img = res.ctx?.output ? res.ctx.output : false
|
||||
const backupImgs = res.backupCtx?.output ? res.backupCtx.output : false
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
if (img !== false) {
|
||||
const pasteStyle = allConfig.settings?.pasteStyle || IPasteStyle.MARKDOWN
|
||||
|
||||
@@ -34,7 +34,7 @@ import { IRemoteNoticeTriggerHook, ISartMode, IWindowList } from '~/utils/enum'
|
||||
import { getUploadFiles } from '~/utils/handleArgv'
|
||||
import { initI18n } from '~/utils/handleI18n'
|
||||
import { notificationList } from '~/utils/notification'
|
||||
import { MemoryMonitor } from '~/utils/performanceOptimizer'
|
||||
import { runScriptInStage } from '~/utils/runScript'
|
||||
import { CLIPBOARD_IMAGE_FOLDER } from '~/utils/static'
|
||||
import updateChecker from '~/utils/updateChecker'
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
@@ -146,9 +146,6 @@ class LifeCycle {
|
||||
notice.show()
|
||||
}
|
||||
}
|
||||
if (isDevelopment) {
|
||||
MemoryMonitor.start()
|
||||
}
|
||||
await remoteNoticeHandler.init()
|
||||
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.APP_START)
|
||||
if (startMode === ISartMode.MINI && process.platform !== 'darwin') {
|
||||
@@ -196,6 +193,7 @@ class LifeCycle {
|
||||
}
|
||||
const clipboardDir = path.join(picgo.baseDir, CLIPBOARD_IMAGE_FOLDER)
|
||||
fs.emptyDir(clipboardDir)
|
||||
runScriptInStage('onSoftwareOpen', picgo, {})
|
||||
}
|
||||
app.whenReady().then(readyFunction)
|
||||
}
|
||||
@@ -251,7 +249,7 @@ class LifeCycle {
|
||||
server.shutdown()
|
||||
webServer.stop()
|
||||
stopFileServer()
|
||||
MemoryMonitor.stop()
|
||||
runScriptInStage('onSoftwareClose', picgo, {})
|
||||
})
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDevelopment) {
|
||||
|
||||
@@ -375,3 +375,23 @@ export function getUploaderType(ctx: IPicGo): {
|
||||
const id = picBedConfig._id || ''
|
||||
return { picBed, id }
|
||||
}
|
||||
|
||||
export async function getDirectoryTree(currentPath: string): Promise<Record<string, any>> {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
const entries = await fs.readdir(currentPath, { withFileTypes: true })
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async entry => {
|
||||
const fullPath = path.join(currentPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
result[entry.name] = await getDirectoryTree(fullPath)
|
||||
} else if (entry.isFile()) {
|
||||
result[entry.name] = null
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export const IRPCActionType = {
|
||||
SET_CURRENT_LANGUAGE: 'SET_CURRENT_LANGUAGE',
|
||||
OPEN_WINDOW: 'OPEN_WINDOW',
|
||||
OPEN_MINI_WINDOW: 'OPEN_MINI_WINDOW',
|
||||
RELOAD_WINDOW: 'RELOAD_WINDOW',
|
||||
CLOSE_WINDOW: 'CLOSE_WINDOW',
|
||||
MINIMIZE_WINDOW: 'MINIMIZE_WINDOW',
|
||||
SHOW_MINI_PAGE_MENU: 'SHOW_MINI_PAGE_MENU',
|
||||
@@ -123,9 +124,17 @@ export const IRPCActionType = {
|
||||
PICLIST_OPEN_DIRECTORY: 'PICLIST_OPEN_DIRECTORY',
|
||||
PICLIST_AUTO_START: 'PICLIST_AUTO_START',
|
||||
PICLIST_AUTO_START_STATUS: 'PICLIST_AUTO_START_STATUS',
|
||||
|
||||
// file operation rpc
|
||||
READ_FILE_CONTENT: 'READ_FILE_CONTENT',
|
||||
WRITE_FILE_CONTENT: 'WRITE_FILE_CONTENT',
|
||||
RELOAD_WINDOW: 'RELOAD_WINDOW',
|
||||
CREATE_SCRIPTS_FILE: 'CREATE_SCRIPTS_FILE',
|
||||
READ_SCRIPTS_FILE: 'READ_SCRIPTS_FILE',
|
||||
LIST_SCRIPTS_FILES: 'LIST_SCRIPTS_FILES',
|
||||
WRITE_SCRIPT_FILE: 'WRITE_SCRIPT_FILE',
|
||||
GET_FILES_STAT: 'GET_FILES_STAT',
|
||||
DELETE_SCRIPTS_FILE: 'DELETE_SCRIPTS_FILE',
|
||||
RUN_SCRIPT_FILE: 'RUN_SCRIPT_FILE',
|
||||
|
||||
// shortkey setting rpc
|
||||
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
|
||||
|
||||
123
src/main/utils/runScript.ts
Normal file
123
src/main/utils/runScript.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import crypto from 'node:crypto'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import vm from 'node:vm'
|
||||
|
||||
import { scriptsDir } from '@core/datastore/dirs'
|
||||
import picgo from '@core/picgo'
|
||||
import logger from '@core/picgo/logger'
|
||||
import axios from 'axios'
|
||||
import fs from 'fs-extra'
|
||||
import { IPicGo } from 'piclist'
|
||||
|
||||
export const scriptLifecycleStages = [
|
||||
'onSoftwareOpen',
|
||||
'onSoftwareClose',
|
||||
'preProcess',
|
||||
'beforeTransform',
|
||||
'transform',
|
||||
'beforeUpload',
|
||||
'upload',
|
||||
'afterUpload',
|
||||
'onUploadSuccess',
|
||||
'onGalleryRemove',
|
||||
'manualTrigger',
|
||||
'uploader.advancedplist',
|
||||
] as const
|
||||
|
||||
function format(data: unknown): string {
|
||||
if (data instanceof Error) {
|
||||
return `${data.name}: ${data.message}\n${data.stack}`
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(data)
|
||||
} catch {
|
||||
return String(data)
|
||||
}
|
||||
}
|
||||
|
||||
export async function runScript(ctx: IPicGo, script: string, extra: Record<string, any>): Promise<any> {
|
||||
try {
|
||||
const base64Decode = (str: string): string => Buffer.from(str, 'base64').toString('utf-8')
|
||||
const base64Encode = (data: Buffer | string): string =>
|
||||
(Buffer.isBuffer(data) ? data : Buffer.from(String(data))).toString('base64')
|
||||
const exposedAPI = {
|
||||
ctx,
|
||||
extra,
|
||||
console: Object.freeze({
|
||||
log: (...args: unknown[]) => ctx.log.info(args.map(format).join(' ')),
|
||||
info: (...args: unknown[]) => ctx.log.info(args.map(format).join(' ')),
|
||||
error: (...args: unknown[]) => ctx.log.error(args.map(format).join(' ')),
|
||||
debug: (...args: unknown[]) => ctx.log.debug(args.map(format).join(' ')),
|
||||
}),
|
||||
axios,
|
||||
crypto,
|
||||
setTimeout,
|
||||
setInterval,
|
||||
clearTimeout,
|
||||
clearInterval,
|
||||
fs,
|
||||
path,
|
||||
base64Decode,
|
||||
base64Encode,
|
||||
os,
|
||||
Buffer,
|
||||
}
|
||||
vm.createContext(exposedAPI)
|
||||
ctx.log.info('start to run script')
|
||||
vm.runInContext(script, exposedAPI)
|
||||
const promise = vm.runInContext(
|
||||
`(async () => {
|
||||
const result = main(ctx, extra)
|
||||
if (result instanceof Promise) return await result
|
||||
return result
|
||||
})()`,
|
||||
exposedAPI,
|
||||
)
|
||||
const result = await promise
|
||||
ctx.log.info('script executed successfully')
|
||||
return result
|
||||
} catch (e) {
|
||||
ctx.log.error(`script execution failed: ${e}`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function runScriptInStage(stage: string, ctx: IPicGo, extra: Record<string, any>): Promise<void> {
|
||||
const baseDir = scriptsDir()
|
||||
const enabledPaths: string[] = []
|
||||
let scriptDir: string
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
const disabledList: string[] = allConfig.scripts?.disabledList || []
|
||||
if (stage === 'uploader.advancedplist') {
|
||||
scriptDir = path.join(baseDir, 'uploader', 'advancedplist')
|
||||
stage = 'uploader/advancedplist'
|
||||
} else {
|
||||
scriptDir = path.join(baseDir, stage)
|
||||
}
|
||||
|
||||
const files = await fs.readdir(scriptDir).catch(() => [])
|
||||
if (files.length === 0) {
|
||||
return
|
||||
}
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.js')) {
|
||||
if (!disabledList.includes(`${stage}/${file}`)) {
|
||||
enabledPaths.push(path.join(scriptDir, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (enabledPaths.length === 0) {
|
||||
logger.info(`no enabled scripts found in stage ${stage}`)
|
||||
return
|
||||
}
|
||||
for (const scriptPath of enabledPaths) {
|
||||
const scriptContent = fs.readFileSync(scriptPath, 'utf-8')
|
||||
try {
|
||||
await runScript(ctx, scriptContent, extra)
|
||||
logger.info(`script ${scriptPath} in stage ${stage} executed successfully`)
|
||||
} catch (e) {
|
||||
logger.error(`script ${scriptPath} in stage ${stage} execution failed: ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,8 +238,8 @@ class UploadTaskQueueManager {
|
||||
const rawInput = cloneDeep(input)
|
||||
|
||||
const res = await uploader.setWebContents(webContents).uploadReturnCtx(input)
|
||||
const imgs = res[0] ? res[0] : false
|
||||
const backupImgs = res[1] ? res[1] : false
|
||||
const imgs = res.ctx?.output ? res.ctx.output : false
|
||||
const backupImgs = res.backupCtx?.output ? res.backupCtx.output : false
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
|
||||
if (imgs !== false && imgs.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user