mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-27 19:20:24 +08:00
🎨 Style(custom): lint code
This commit is contained in:
@@ -1,75 +1,60 @@
|
||||
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 '{}'
|
||||
}
|
||||
}
|
||||
}
|
||||
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 '{}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const configPath = dbPathChecker()
|
||||
const CONFIG_DIR = path.dirname(configPath)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function beforeOpen () {
|
||||
function beforeOpen() {
|
||||
if (process.platform === 'darwin') {
|
||||
resolveMacWorkFlow()
|
||||
}
|
||||
@@ -21,7 +21,7 @@ function beforeOpen () {
|
||||
resolveOtherI18nFiles()
|
||||
}
|
||||
|
||||
function copyFileOutsideOfElectronAsar (sourceInAsarArchive: string, destOutsideAsarArchive: string) {
|
||||
function copyFileOutsideOfElectronAsar(sourceInAsarArchive: string, destOutsideAsarArchive: string) {
|
||||
if (fs.existsSync(sourceInAsarArchive)) {
|
||||
// file will be copied
|
||||
if (fs.statSync(sourceInAsarArchive).isFile()) {
|
||||
@@ -45,16 +45,21 @@ function copyFileOutsideOfElectronAsar (sourceInAsarArchive: string, destOutside
|
||||
/**
|
||||
* macOS 右键菜单
|
||||
*/
|
||||
function resolveMacWorkFlow () {
|
||||
function resolveMacWorkFlow() {
|
||||
const dest = `${os.homedir()}/Library/Services/Upload pictures with PicList.workflow`
|
||||
try {
|
||||
copyFileOutsideOfElectronAsar(path.join(__dirname, '../../resources', 'Upload pictures with PicList.workflow').replace('app.asar', 'app.asar.unpacked'), dest)
|
||||
copyFileOutsideOfElectronAsar(
|
||||
path
|
||||
.join(__dirname, '../../resources', 'Upload pictures with PicList.workflow')
|
||||
.replace('app.asar', 'app.asar.unpacked'),
|
||||
dest
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
function diffFilesAndUpdate (filePath1: string, filePath2: string) {
|
||||
function diffFilesAndUpdate(filePath1: string, filePath2: string) {
|
||||
try {
|
||||
const file1 = fs.existsSync(filePath1) && fs.readFileSync(filePath1)
|
||||
const file2 = fs.existsSync(filePath1) && fs.readFileSync(filePath2)
|
||||
@@ -71,7 +76,7 @@ function diffFilesAndUpdate (filePath1: string, filePath2: string) {
|
||||
/**
|
||||
* 初始化剪贴板生成图片的脚本
|
||||
*/
|
||||
function resolveClipboardImageGenerator () {
|
||||
function resolveClipboardImageGenerator() {
|
||||
const clipboardFiles = getClipboardFiles()
|
||||
if (!fs.pathExistsSync(path.join(CONFIG_DIR, 'windows10.ps1'))) {
|
||||
clipboardFiles.forEach(item => {
|
||||
@@ -84,7 +89,7 @@ function resolveClipboardImageGenerator () {
|
||||
})
|
||||
}
|
||||
|
||||
function getClipboardFiles () {
|
||||
function getClipboardFiles() {
|
||||
const files = ['linux.sh', 'mac.applescript', 'windows.ps1', 'windows10.ps1', 'wsl.sh']
|
||||
|
||||
return files.map(item => {
|
||||
@@ -99,7 +104,7 @@ function resolveClipboardImageGenerator () {
|
||||
/**
|
||||
* 初始化其他语言文件
|
||||
*/
|
||||
function resolveOtherI18nFiles () {
|
||||
function resolveOtherI18nFiles() {
|
||||
const i18nFolder = path.join(CONFIG_DIR, 'i18n')
|
||||
if (!fs.pathExistsSync(i18nFolder)) {
|
||||
fs.mkdirSync(i18nFolder)
|
||||
|
||||
@@ -9,13 +9,13 @@ class ClipboardWatcher extends EventEmitter {
|
||||
timer: NodeJS.Timeout | null
|
||||
lastImageHash: string | null
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
super()
|
||||
this.lastImageHash = null
|
||||
this.timer = null
|
||||
}
|
||||
|
||||
startListening (watchDelay = 1000) {
|
||||
startListening(watchDelay = 1000) {
|
||||
this.stopListening(false)
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
@@ -34,7 +34,7 @@ class ClipboardWatcher extends EventEmitter {
|
||||
logger.info('Start to watch clipboard')
|
||||
}
|
||||
|
||||
stopListening (isLog = true) {
|
||||
stopListening(isLog = true) {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
@@ -43,7 +43,7 @@ class ClipboardWatcher extends EventEmitter {
|
||||
isLog && logger.info('Stop to watch clipboard')
|
||||
}
|
||||
|
||||
getImageHash (image: NativeImage): string {
|
||||
getImageHash(image: NativeImage): string {
|
||||
const buffer = image.toBitmap()
|
||||
return crypto.createHash('md5').update(buffer).digest('hex')
|
||||
}
|
||||
|
||||
@@ -1,323 +1,323 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import db from '@core/datastore'
|
||||
import logger from '@core/picgo/logger'
|
||||
import axios from 'axios'
|
||||
import { clipboard, Notification, Tray } from 'electron'
|
||||
import FormData from 'form-data'
|
||||
import fs from 'fs-extra'
|
||||
import { isReactive, isRef, toRaw, unref } from 'vue'
|
||||
|
||||
import type { IHTTPProxy, IPrivateShowNotificationOption, IStringKeyMap } from '#/types/types'
|
||||
import { configPaths } from '~/utils/configPaths'
|
||||
import { IShortUrlServer } from '~/utils/enum'
|
||||
|
||||
/**
|
||||
* get raw data from reactive or ref
|
||||
*/
|
||||
export const getRawData = (args: any): any => {
|
||||
if (isRef(args)) return unref(args)
|
||||
if (isReactive(args)) return toRaw(args)
|
||||
if (Array.isArray(args)) return args.map(getRawData)
|
||||
if (typeof args === 'object' && args !== null) {
|
||||
const data = {} as Record<string, any>
|
||||
for (const key in args) {
|
||||
data[key] = getRawData(args[key])
|
||||
}
|
||||
return data
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
const getExtension = (fileName: string) => path.extname(fileName).slice(1)
|
||||
|
||||
export const isImage = (fileName: string) =>
|
||||
['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif'].includes(getExtension(fileName))
|
||||
|
||||
export let tray: Tray
|
||||
|
||||
export const setTray = (t: Tray) => {
|
||||
tray = t
|
||||
}
|
||||
|
||||
export const getTray = () => tray
|
||||
|
||||
export function setTrayToolTip (title: string): void {
|
||||
if (tray) {
|
||||
tray.setToolTip(title)
|
||||
}
|
||||
}
|
||||
|
||||
export const handleCopyUrl = (str: string): void => {
|
||||
if (db.get(configPaths.settings.autoCopy) !== false) {
|
||||
clipboard.writeText(str)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* show notification
|
||||
* @param options
|
||||
*/
|
||||
export const showNotification = (
|
||||
options: IPrivateShowNotificationOption = {
|
||||
title: '',
|
||||
body: '',
|
||||
clickToCopy: false,
|
||||
copyContent: '',
|
||||
clickFn: () => {}
|
||||
}
|
||||
) => {
|
||||
const notification = new Notification({
|
||||
title: options.title,
|
||||
body: options.body
|
||||
})
|
||||
const handleClick = () => {
|
||||
if (options.clickToCopy) {
|
||||
clipboard.writeText(options.copyContent || options.body)
|
||||
}
|
||||
if (options.clickFn) {
|
||||
options.clickFn()
|
||||
}
|
||||
}
|
||||
notification.once('click', handleClick)
|
||||
notification.once('close', () => {
|
||||
notification.removeListener('click', handleClick)
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS public.file-url will get encoded file path,
|
||||
* so we need to decode it
|
||||
*/
|
||||
export const ensureFilePath = (filePath: string, prefix = 'file://'): string => {
|
||||
filePath = filePath.replace(prefix, '')
|
||||
if (fs.existsSync(filePath)) {
|
||||
return `${prefix}${filePath}`
|
||||
}
|
||||
filePath = decodeURIComponent(filePath)
|
||||
if (fs.existsSync(filePath)) {
|
||||
return `${prefix}${filePath}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* for builtin clipboard to get image path from clipboard
|
||||
* @returns
|
||||
*/
|
||||
export const getClipboardFilePath = (): string => {
|
||||
// TODO: linux support
|
||||
const img = clipboard.readImage()
|
||||
const platform = process.platform
|
||||
|
||||
if (!img.isEmpty() && platform === 'darwin') {
|
||||
let imgPath = clipboard.read('public.file-url') // will get file://xxx/xxx
|
||||
imgPath = ensureFilePath(imgPath)
|
||||
return imgPath ? imgPath.replace('file://', '') : ''
|
||||
}
|
||||
|
||||
if (img.isEmpty() && platform === 'win32') {
|
||||
const imgPath = clipboard
|
||||
.readBuffer('FileNameW')
|
||||
?.toString('ucs2')
|
||||
?.replace(RegExp(String.fromCharCode(0), 'g'), '')
|
||||
return imgPath || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const c1nApi = 'https://c1n.cn/link/short'
|
||||
|
||||
const createC1NShortUrl = async (url: string) => {
|
||||
const c1nToken = db.get(configPaths.settings.c1nToken) || ''
|
||||
if (!c1nToken) {
|
||||
logger.warn('c1n token is not set')
|
||||
return url
|
||||
}
|
||||
try {
|
||||
const form = new FormData()
|
||||
form.append('url', url)
|
||||
const res = await axios.post(c1nApi, form, {
|
||||
headers: {
|
||||
token: c1nToken
|
||||
}
|
||||
})
|
||||
if (res.status >= 200 && res.status < 300 && res.data?.code === 0) {
|
||||
return res.data.data
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
const createYOURLSShortLink = async (url: string) => {
|
||||
let domain = db.get(configPaths.settings.yourlsDomain) || ''
|
||||
const signature = db.get(configPaths.settings.yourlsSignature) || ''
|
||||
|
||||
if (!domain || !signature) {
|
||||
logger.warn('Yourls server or signature is not set')
|
||||
return url
|
||||
}
|
||||
if (!/^https?:\/\//.test(domain)) {
|
||||
domain = `http://${domain}`
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
signature,
|
||||
action: 'shorturl',
|
||||
format: 'json',
|
||||
url
|
||||
})
|
||||
try {
|
||||
const res = await axios.get(`${domain}/yourls-api.php?${params.toString()}`)
|
||||
if (res.data?.shorturl) {
|
||||
return res.data.shorturl
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.response?.data?.message?.includes('already exists in database')) {
|
||||
return e.response.data.shorturl
|
||||
}
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
const createShortUrlForCFWorker = async (url: string) => {
|
||||
let cfWorkerHost = db.get(configPaths.settings.cfWorkerHost) || ''
|
||||
cfWorkerHost = cfWorkerHost.replace(/\/$/, '')
|
||||
if (!cfWorkerHost) {
|
||||
logger.warn('CF Worker host is not set')
|
||||
return url
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post(cfWorkerHost, { url })
|
||||
if (res.data?.status === 200 && res.data?.key?.startsWith('/')) {
|
||||
return `${cfWorkerHost}${res.data.key}`
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
const createShortUrlFromSink = async (url: string) => {
|
||||
let sinkDomain = db.get(configPaths.settings.sinkDomain) || ''
|
||||
const sinkToken = db.get(configPaths.settings.sinkToken) || ''
|
||||
if (!sinkDomain || !sinkToken) {
|
||||
logger.warn('Sink domain or token is not set')
|
||||
return url
|
||||
}
|
||||
if (!/^https?:\/\//.test(sinkDomain)) {
|
||||
sinkDomain = `http://${sinkDomain}`
|
||||
}
|
||||
if (sinkDomain.endsWith('/')) {
|
||||
sinkDomain = sinkDomain.slice(0, -1)
|
||||
}
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`${sinkDomain}/api/link/create`,
|
||||
{ url },
|
||||
{ headers: { Authorization: `Bearer ${sinkToken}` } }
|
||||
)
|
||||
if (res.data?.link?.slug) {
|
||||
return `${sinkDomain}/${res.data.link.slug}`
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export const generateShortUrl = async (url: string) => {
|
||||
const server = db.get(configPaths.settings.shortUrlServer) || IShortUrlServer.C1N
|
||||
switch (server) {
|
||||
case IShortUrlServer.C1N:
|
||||
return createC1NShortUrl(url)
|
||||
case IShortUrlServer.YOURLS:
|
||||
return createYOURLSShortLink(url)
|
||||
case IShortUrlServer.CFWORKER:
|
||||
return createShortUrlForCFWorker(url)
|
||||
case IShortUrlServer.SINK:
|
||||
return createShortUrlFromSink(url)
|
||||
default:
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export const isUrl = (url: string): boolean => {
|
||||
try {
|
||||
return Boolean(new URL(url))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isUrlEncode = (url: string): boolean => {
|
||||
url = url || ''
|
||||
try {
|
||||
return url !== decodeURI(url)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const handleUrlEncode = (url: string): string => (isUrlEncode(url) ? url : encodeURI(url))
|
||||
|
||||
export const handleUrlEncodeWithSetting = (url: string) =>
|
||||
db.get(configPaths.settings.encodeOutputURL) ? handleUrlEncode(url) : url
|
||||
|
||||
export const handleStreamlinePluginName = (name: string) => name.replace(/(@[^/]+\/)?picgo-plugin-/, '')
|
||||
export const simpleClone = (obj: any) => JSON.parse(JSON.stringify(obj))
|
||||
export const enforceNumber = (num: number | string) => (isNaN(+num) ? 0 : +num)
|
||||
|
||||
export const trimValues = <T extends IStringKeyMap>(
|
||||
obj: T
|
||||
): { [K in keyof T]: T[K] extends string ? string : T[K] } => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
|
||||
) as { [K in keyof T]: T[K] extends string ? string : T[K] }
|
||||
}
|
||||
|
||||
export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string => {
|
||||
const hasProtocol = /^https?:\/\//.test(endpoint)
|
||||
if (!hasProtocol) {
|
||||
return `${sslEnabled ? 'https' : 'http'}://${endpoint}`
|
||||
}
|
||||
return sslEnabled ? endpoint.replace(/^http:\/\//, 'https://') : endpoint.replace(/^https:\/\//, 'http://')
|
||||
}
|
||||
|
||||
export const formatHttpProxy = (
|
||||
proxy: string | undefined,
|
||||
type: 'object' | 'string'
|
||||
): IHTTPProxy | undefined | string => {
|
||||
if (!proxy) return undefined
|
||||
if (/^https?:\/\//.test(proxy)) {
|
||||
const { protocol, hostname, port } = new URL(proxy)
|
||||
return type === 'string'
|
||||
? `${protocol}//${hostname}:${port}`
|
||||
: {
|
||||
host: hostname,
|
||||
port: Number(port),
|
||||
protocol: protocol.slice(0, -1)
|
||||
}
|
||||
}
|
||||
const [host, port] = proxy.split(':')
|
||||
return type === 'string'
|
||||
? `http://${host}:${port}`
|
||||
: {
|
||||
host,
|
||||
port: port ? Number(port) : 80,
|
||||
protocol: 'http'
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeFilePath (filePath: string) {
|
||||
return filePath.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/')
|
||||
}
|
||||
|
||||
export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
|
||||
import path from 'node:path'
|
||||
|
||||
import db from '@core/datastore'
|
||||
import logger from '@core/picgo/logger'
|
||||
import axios from 'axios'
|
||||
import { clipboard, Notification, Tray } from 'electron'
|
||||
import FormData from 'form-data'
|
||||
import fs from 'fs-extra'
|
||||
import { isReactive, isRef, toRaw, unref } from 'vue'
|
||||
|
||||
import type { IHTTPProxy, IPrivateShowNotificationOption, IStringKeyMap } from '#/types/types'
|
||||
import { configPaths } from '~/utils/configPaths'
|
||||
import { IShortUrlServer } from '~/utils/enum'
|
||||
|
||||
/**
|
||||
* get raw data from reactive or ref
|
||||
*/
|
||||
export const getRawData = (args: any): any => {
|
||||
if (isRef(args)) return unref(args)
|
||||
if (isReactive(args)) return toRaw(args)
|
||||
if (Array.isArray(args)) return args.map(getRawData)
|
||||
if (typeof args === 'object' && args !== null) {
|
||||
const data = {} as Record<string, any>
|
||||
for (const key in args) {
|
||||
data[key] = getRawData(args[key])
|
||||
}
|
||||
return data
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
const getExtension = (fileName: string) => path.extname(fileName).slice(1)
|
||||
|
||||
export const isImage = (fileName: string) =>
|
||||
['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif'].includes(getExtension(fileName))
|
||||
|
||||
export let tray: Tray
|
||||
|
||||
export const setTray = (t: Tray) => {
|
||||
tray = t
|
||||
}
|
||||
|
||||
export const getTray = () => tray
|
||||
|
||||
export function setTrayToolTip(title: string): void {
|
||||
if (tray) {
|
||||
tray.setToolTip(title)
|
||||
}
|
||||
}
|
||||
|
||||
export const handleCopyUrl = (str: string): void => {
|
||||
if (db.get(configPaths.settings.autoCopy) !== false) {
|
||||
clipboard.writeText(str)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* show notification
|
||||
* @param options
|
||||
*/
|
||||
export const showNotification = (
|
||||
options: IPrivateShowNotificationOption = {
|
||||
title: '',
|
||||
body: '',
|
||||
clickToCopy: false,
|
||||
copyContent: '',
|
||||
clickFn: () => {}
|
||||
}
|
||||
) => {
|
||||
const notification = new Notification({
|
||||
title: options.title,
|
||||
body: options.body
|
||||
})
|
||||
const handleClick = () => {
|
||||
if (options.clickToCopy) {
|
||||
clipboard.writeText(options.copyContent || options.body)
|
||||
}
|
||||
if (options.clickFn) {
|
||||
options.clickFn()
|
||||
}
|
||||
}
|
||||
notification.once('click', handleClick)
|
||||
notification.once('close', () => {
|
||||
notification.removeListener('click', handleClick)
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS public.file-url will get encoded file path,
|
||||
* so we need to decode it
|
||||
*/
|
||||
export const ensureFilePath = (filePath: string, prefix = 'file://'): string => {
|
||||
filePath = filePath.replace(prefix, '')
|
||||
if (fs.existsSync(filePath)) {
|
||||
return `${prefix}${filePath}`
|
||||
}
|
||||
filePath = decodeURIComponent(filePath)
|
||||
if (fs.existsSync(filePath)) {
|
||||
return `${prefix}${filePath}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* for builtin clipboard to get image path from clipboard
|
||||
* @returns
|
||||
*/
|
||||
export const getClipboardFilePath = (): string => {
|
||||
// TODO: linux support
|
||||
const img = clipboard.readImage()
|
||||
const platform = process.platform
|
||||
|
||||
if (!img.isEmpty() && platform === 'darwin') {
|
||||
let imgPath = clipboard.read('public.file-url') // will get file://xxx/xxx
|
||||
imgPath = ensureFilePath(imgPath)
|
||||
return imgPath ? imgPath.replace('file://', '') : ''
|
||||
}
|
||||
|
||||
if (img.isEmpty() && platform === 'win32') {
|
||||
const imgPath = clipboard
|
||||
.readBuffer('FileNameW')
|
||||
?.toString('ucs2')
|
||||
?.replace(RegExp(String.fromCharCode(0), 'g'), '')
|
||||
return imgPath || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const c1nApi = 'https://c1n.cn/link/short'
|
||||
|
||||
const createC1NShortUrl = async (url: string) => {
|
||||
const c1nToken = db.get(configPaths.settings.c1nToken) || ''
|
||||
if (!c1nToken) {
|
||||
logger.warn('c1n token is not set')
|
||||
return url
|
||||
}
|
||||
try {
|
||||
const form = new FormData()
|
||||
form.append('url', url)
|
||||
const res = await axios.post(c1nApi, form, {
|
||||
headers: {
|
||||
token: c1nToken
|
||||
}
|
||||
})
|
||||
if (res.status >= 200 && res.status < 300 && res.data?.code === 0) {
|
||||
return res.data.data
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
const createYOURLSShortLink = async (url: string) => {
|
||||
let domain = db.get(configPaths.settings.yourlsDomain) || ''
|
||||
const signature = db.get(configPaths.settings.yourlsSignature) || ''
|
||||
|
||||
if (!domain || !signature) {
|
||||
logger.warn('Yourls server or signature is not set')
|
||||
return url
|
||||
}
|
||||
if (!/^https?:\/\//.test(domain)) {
|
||||
domain = `http://${domain}`
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
signature,
|
||||
action: 'shorturl',
|
||||
format: 'json',
|
||||
url
|
||||
})
|
||||
try {
|
||||
const res = await axios.get(`${domain}/yourls-api.php?${params.toString()}`)
|
||||
if (res.data?.shorturl) {
|
||||
return res.data.shorturl
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.response?.data?.message?.includes('already exists in database')) {
|
||||
return e.response.data.shorturl
|
||||
}
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
const createShortUrlForCFWorker = async (url: string) => {
|
||||
let cfWorkerHost = db.get(configPaths.settings.cfWorkerHost) || ''
|
||||
cfWorkerHost = cfWorkerHost.replace(/\/$/, '')
|
||||
if (!cfWorkerHost) {
|
||||
logger.warn('CF Worker host is not set')
|
||||
return url
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post(cfWorkerHost, { url })
|
||||
if (res.data?.status === 200 && res.data?.key?.startsWith('/')) {
|
||||
return `${cfWorkerHost}${res.data.key}`
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
const createShortUrlFromSink = async (url: string) => {
|
||||
let sinkDomain = db.get(configPaths.settings.sinkDomain) || ''
|
||||
const sinkToken = db.get(configPaths.settings.sinkToken) || ''
|
||||
if (!sinkDomain || !sinkToken) {
|
||||
logger.warn('Sink domain or token is not set')
|
||||
return url
|
||||
}
|
||||
if (!/^https?:\/\//.test(sinkDomain)) {
|
||||
sinkDomain = `http://${sinkDomain}`
|
||||
}
|
||||
if (sinkDomain.endsWith('/')) {
|
||||
sinkDomain = sinkDomain.slice(0, -1)
|
||||
}
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`${sinkDomain}/api/link/create`,
|
||||
{ url },
|
||||
{ headers: { Authorization: `Bearer ${sinkToken}` } }
|
||||
)
|
||||
if (res.data?.link?.slug) {
|
||||
return `${sinkDomain}/${res.data.link.slug}`
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export const generateShortUrl = async (url: string) => {
|
||||
const server = db.get(configPaths.settings.shortUrlServer) || IShortUrlServer.C1N
|
||||
switch (server) {
|
||||
case IShortUrlServer.C1N:
|
||||
return createC1NShortUrl(url)
|
||||
case IShortUrlServer.YOURLS:
|
||||
return createYOURLSShortLink(url)
|
||||
case IShortUrlServer.CFWORKER:
|
||||
return createShortUrlForCFWorker(url)
|
||||
case IShortUrlServer.SINK:
|
||||
return createShortUrlFromSink(url)
|
||||
default:
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export const isUrl = (url: string): boolean => {
|
||||
try {
|
||||
return Boolean(new URL(url))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isUrlEncode = (url: string): boolean => {
|
||||
url = url || ''
|
||||
try {
|
||||
return url !== decodeURI(url)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const handleUrlEncode = (url: string): string => (isUrlEncode(url) ? url : encodeURI(url))
|
||||
|
||||
export const handleUrlEncodeWithSetting = (url: string) =>
|
||||
db.get(configPaths.settings.encodeOutputURL) ? handleUrlEncode(url) : url
|
||||
|
||||
export const handleStreamlinePluginName = (name: string) => name.replace(/(@[^/]+\/)?picgo-plugin-/, '')
|
||||
export const simpleClone = (obj: any) => JSON.parse(JSON.stringify(obj))
|
||||
export const enforceNumber = (num: number | string) => (isNaN(+num) ? 0 : +num)
|
||||
|
||||
export const trimValues = <T extends IStringKeyMap>(
|
||||
obj: T
|
||||
): { [K in keyof T]: T[K] extends string ? string : T[K] } => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
|
||||
) as { [K in keyof T]: T[K] extends string ? string : T[K] }
|
||||
}
|
||||
|
||||
export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string => {
|
||||
const hasProtocol = /^https?:\/\//.test(endpoint)
|
||||
if (!hasProtocol) {
|
||||
return `${sslEnabled ? 'https' : 'http'}://${endpoint}`
|
||||
}
|
||||
return sslEnabled ? endpoint.replace(/^http:\/\//, 'https://') : endpoint.replace(/^https:\/\//, 'http://')
|
||||
}
|
||||
|
||||
export const formatHttpProxy = (
|
||||
proxy: string | undefined,
|
||||
type: 'object' | 'string'
|
||||
): IHTTPProxy | undefined | string => {
|
||||
if (!proxy) return undefined
|
||||
if (/^https?:\/\//.test(proxy)) {
|
||||
const { protocol, hostname, port } = new URL(proxy)
|
||||
return type === 'string'
|
||||
? `${protocol}//${hostname}:${port}`
|
||||
: {
|
||||
host: hostname,
|
||||
port: Number(port),
|
||||
protocol: protocol.slice(0, -1)
|
||||
}
|
||||
}
|
||||
const [host, port] = proxy.split(':')
|
||||
return type === 'string'
|
||||
? `http://${host}:${port}`
|
||||
: {
|
||||
host,
|
||||
port: port ? Number(port) : 80,
|
||||
protocol: 'http'
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeFilePath(filePath: string) {
|
||||
return filePath.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/')
|
||||
}
|
||||
|
||||
export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
|
||||
|
||||
@@ -1,188 +1,206 @@
|
||||
import type { IBuildInCompressOptions, IBuildInWaterMarkOptions } from 'piclist'
|
||||
|
||||
import type { IAliYunConfig, IAwsS3PListUserConfig, IGitHubConfig, IImgurConfig, ILocalConfig, ILskyConfig, IPicBedType, IQiniuConfig, IServerConfig, ISftpPlistConfig, IShortKeyConfig, ISMMSConfig, ISyncConfig, ITcYunConfig, IUploaderConfig, IUpYunConfig, IWebdavPlistConfig } from '#/types/types'
|
||||
|
||||
export type manualPageOpenType = 'window' | 'browser'
|
||||
|
||||
interface IPicGoPlugins {
|
||||
[key: `picgo-plugin-${string}`]: boolean
|
||||
}
|
||||
|
||||
export interface IConfigStruct {
|
||||
picBed: {
|
||||
uploader: string
|
||||
current?: string
|
||||
smms?: ISMMSConfig
|
||||
qiniu?: IQiniuConfig
|
||||
upyun?: IUpYunConfig
|
||||
tcyun?: ITcYunConfig
|
||||
github?: IGitHubConfig
|
||||
aliyun?: IAliYunConfig
|
||||
imgur?: IImgurConfig
|
||||
webdavplist?: IWebdavPlistConfig
|
||||
local?: ILocalConfig
|
||||
sftpplist?: ISftpPlistConfig
|
||||
lskyplist?: ILskyConfig
|
||||
'aws-s3-plist': IAwsS3PListUserConfig
|
||||
proxy?: string
|
||||
transformer?: string
|
||||
list: IPicBedType[]
|
||||
[others: string]: any
|
||||
}
|
||||
settings: {
|
||||
shortKey: {
|
||||
[key: string]: IShortKeyConfig
|
||||
}
|
||||
logLevel: string[]
|
||||
logPath: string
|
||||
logFileSizeLimit: number
|
||||
isAutoListenClipboard: boolean
|
||||
isListeningClipboard: boolean
|
||||
showUpdateTip: boolean
|
||||
miniWindowPosition: [number, number]
|
||||
miniWindowOntop: boolean
|
||||
mainWindowWidth: number
|
||||
mainWindowHeight: number
|
||||
isHideDock: boolean
|
||||
autoCloseMiniWindow: boolean
|
||||
autoCloseMainWindow: boolean
|
||||
isCustomMiniIcon: boolean
|
||||
customMiniIcon: string
|
||||
startMode: string
|
||||
autoRename: boolean
|
||||
deleteCloudFile: boolean
|
||||
server: IServerConfig
|
||||
serverKey: string
|
||||
pasteStyle: string
|
||||
aesPassword: string
|
||||
rename: boolean
|
||||
sync: ISyncConfig
|
||||
tempDirPath: string
|
||||
language: string
|
||||
customLink: string
|
||||
manualPageOpen: manualPageOpenType
|
||||
encodeOutputURL: boolean
|
||||
useShortUrl: boolean
|
||||
shortUrlServer: string
|
||||
c1nToken: string
|
||||
cfWorkerHost: string
|
||||
yourlsDomain: string
|
||||
yourlsSignature: string
|
||||
sinkDomain: string
|
||||
sinkToken: string
|
||||
isSilentNotice: boolean
|
||||
proxy: string
|
||||
registry: string
|
||||
autoCopy: boolean
|
||||
enableWebServer: boolean
|
||||
webServerHost: string
|
||||
webServerPort: number
|
||||
webServerPath: string
|
||||
deleteLocalFile: boolean
|
||||
uploadResultNotification: boolean
|
||||
uploadNotification: boolean
|
||||
useBuiltinClipboard: boolean
|
||||
autoStart: boolean
|
||||
autoImport: boolean
|
||||
autoImportPicBed: string[]
|
||||
}
|
||||
needReload: boolean
|
||||
picgoPlugins: IPicGoPlugins
|
||||
uploader: IUploaderConfig
|
||||
buildIn: {
|
||||
compress: IBuildInCompressOptions
|
||||
watermark: IBuildInWaterMarkOptions
|
||||
rename: {
|
||||
enable: boolean
|
||||
format: string
|
||||
}
|
||||
skipProcess: {
|
||||
skipProcessExtList: string
|
||||
}
|
||||
}
|
||||
debug: boolean
|
||||
PICGO_ENV: string
|
||||
}
|
||||
|
||||
export const configPaths = {
|
||||
picBed: {
|
||||
current: 'picBed.current',
|
||||
uploader: 'picBed.uploader',
|
||||
secondUploader: 'picBed.secondUploader',
|
||||
secondUploaderId: 'picBed.secondUploaderId',
|
||||
secondUploaderConfig: 'picBed.secondUploaderConfig',
|
||||
proxy: 'picBed.proxy',
|
||||
transformer: 'picBed.transformer',
|
||||
list: 'picBed.list'
|
||||
},
|
||||
settings: {
|
||||
shortKey: {
|
||||
_path: 'settings.shortKey',
|
||||
'picgo:upload': 'settings.shortKey[picgo:upload]'
|
||||
},
|
||||
logLevel: 'settings.logLevel',
|
||||
logPath: 'settings.logPath',
|
||||
logFileSizeLimit: 'settings.logFileSizeLimit',
|
||||
isAutoListenClipboard: 'settings.isAutoListenClipboard',
|
||||
isListeningClipboard: 'settings.isListeningClipboard',
|
||||
showUpdateTip: 'settings.showUpdateTip',
|
||||
miniWindowPosition: 'settings.miniWindowPosition',
|
||||
miniWindowOntop: 'settings.miniWindowOntop',
|
||||
isHideDock: 'settings.isHideDock',
|
||||
mainWindowWidth: 'settings.mainWindowWidth',
|
||||
mainWindowHeight: 'settings.mainWindowHeight',
|
||||
autoCloseMiniWindow: 'settings.autoCloseMiniWindow',
|
||||
autoCloseMainWindow: 'settings.autoCloseMainWindow',
|
||||
isCustomMiniIcon: 'settings.isCustomMiniIcon',
|
||||
customMiniIcon: 'settings.customMiniIcon',
|
||||
startMode: 'settings.startMode',
|
||||
autoRename: 'settings.autoRename',
|
||||
deleteCloudFile: 'settings.deleteCloudFile',
|
||||
server: 'settings.server',
|
||||
serverKey: 'settings.serverKey',
|
||||
pasteStyle: 'settings.pasteStyle',
|
||||
aesPassword: 'settings.aesPassword',
|
||||
rename: 'settings.rename',
|
||||
sync: 'settings.sync',
|
||||
tempDirPath: 'settings.tempDirPath',
|
||||
language: 'settings.language',
|
||||
customLink: 'settings.customLink',
|
||||
manualPageOpen: 'settings.manualPageOpen',
|
||||
encodeOutputURL: 'settings.encodeOutputURL',
|
||||
useShortUrl: 'settings.useShortUrl',
|
||||
shortUrlServer: 'settings.shortUrlServer',
|
||||
c1nToken: 'settings.c1nToken',
|
||||
cfWorkerHost: 'settings.cfWorkerHost',
|
||||
yourlsDomain: 'settings.yourlsDomain',
|
||||
yourlsSignature: 'settings.yourlsSignature',
|
||||
sinkDomain: 'settings.sinkDomain',
|
||||
sinkToken: 'settings.sinkToken',
|
||||
isSilentNotice: 'settings.isSilentNotice',
|
||||
proxy: 'settings.proxy',
|
||||
registry: 'settings.registry',
|
||||
autoCopy: 'settings.autoCopy',
|
||||
enableWebServer: 'settings.enableWebServer',
|
||||
webServerHost: 'settings.webServerHost',
|
||||
webServerPort: 'settings.webServerPort',
|
||||
webServerPath: 'settings.webServerPath',
|
||||
deleteLocalFile: 'settings.deleteLocalFile',
|
||||
uploadResultNotification: 'settings.uploadResultNotification',
|
||||
uploadNotification: 'settings.uploadNotification',
|
||||
useBuiltinClipboard: 'settings.useBuiltinClipboard',
|
||||
autoStart: 'settings.autoStart',
|
||||
autoImport: 'settings.autoImport',
|
||||
autoImportPicBed: 'settings.autoImportPicBed',
|
||||
enableSecondUploader: 'settings.enableSecondUploader'
|
||||
},
|
||||
needReload: 'needReload',
|
||||
picgoPlugins: 'picgoPlugins',
|
||||
uploader: 'uploader',
|
||||
buildIn: {
|
||||
compress: 'buildIn.compress',
|
||||
watermark: 'buildIn.watermark',
|
||||
rename: 'buildIn.rename',
|
||||
skipProcess: 'buildIn.skipProcess'
|
||||
},
|
||||
debug: 'debug',
|
||||
PICGO_ENV: 'PICGO_ENV'
|
||||
}
|
||||
import type { IBuildInCompressOptions, IBuildInWaterMarkOptions } from 'piclist'
|
||||
|
||||
import type {
|
||||
IAliYunConfig,
|
||||
IAwsS3PListUserConfig,
|
||||
IGitHubConfig,
|
||||
IImgurConfig,
|
||||
ILocalConfig,
|
||||
ILskyConfig,
|
||||
IPicBedType,
|
||||
IQiniuConfig,
|
||||
IServerConfig,
|
||||
ISftpPlistConfig,
|
||||
IShortKeyConfig,
|
||||
ISMMSConfig,
|
||||
ISyncConfig,
|
||||
ITcYunConfig,
|
||||
IUploaderConfig,
|
||||
IUpYunConfig,
|
||||
IWebdavPlistConfig
|
||||
} from '#/types/types'
|
||||
|
||||
export type manualPageOpenType = 'window' | 'browser'
|
||||
|
||||
interface IPicGoPlugins {
|
||||
[key: `picgo-plugin-${string}`]: boolean
|
||||
}
|
||||
|
||||
export interface IConfigStruct {
|
||||
picBed: {
|
||||
uploader: string
|
||||
current?: string
|
||||
smms?: ISMMSConfig
|
||||
qiniu?: IQiniuConfig
|
||||
upyun?: IUpYunConfig
|
||||
tcyun?: ITcYunConfig
|
||||
github?: IGitHubConfig
|
||||
aliyun?: IAliYunConfig
|
||||
imgur?: IImgurConfig
|
||||
webdavplist?: IWebdavPlistConfig
|
||||
local?: ILocalConfig
|
||||
sftpplist?: ISftpPlistConfig
|
||||
lskyplist?: ILskyConfig
|
||||
'aws-s3-plist': IAwsS3PListUserConfig
|
||||
proxy?: string
|
||||
transformer?: string
|
||||
list: IPicBedType[]
|
||||
[others: string]: any
|
||||
}
|
||||
settings: {
|
||||
shortKey: {
|
||||
[key: string]: IShortKeyConfig
|
||||
}
|
||||
logLevel: string[]
|
||||
logPath: string
|
||||
logFileSizeLimit: number
|
||||
isAutoListenClipboard: boolean
|
||||
isListeningClipboard: boolean
|
||||
showUpdateTip: boolean
|
||||
miniWindowPosition: [number, number]
|
||||
miniWindowOntop: boolean
|
||||
mainWindowWidth: number
|
||||
mainWindowHeight: number
|
||||
isHideDock: boolean
|
||||
autoCloseMiniWindow: boolean
|
||||
autoCloseMainWindow: boolean
|
||||
isCustomMiniIcon: boolean
|
||||
customMiniIcon: string
|
||||
startMode: string
|
||||
autoRename: boolean
|
||||
deleteCloudFile: boolean
|
||||
server: IServerConfig
|
||||
serverKey: string
|
||||
pasteStyle: string
|
||||
aesPassword: string
|
||||
rename: boolean
|
||||
sync: ISyncConfig
|
||||
tempDirPath: string
|
||||
language: string
|
||||
customLink: string
|
||||
manualPageOpen: manualPageOpenType
|
||||
encodeOutputURL: boolean
|
||||
useShortUrl: boolean
|
||||
shortUrlServer: string
|
||||
c1nToken: string
|
||||
cfWorkerHost: string
|
||||
yourlsDomain: string
|
||||
yourlsSignature: string
|
||||
sinkDomain: string
|
||||
sinkToken: string
|
||||
isSilentNotice: boolean
|
||||
proxy: string
|
||||
registry: string
|
||||
autoCopy: boolean
|
||||
enableWebServer: boolean
|
||||
webServerHost: string
|
||||
webServerPort: number
|
||||
webServerPath: string
|
||||
deleteLocalFile: boolean
|
||||
uploadResultNotification: boolean
|
||||
uploadNotification: boolean
|
||||
useBuiltinClipboard: boolean
|
||||
autoStart: boolean
|
||||
autoImport: boolean
|
||||
autoImportPicBed: string[]
|
||||
}
|
||||
needReload: boolean
|
||||
picgoPlugins: IPicGoPlugins
|
||||
uploader: IUploaderConfig
|
||||
buildIn: {
|
||||
compress: IBuildInCompressOptions
|
||||
watermark: IBuildInWaterMarkOptions
|
||||
rename: {
|
||||
enable: boolean
|
||||
format: string
|
||||
}
|
||||
skipProcess: {
|
||||
skipProcessExtList: string
|
||||
}
|
||||
}
|
||||
debug: boolean
|
||||
PICGO_ENV: string
|
||||
}
|
||||
|
||||
export const configPaths = {
|
||||
picBed: {
|
||||
current: 'picBed.current',
|
||||
uploader: 'picBed.uploader',
|
||||
secondUploader: 'picBed.secondUploader',
|
||||
secondUploaderId: 'picBed.secondUploaderId',
|
||||
secondUploaderConfig: 'picBed.secondUploaderConfig',
|
||||
proxy: 'picBed.proxy',
|
||||
transformer: 'picBed.transformer',
|
||||
list: 'picBed.list'
|
||||
},
|
||||
settings: {
|
||||
shortKey: {
|
||||
_path: 'settings.shortKey',
|
||||
'picgo:upload': 'settings.shortKey[picgo:upload]'
|
||||
},
|
||||
logLevel: 'settings.logLevel',
|
||||
logPath: 'settings.logPath',
|
||||
logFileSizeLimit: 'settings.logFileSizeLimit',
|
||||
isAutoListenClipboard: 'settings.isAutoListenClipboard',
|
||||
isListeningClipboard: 'settings.isListeningClipboard',
|
||||
showUpdateTip: 'settings.showUpdateTip',
|
||||
miniWindowPosition: 'settings.miniWindowPosition',
|
||||
miniWindowOntop: 'settings.miniWindowOntop',
|
||||
isHideDock: 'settings.isHideDock',
|
||||
mainWindowWidth: 'settings.mainWindowWidth',
|
||||
mainWindowHeight: 'settings.mainWindowHeight',
|
||||
autoCloseMiniWindow: 'settings.autoCloseMiniWindow',
|
||||
autoCloseMainWindow: 'settings.autoCloseMainWindow',
|
||||
isCustomMiniIcon: 'settings.isCustomMiniIcon',
|
||||
customMiniIcon: 'settings.customMiniIcon',
|
||||
startMode: 'settings.startMode',
|
||||
autoRename: 'settings.autoRename',
|
||||
deleteCloudFile: 'settings.deleteCloudFile',
|
||||
server: 'settings.server',
|
||||
serverKey: 'settings.serverKey',
|
||||
pasteStyle: 'settings.pasteStyle',
|
||||
aesPassword: 'settings.aesPassword',
|
||||
rename: 'settings.rename',
|
||||
sync: 'settings.sync',
|
||||
tempDirPath: 'settings.tempDirPath',
|
||||
language: 'settings.language',
|
||||
customLink: 'settings.customLink',
|
||||
manualPageOpen: 'settings.manualPageOpen',
|
||||
encodeOutputURL: 'settings.encodeOutputURL',
|
||||
useShortUrl: 'settings.useShortUrl',
|
||||
shortUrlServer: 'settings.shortUrlServer',
|
||||
c1nToken: 'settings.c1nToken',
|
||||
cfWorkerHost: 'settings.cfWorkerHost',
|
||||
yourlsDomain: 'settings.yourlsDomain',
|
||||
yourlsSignature: 'settings.yourlsSignature',
|
||||
sinkDomain: 'settings.sinkDomain',
|
||||
sinkToken: 'settings.sinkToken',
|
||||
isSilentNotice: 'settings.isSilentNotice',
|
||||
proxy: 'settings.proxy',
|
||||
registry: 'settings.registry',
|
||||
autoCopy: 'settings.autoCopy',
|
||||
enableWebServer: 'settings.enableWebServer',
|
||||
webServerHost: 'settings.webServerHost',
|
||||
webServerPort: 'settings.webServerPort',
|
||||
webServerPath: 'settings.webServerPath',
|
||||
deleteLocalFile: 'settings.deleteLocalFile',
|
||||
uploadResultNotification: 'settings.uploadResultNotification',
|
||||
uploadNotification: 'settings.uploadNotification',
|
||||
useBuiltinClipboard: 'settings.useBuiltinClipboard',
|
||||
autoStart: 'settings.autoStart',
|
||||
autoImport: 'settings.autoImport',
|
||||
autoImportPicBed: 'settings.autoImportPicBed',
|
||||
enableSecondUploader: 'settings.enableSecondUploader'
|
||||
},
|
||||
needReload: 'needReload',
|
||||
picgoPlugins: 'picgoPlugins',
|
||||
uploader: 'uploader',
|
||||
buildIn: {
|
||||
compress: 'buildIn.compress',
|
||||
watermark: 'buildIn.watermark',
|
||||
rename: 'buildIn.rename',
|
||||
skipProcess: 'buildIn.skipProcess'
|
||||
},
|
||||
debug: 'debug',
|
||||
PICGO_ENV: 'PICGO_ENV'
|
||||
}
|
||||
|
||||
@@ -1,98 +1,98 @@
|
||||
import crypto from 'node:crypto'
|
||||
import http, { AgentOptions } from 'node:http'
|
||||
import https from 'node:https'
|
||||
import path from 'node:path'
|
||||
import querystring from 'node:querystring'
|
||||
|
||||
import type { S3ClientConfig } from '@aws-sdk/client-s3'
|
||||
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
import logger from '@core/picgo/logger'
|
||||
import { NodeHttpHandler } from '@smithy/node-http-handler'
|
||||
import axios from 'axios'
|
||||
import type { ISftpPlistConfig } from 'piclist'
|
||||
|
||||
import type { IObj, IStringKeyMap } from '#/types/types'
|
||||
import { getAgent } from '~/manage/utils/common'
|
||||
import SSHClient from '~/utils/sshClient'
|
||||
|
||||
interface DogecloudTokenFull {
|
||||
Credentials: {
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
sessionToken: string
|
||||
}
|
||||
ExpiredAt: number
|
||||
Buckets: {
|
||||
name: string
|
||||
s3Bucket: string
|
||||
s3Endpoint: string
|
||||
}[]
|
||||
}
|
||||
|
||||
const dogeRegionMap: IStringKeyMap = {
|
||||
'ap-shanghai': '0',
|
||||
'ap-beijing': '1',
|
||||
'ap-guangzhou': '2',
|
||||
'ap-chengdu': '3'
|
||||
}
|
||||
|
||||
async function dogecloudApi (
|
||||
apiPath: string,
|
||||
data = {},
|
||||
jsonMode: boolean = false,
|
||||
accessKey: string,
|
||||
secretKey: string
|
||||
) {
|
||||
const body = jsonMode ? JSON.stringify(data) : querystring.encode(data)
|
||||
const sign = crypto
|
||||
.createHmac('sha1', secretKey)
|
||||
.update(Buffer.from(apiPath + '\n' + body, 'utf8'))
|
||||
.digest('hex')
|
||||
const authorization = `TOKEN ${accessKey}:${sign}`
|
||||
try {
|
||||
const res = await axios.request({
|
||||
url: `https://api.dogecloud.com${apiPath}`,
|
||||
method: 'POST',
|
||||
data: body,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded',
|
||||
Authorization: authorization
|
||||
}
|
||||
})
|
||||
if (res.data.code !== 200) {
|
||||
throw new Error('API Error')
|
||||
}
|
||||
return res.data.data
|
||||
} catch (err: any) {
|
||||
throw new Error('API Error')
|
||||
}
|
||||
}
|
||||
|
||||
async function getDogeToken (accessKey: string, secretKey: string): Promise<IObj | DogecloudTokenFull> {
|
||||
try {
|
||||
const data = await dogecloudApi(
|
||||
'/auth/tmp_token.json',
|
||||
{
|
||||
channel: 'OSS_FULL',
|
||||
scopes: ['*']
|
||||
},
|
||||
true,
|
||||
accessKey,
|
||||
secretKey
|
||||
)
|
||||
return data
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) {
|
||||
try {
|
||||
const {
|
||||
url: rawUrl,
|
||||
type,
|
||||
import crypto from 'node:crypto'
|
||||
import http, { AgentOptions } from 'node:http'
|
||||
import https from 'node:https'
|
||||
import path from 'node:path'
|
||||
import querystring from 'node:querystring'
|
||||
|
||||
import type { S3ClientConfig } from '@aws-sdk/client-s3'
|
||||
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
import logger from '@core/picgo/logger'
|
||||
import { NodeHttpHandler } from '@smithy/node-http-handler'
|
||||
import axios from 'axios'
|
||||
import type { ISftpPlistConfig } from 'piclist'
|
||||
|
||||
import type { IObj, IStringKeyMap } from '#/types/types'
|
||||
import { getAgent } from '~/manage/utils/common'
|
||||
import SSHClient from '~/utils/sshClient'
|
||||
|
||||
interface DogecloudTokenFull {
|
||||
Credentials: {
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
sessionToken: string
|
||||
}
|
||||
ExpiredAt: number
|
||||
Buckets: {
|
||||
name: string
|
||||
s3Bucket: string
|
||||
s3Endpoint: string
|
||||
}[]
|
||||
}
|
||||
|
||||
const dogeRegionMap: IStringKeyMap = {
|
||||
'ap-shanghai': '0',
|
||||
'ap-beijing': '1',
|
||||
'ap-guangzhou': '2',
|
||||
'ap-chengdu': '3'
|
||||
}
|
||||
|
||||
async function dogecloudApi(
|
||||
apiPath: string,
|
||||
data = {},
|
||||
jsonMode: boolean = false,
|
||||
accessKey: string,
|
||||
secretKey: string
|
||||
) {
|
||||
const body = jsonMode ? JSON.stringify(data) : querystring.encode(data)
|
||||
const sign = crypto
|
||||
.createHmac('sha1', secretKey)
|
||||
.update(Buffer.from(apiPath + '\n' + body, 'utf8'))
|
||||
.digest('hex')
|
||||
const authorization = `TOKEN ${accessKey}:${sign}`
|
||||
try {
|
||||
const res = await axios.request({
|
||||
url: `https://api.dogecloud.com${apiPath}`,
|
||||
method: 'POST',
|
||||
data: body,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded',
|
||||
Authorization: authorization
|
||||
}
|
||||
})
|
||||
if (res.data.code !== 200) {
|
||||
throw new Error('API Error')
|
||||
}
|
||||
return res.data.data
|
||||
} catch (err: any) {
|
||||
throw new Error('API Error')
|
||||
}
|
||||
}
|
||||
|
||||
async function getDogeToken(accessKey: string, secretKey: string): Promise<IObj | DogecloudTokenFull> {
|
||||
try {
|
||||
const data = await dogecloudApi(
|
||||
'/auth/tmp_token.json',
|
||||
{
|
||||
channel: 'OSS_FULL',
|
||||
scopes: ['*']
|
||||
},
|
||||
true,
|
||||
accessKey,
|
||||
secretKey
|
||||
)
|
||||
return data
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromS3InMain(configMap: IStringKeyMap, dogeMode: boolean = false) {
|
||||
try {
|
||||
const {
|
||||
url: rawUrl,
|
||||
type,
|
||||
config: {
|
||||
accessKeyID,
|
||||
secretAccessKey,
|
||||
@@ -103,14 +103,14 @@ export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode
|
||||
proxy,
|
||||
urlPrefix
|
||||
}
|
||||
} = configMap
|
||||
let {
|
||||
imgUrl,
|
||||
config: { region }
|
||||
} = configMap
|
||||
if (type === 'aws-s3' || type === 'aws-s3-plist') {
|
||||
imgUrl = rawUrl || imgUrl || ''
|
||||
}
|
||||
} = configMap
|
||||
let {
|
||||
imgUrl,
|
||||
config: { region }
|
||||
} = configMap
|
||||
if (type === 'aws-s3' || type === 'aws-s3-plist') {
|
||||
imgUrl = rawUrl || imgUrl || ''
|
||||
}
|
||||
let fileKey
|
||||
if (urlPrefix && imgUrl.startsWith(urlPrefix)) {
|
||||
const urlPrefixObj = new URL(urlPrefix)
|
||||
@@ -126,159 +126,159 @@ export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode
|
||||
if (pathStyleAccess) {
|
||||
fileKey = fileKey.replace(/^[^/]+\//, '')
|
||||
}
|
||||
}
|
||||
const endpointUrl: string | undefined = endpoint
|
||||
? /^https?:\/\//.test(endpoint)
|
||||
? endpoint
|
||||
: `http://${endpoint}`
|
||||
: undefined
|
||||
if (endpointUrl && endpointUrl.includes('cloudflarestorage')) {
|
||||
region = region || 'auto'
|
||||
}
|
||||
const sslEnabled = endpointUrl ? endpointUrl.startsWith('https') : true
|
||||
const agent = getAgent(proxy, sslEnabled)
|
||||
const commonOptions: AgentOptions = {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
|
||||
}
|
||||
const extraOptions = sslEnabled ? { rejectUnauthorized: !!rejectUnauthorized } : {}
|
||||
const handler = sslEnabled
|
||||
? new NodeHttpHandler({
|
||||
httpsAgent: agent.https
|
||||
? agent.https
|
||||
: new https.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
: new NodeHttpHandler({
|
||||
httpAgent: agent.http
|
||||
? agent.http
|
||||
: new http.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
const s3Options: S3ClientConfig = {
|
||||
credentials: {
|
||||
accessKeyId: accessKeyID,
|
||||
secretAccessKey
|
||||
},
|
||||
endpoint: endpointUrl,
|
||||
tls: sslEnabled,
|
||||
forcePathStyle: pathStyleAccess,
|
||||
region,
|
||||
requestHandler: handler
|
||||
}
|
||||
if (dogeMode) {
|
||||
s3Options.credentials = {
|
||||
accessKeyId: configMap.config.accessKeyID,
|
||||
secretAccessKey: configMap.config.secretAccessKey,
|
||||
sessionToken: configMap.config.sessionToken
|
||||
}
|
||||
}
|
||||
let result: any
|
||||
try {
|
||||
fileKey = decodeURIComponent(fileKey)
|
||||
} catch (err: any) {}
|
||||
try {
|
||||
const client = new S3Client(s3Options)
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: fileKey
|
||||
})
|
||||
result = await client.send(command)
|
||||
} catch (err: any) {
|
||||
s3Options.region = 'us-east-1'
|
||||
const client = new S3Client(s3Options)
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: fileKey
|
||||
})
|
||||
result = await client.send(command)
|
||||
}
|
||||
return result.$metadata.httpStatusCode === 204
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromDogeInMain (configMap: IStringKeyMap) {
|
||||
try {
|
||||
const {
|
||||
config: { bucketName, AccessKey, SecretKey }
|
||||
} = configMap
|
||||
const token = (await getDogeToken(AccessKey, SecretKey)) as DogecloudTokenFull
|
||||
const bucket = token.Buckets?.find(item => item.name === bucketName || item.s3Bucket === bucketName)
|
||||
const newConfigMap = { ...configMap }
|
||||
newConfigMap.config = {
|
||||
...newConfigMap.config,
|
||||
accessKeyID: token.Credentials?.accessKeyId,
|
||||
secretAccessKey: token.Credentials?.secretAccessKey,
|
||||
sessionToken: token.Credentials?.sessionToken,
|
||||
endpoint: bucket?.s3Endpoint,
|
||||
region: dogeRegionMap[bucket?.s3Endpoint?.split('.')[1] || 'ap-shanghai'],
|
||||
bucketName: bucket?.s3Bucket
|
||||
}
|
||||
return await removeFileFromS3InMain(newConfigMap, true)
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function createHuaweiAuthorization (
|
||||
bucketName: string,
|
||||
path: string,
|
||||
fileName: string,
|
||||
accessKey: string,
|
||||
secretKey: string,
|
||||
date: string = new Date().toUTCString()
|
||||
) {
|
||||
const strToSign = `DELETE\n\n\n${date}\n/${bucketName}${path}/${fileName}`
|
||||
const singature = crypto.createHmac('sha1', secretKey).update(strToSign).digest('base64')
|
||||
return `OBS ${accessKey}:${singature}`
|
||||
}
|
||||
|
||||
export async function removeFileFromHuaweiInMain (configMap: IStringKeyMap) {
|
||||
const { fileName, config } = configMap
|
||||
const { accessKeyId, accessKeySecret, bucketName, endpoint } = config
|
||||
let path = config.path || '/'
|
||||
path = `/${path.replace(/^\/+|\/+$/, '')}`
|
||||
path = path === '/' ? '' : path
|
||||
const date = new Date().toUTCString()
|
||||
const authorization = createHuaweiAuthorization(bucketName, path, fileName, accessKeyId, accessKeySecret, date)
|
||||
try {
|
||||
const res = await axios.request({
|
||||
url: `https://${bucketName}.${endpoint}${encodeURI(path)}/${encodeURIComponent(fileName)}`,
|
||||
method: 'DELETE',
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
Host: `${bucketName}.${endpoint}`,
|
||||
Date: date,
|
||||
Authorization: authorization
|
||||
}
|
||||
})
|
||||
return res.status === 204
|
||||
} catch (error: any) {
|
||||
logger.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromSFTPInMain (config: ISftpPlistConfig, fileName: string) {
|
||||
try {
|
||||
const client = SSHClient.instance
|
||||
await client.connect(config)
|
||||
const uploadPath = `/${config.uploadPath || ''}/`.replace(/\/+/g, '/')
|
||||
const remote = path.join(uploadPath, fileName)
|
||||
const deleteResult = await client.deleteFileSFTP(config, remote)
|
||||
client.close()
|
||||
return deleteResult
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
const endpointUrl: string | undefined = endpoint
|
||||
? /^https?:\/\//.test(endpoint)
|
||||
? endpoint
|
||||
: `http://${endpoint}`
|
||||
: undefined
|
||||
if (endpointUrl && endpointUrl.includes('cloudflarestorage')) {
|
||||
region = region || 'auto'
|
||||
}
|
||||
const sslEnabled = endpointUrl ? endpointUrl.startsWith('https') : true
|
||||
const agent = getAgent(proxy, sslEnabled)
|
||||
const commonOptions: AgentOptions = {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
|
||||
}
|
||||
const extraOptions = sslEnabled ? { rejectUnauthorized: !!rejectUnauthorized } : {}
|
||||
const handler = sslEnabled
|
||||
? new NodeHttpHandler({
|
||||
httpsAgent: agent.https
|
||||
? agent.https
|
||||
: new https.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
: new NodeHttpHandler({
|
||||
httpAgent: agent.http
|
||||
? agent.http
|
||||
: new http.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
const s3Options: S3ClientConfig = {
|
||||
credentials: {
|
||||
accessKeyId: accessKeyID,
|
||||
secretAccessKey
|
||||
},
|
||||
endpoint: endpointUrl,
|
||||
tls: sslEnabled,
|
||||
forcePathStyle: pathStyleAccess,
|
||||
region,
|
||||
requestHandler: handler
|
||||
}
|
||||
if (dogeMode) {
|
||||
s3Options.credentials = {
|
||||
accessKeyId: configMap.config.accessKeyID,
|
||||
secretAccessKey: configMap.config.secretAccessKey,
|
||||
sessionToken: configMap.config.sessionToken
|
||||
}
|
||||
}
|
||||
let result: any
|
||||
try {
|
||||
fileKey = decodeURIComponent(fileKey)
|
||||
} catch (err: any) {}
|
||||
try {
|
||||
const client = new S3Client(s3Options)
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: fileKey
|
||||
})
|
||||
result = await client.send(command)
|
||||
} catch (err: any) {
|
||||
s3Options.region = 'us-east-1'
|
||||
const client = new S3Client(s3Options)
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: fileKey
|
||||
})
|
||||
result = await client.send(command)
|
||||
}
|
||||
return result.$metadata.httpStatusCode === 204
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromDogeInMain(configMap: IStringKeyMap) {
|
||||
try {
|
||||
const {
|
||||
config: { bucketName, AccessKey, SecretKey }
|
||||
} = configMap
|
||||
const token = (await getDogeToken(AccessKey, SecretKey)) as DogecloudTokenFull
|
||||
const bucket = token.Buckets?.find(item => item.name === bucketName || item.s3Bucket === bucketName)
|
||||
const newConfigMap = { ...configMap }
|
||||
newConfigMap.config = {
|
||||
...newConfigMap.config,
|
||||
accessKeyID: token.Credentials?.accessKeyId,
|
||||
secretAccessKey: token.Credentials?.secretAccessKey,
|
||||
sessionToken: token.Credentials?.sessionToken,
|
||||
endpoint: bucket?.s3Endpoint,
|
||||
region: dogeRegionMap[bucket?.s3Endpoint?.split('.')[1] || 'ap-shanghai'],
|
||||
bucketName: bucket?.s3Bucket
|
||||
}
|
||||
return await removeFileFromS3InMain(newConfigMap, true)
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function createHuaweiAuthorization(
|
||||
bucketName: string,
|
||||
path: string,
|
||||
fileName: string,
|
||||
accessKey: string,
|
||||
secretKey: string,
|
||||
date: string = new Date().toUTCString()
|
||||
) {
|
||||
const strToSign = `DELETE\n\n\n${date}\n/${bucketName}${path}/${fileName}`
|
||||
const singature = crypto.createHmac('sha1', secretKey).update(strToSign).digest('base64')
|
||||
return `OBS ${accessKey}:${singature}`
|
||||
}
|
||||
|
||||
export async function removeFileFromHuaweiInMain(configMap: IStringKeyMap) {
|
||||
const { fileName, config } = configMap
|
||||
const { accessKeyId, accessKeySecret, bucketName, endpoint } = config
|
||||
let path = config.path || '/'
|
||||
path = `/${path.replace(/^\/+|\/+$/, '')}`
|
||||
path = path === '/' ? '' : path
|
||||
const date = new Date().toUTCString()
|
||||
const authorization = createHuaweiAuthorization(bucketName, path, fileName, accessKeyId, accessKeySecret, date)
|
||||
try {
|
||||
const res = await axios.request({
|
||||
url: `https://${bucketName}.${endpoint}${encodeURI(path)}/${encodeURIComponent(fileName)}`,
|
||||
method: 'DELETE',
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
Host: `${bucketName}.${endpoint}`,
|
||||
Date: date,
|
||||
Authorization: authorization
|
||||
}
|
||||
})
|
||||
return res.status === 204
|
||||
} catch (error: any) {
|
||||
logger.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromSFTPInMain(config: ISftpPlistConfig, fileName: string) {
|
||||
try {
|
||||
const client = SSHClient.instance
|
||||
await client.connect(config)
|
||||
const uploadPath = `/${config.uploadPath || ''}/`.replace(/\/+/g, '/')
|
||||
const remote = path.join(uploadPath, fileName)
|
||||
const deleteResult = await client.deleteFileSFTP(config, remote)
|
||||
client.close()
|
||||
return deleteResult
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const deleteLog = (fileName?: string, type?: string, isSuccess = true, msg?: string) => {
|
||||
console.log(`Delete ${fileName} on ${type} ${isSuccess ? 'success' : 'failed'}, message: ${msg || ''}`)
|
||||
}
|
||||
|
||||
export const deleteFailedLog = (fileName: string, type: string, error: any) => {
|
||||
deleteLog(fileName, type, false)
|
||||
console.error(error)
|
||||
}
|
||||
export const deleteLog = (fileName?: string, type?: string, isSuccess = true, msg?: string) => {
|
||||
console.log(`Delete ${fileName} on ${type} ${isSuccess ? 'success' : 'failed'}, message: ${msg || ''}`)
|
||||
}
|
||||
|
||||
export const deleteFailedLog = (fileName: string, type: string, error: any) => {
|
||||
deleteLog(fileName, type, false)
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ const AUTH_KEY_VALUE_RE = /(\w+)=["']?([^'"]{1,10000})["']?/
|
||||
let NC = 0
|
||||
const NC_PAD = '00000000'
|
||||
|
||||
function md5 (text: crypto.BinaryLike) {
|
||||
function md5(text: crypto.BinaryLike) {
|
||||
return crypto.createHash('md5').update(text).digest('hex')
|
||||
}
|
||||
|
||||
export function digestAuthHeader (
|
||||
export function digestAuthHeader(
|
||||
method: string,
|
||||
uri: string,
|
||||
wwwAuthenticate: string,
|
||||
@@ -70,7 +70,7 @@ export function digestAuthHeader (
|
||||
return authstring
|
||||
}
|
||||
|
||||
export async function getAuthHeader (method: string, host: string, uri: string, username: string, password: string) {
|
||||
export async function getAuthHeader(method: string, host: string, uri: string, username: string, password: string) {
|
||||
try {
|
||||
await axios.get(`${host}${uri}`)
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,242 +1,242 @@
|
||||
export const ILogType = {
|
||||
success: 'success',
|
||||
info: 'info',
|
||||
warn: 'warn',
|
||||
error: 'error'
|
||||
}
|
||||
|
||||
export const ICOREBuildInEvent = {
|
||||
UPLOAD_PROGRESS: 'uploadProgress',
|
||||
FAILED: 'failed',
|
||||
BEFORE_TRANSFORM: 'beforeTransform',
|
||||
BEFORE_UPLOAD: 'beforeUpload',
|
||||
AFTER_UPLOAD: 'afterUpload',
|
||||
FINISHED: 'finished',
|
||||
INSTALL: 'install',
|
||||
UNINSTALL: 'uninstall',
|
||||
UPDATE: 'update',
|
||||
NOTIFICATION: 'notification',
|
||||
REMOVE: 'remove'
|
||||
}
|
||||
|
||||
export const IPicGoHelperType = {
|
||||
afterUploadPlugins: 'afterUploadPlugins',
|
||||
beforeTransformPlugins: 'beforeTransformPlugins',
|
||||
beforeUploadPlugins: 'beforeUploadPlugins',
|
||||
uploader: 'uploader',
|
||||
transformer: 'transformer'
|
||||
}
|
||||
|
||||
export const IPasteStyle = {
|
||||
MARKDOWN: 'markdown',
|
||||
HTML: 'HTML',
|
||||
URL: 'URL',
|
||||
UBB: 'UBB',
|
||||
CUSTOM: 'Custom'
|
||||
}
|
||||
|
||||
export const IWindowList = {
|
||||
SETTING_WINDOW: 'SETTING_WINDOW',
|
||||
TRAY_WINDOW: 'TRAY_WINDOW',
|
||||
MINI_WINDOW: 'MINI_WINDOW',
|
||||
RENAME_WINDOW: 'RENAME_WINDOW',
|
||||
TOOLBOX_WINDOW: 'TOOLBOX_WINDOW'
|
||||
}
|
||||
|
||||
export const IRemoteNoticeActionType = {
|
||||
OPEN_URL: 'OPEN_URL',
|
||||
SHOW_NOTICE: 'SHOW_NOTICE', // notification
|
||||
SHOW_DIALOG: 'SHOW_DIALOG', // dialog notice
|
||||
COMMON: 'COMMON',
|
||||
VOID: 'VOID', // do nothing
|
||||
SHOW_MESSAGE_BOX: 'SHOW_MESSAGE_BOX'
|
||||
}
|
||||
|
||||
export const IRemoteNoticeTriggerHook = {
|
||||
APP_START: 'APP_START',
|
||||
SETTING_WINDOW_OPEN: 'SETTING_WINDOW_OPEN'
|
||||
}
|
||||
|
||||
export const IRemoteNoticeTriggerCount = {
|
||||
ONCE: 'ONCE', // default
|
||||
ALWAYS: 'ALWAYS'
|
||||
}
|
||||
|
||||
export const IRPCType = {
|
||||
INVOKE: 'INVOKE',
|
||||
SEND: 'SEND'
|
||||
}
|
||||
|
||||
export const IRPCActionType = {
|
||||
// system rpc
|
||||
RELOAD_APP: 'RELOAD_APP',
|
||||
OPEN_URL: 'OPEN_URL',
|
||||
OPEN_FILE: 'OPEN_FILE',
|
||||
HIDE_DOCK: 'HIDE_DOCK',
|
||||
SET_CURRENT_LANGUAGE: 'SET_CURRENT_LANGUAGE',
|
||||
OPEN_WINDOW: 'OPEN_WINDOW',
|
||||
OPEN_MINI_WINDOW: 'OPEN_MINI_WINDOW',
|
||||
CLOSE_WINDOW: 'CLOSE_WINDOW',
|
||||
MINIMIZE_WINDOW: 'MINIMIZE_WINDOW',
|
||||
SHOW_MINI_PAGE_MENU: 'SHOW_MINI_PAGE_MENU',
|
||||
SHOW_MAIN_PAGE_MENU: 'SHOW_MAIN_PAGE_MENU',
|
||||
SHOW_UPLOAD_PAGE_MENU: 'SHOW_UPLOAD_PAGE_MENU',
|
||||
SHOW_SECOND_UPLOADER_MENU: 'SHOW_SECOND_UPLOADER_MENU',
|
||||
SHOW_PLUGIN_PAGE_MENU: 'SHOW_PLUGIN_PAGE_MENU',
|
||||
SET_MINI_WINDOW_POS: 'SET_MINI_WINDOW_POS',
|
||||
MINI_WINDOW_ON_TOP: 'MINI_WINDOW_ON_TOP',
|
||||
MAIN_WINDOW_ON_TOP: 'MAIN_WINDOW_ON_TOP',
|
||||
UPDATE_MINI_WINDOW_ICON: 'UPDATE_MINI_WINDOW_ICON',
|
||||
REFRESH_SETTING_WINDOW: 'REFRESH_SETTING_WINDOW',
|
||||
// picbed RPC
|
||||
PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG',
|
||||
PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
|
||||
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
|
||||
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
|
||||
UPLOADER_SELECT: 'UPLOADER_SELECT',
|
||||
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',
|
||||
UPLOADER_RESET_CONFIG: 'UPLOADER_RESET_CONFIG',
|
||||
DELETE_ALL_API: 'DELETE_ALL_API',
|
||||
|
||||
// toolbox rpc
|
||||
TOOLBOX_CHECK: 'TOOLBOX_CHECK',
|
||||
TOOLBOX_CHECK_RES: 'TOOLBOX_CHECK_RES',
|
||||
TOOLBOX_CHECK_FIX: 'TOOLBOX_CHECK_FIX',
|
||||
|
||||
// main app setting rpc
|
||||
PICLIST_GET_CONFIG: 'PICLIST_GET_CONFIG',
|
||||
PICLIST_GET_CONFIG_SYNC: 'PICLIST_GET_CONFIG_SYNC',
|
||||
PICLIST_SAVE_CONFIG: 'PICLIST_SAVE_CONFIG',
|
||||
PICLIST_OPEN_FILE: 'PICLIST_OPEN_FILE',
|
||||
PICLIST_OPEN_DIRECTORY: 'PICLIST_OPEN_DIRECTORY',
|
||||
PICLIST_AUTO_START: 'PICLIST_AUTO_START',
|
||||
|
||||
// shortkey setting rpc
|
||||
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
|
||||
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
|
||||
SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE: 'SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE',
|
||||
|
||||
// configuration setting rpc
|
||||
CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO',
|
||||
CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG',
|
||||
CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_CONFIG',
|
||||
CONFIGURE_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG',
|
||||
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
|
||||
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
|
||||
CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG',
|
||||
|
||||
// advanced setting rpc
|
||||
ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER',
|
||||
ADVANCED_STOP_WEB_SERVER: 'ADVANCED_STOP_WEB_SERVER',
|
||||
ADVANCED_RESTART_WEB_SERVER: 'ADVANCED_RESTART_WEB_SERVER',
|
||||
|
||||
// upload and main page rpc
|
||||
MAIN_GET_PICBED: 'MAIN_GET_PICBED',
|
||||
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
|
||||
UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES',
|
||||
|
||||
// gallery rpc
|
||||
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
|
||||
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
|
||||
GALLERY_GET_DB: 'GALLERY_GET_DB',
|
||||
GALLERY_GET_BY_ID_DB: 'GALLERY_GET_BY_ID_DB',
|
||||
GALLERY_UPDATE_BY_ID_DB: 'GALLERY_UPDATE_BY_ID_DB',
|
||||
GALLERY_REMOVE_BY_ID_DB: 'GALLERY_REMOVE_BY_ID_DB',
|
||||
GALLERY_INSERT_DB: 'GALLERY_INSERT_DB',
|
||||
GALLERY_INSERT_DB_BATCH: 'GALLERY_INSERT_DB_BATCH',
|
||||
// plugin rpc
|
||||
PLUGIN_GET_LIST: 'PLUGIN_GET_LIST',
|
||||
PLUGIN_INSTALL: 'PLUGIN_INSTALL',
|
||||
PLUGIN_IMPORT_LOCAL: 'PLUGIN_IMPORT_LOCAL',
|
||||
PLUGIN_UPDATE_ALL: 'PLUGIN_UPDATE_ALL',
|
||||
|
||||
// tray rpc
|
||||
TRAY_SET_TOOL_TIP: 'TRAY_SET_TOOL_TIP',
|
||||
TRAY_GET_SHORT_URL: 'TRAY_GET_SHORT_URL',
|
||||
TRAY_UPLOAD_CLIPBOARD_FILES: 'TRAY_UPLOAD_CLIPBOARD_FILES',
|
||||
|
||||
// manage rpc
|
||||
MANAGE_GET_CONFIG: 'MANAGE_GET_CONFIG',
|
||||
MANAGE_SAVE_CONFIG: 'MANAGE_SAVE_CONFIG',
|
||||
MANAGE_REMOVE_CONFIG: 'MANAGE_REMOVE_CONFIG',
|
||||
MANAGE_GET_BUCKET_LIST: 'MANAGE_GET_BUCKET_LIST',
|
||||
MANAGE_GET_BUCKET_LIST_BACKSTAGE: 'MANAGE_GET_BUCKET_LIST_BACKSTAGE',
|
||||
MANAGE_GET_BUCKET_LIST_RECURSIVELY: 'MANAGE_GET_BUCKET_LIST_RECURSIVELY',
|
||||
MANAGE_CREATE_BUCKET: 'MANAGE_CREATE_BUCKET',
|
||||
MANAGE_GET_BUCKET_FILE_LIST: 'MANAGE_GET_BUCKET_FILE_LIST',
|
||||
MANAGE_GET_BUCKET_DOMAIN: 'MANAGE_GET_BUCKET_DOMAIN',
|
||||
MANAGE_SET_BUCKET_ACL_POLICY: 'MANAGE_SET_BUCKET_ACL_POLICY',
|
||||
MANAGE_RENAME_BUCKET_FILE: 'MANAGE_RENAME_BUCKET_FILE',
|
||||
MANAGE_DELETE_BUCKET_FILE: 'MANAGE_DELETE_BUCKET_FILE',
|
||||
MANAGE_DELETE_BUCKET_FOLDER: 'MANAGE_DELETE_BUCKET_FOLDER',
|
||||
MANAGE_GET_PRE_SIGNED_URL: 'MANAGE_GET_PRE_SIGNED_URL',
|
||||
MANAGE_UPLOAD_BUCKET_FILE: 'MANAGE_UPLOAD_BUCKET_FILE',
|
||||
MANAGE_DOWNLOAD_BUCKET_FILE: 'MANAGE_DOWNLOAD_BUCKET_FILE',
|
||||
MANAGE_CREATE_BUCKET_FOLDER: 'MANAGE_CREATE_BUCKET_FOLDER',
|
||||
MANAGE_OPEN_FILE_SELECT_DIALOG: 'MANAGE_OPEN_FILE_SELECT_DIALOG',
|
||||
MANAGE_GET_UPLOAD_TASK_LIST: 'MANAGE_GET_UPLOAD_TASK_LIST',
|
||||
MANAGE_GET_DOWNLOAD_TASK_LIST: 'MANAGE_GET_DOWNLOAD_TASK_LIST',
|
||||
MANAGE_DELETE_UPLOADED_TASK: 'MANAGE_DELETE_UPLOADED_TASK',
|
||||
MANAGE_DELETE_ALL_UPLOADED_TASK: 'MANAGE_DELETE_ALL_UPLOADED_TASK',
|
||||
MANAGE_DELETE_DOWNLOADED_TASK: 'MANAGE_DELETE_DOWNLOADED_TASK',
|
||||
MANAGE_DELETE_ALL_DOWNLOADED_TASK: 'MANAGE_DELETE_ALL_DOWNLOADED_TASK',
|
||||
MANAGE_SELECT_DOWNLOAD_FOLDER: 'MANAGE_SELECT_DOWNLOAD_FOLDER',
|
||||
MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER: 'MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER',
|
||||
MANAGE_OPEN_DOWNLOADED_FOLDER: 'MANAGE_OPEN_DOWNLOADED_FOLDER',
|
||||
MANAGE_OPEN_LOCAL_FILE: 'MANAGE_OPEN_LOCAL_FILE',
|
||||
MANAGE_DOWNLOAD_FILE_FROM_URL: 'MANAGE_DOWNLOAD_FILE_FROM_URL',
|
||||
MANAGE_CONVERT_PATH_TO_BASE64: 'MANAGE_CONVERT_PATH_TO_BASE64'
|
||||
}
|
||||
|
||||
export const IToolboxItemType = {
|
||||
IS_CONFIG_FILE_BROKEN: 'IS_CONFIG_FILE_BROKEN',
|
||||
IS_GALLERY_FILE_BROKEN: 'IS_GALLERY_FILE_BROKEN',
|
||||
HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 'HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD',
|
||||
HAS_PROBLEM_WITH_PROXY: 'HAS_PROBLEM_WITH_PROXY'
|
||||
}
|
||||
|
||||
export const IToolboxItemCheckStatus = {
|
||||
INIT: 'init',
|
||||
LOADING: 'loading',
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error'
|
||||
}
|
||||
|
||||
export const ISartMode = {
|
||||
QUIET: 'quiet',
|
||||
MINI: 'mini',
|
||||
MAIN: 'main',
|
||||
NO_TRAY: 'no-tray'
|
||||
}
|
||||
|
||||
export const II18nLanguage = {
|
||||
ZH_CN: 'zh-CN',
|
||||
ZH_TW: 'zh-TW',
|
||||
EN: 'en'
|
||||
}
|
||||
|
||||
export const IShortUrlServer = {
|
||||
C1N: 'c1n',
|
||||
YOURLS: 'yourls',
|
||||
CFWORKER: 'cf_worker',
|
||||
SINK: 'sink'
|
||||
}
|
||||
|
||||
export const commonTaskStatus = {
|
||||
queuing: 'queuing',
|
||||
failed: 'failed',
|
||||
canceled: 'canceled',
|
||||
paused: 'paused'
|
||||
}
|
||||
|
||||
// manage task status
|
||||
|
||||
export const uploadTaskSpecialStatus = {
|
||||
uploading: 'uploading',
|
||||
uploaded: 'uploaded'
|
||||
}
|
||||
|
||||
export const downloadTaskSpecialStatus = {
|
||||
downloading: 'downloading',
|
||||
downloaded: 'downloaded'
|
||||
}
|
||||
export const ILogType = {
|
||||
success: 'success',
|
||||
info: 'info',
|
||||
warn: 'warn',
|
||||
error: 'error'
|
||||
}
|
||||
|
||||
export const ICOREBuildInEvent = {
|
||||
UPLOAD_PROGRESS: 'uploadProgress',
|
||||
FAILED: 'failed',
|
||||
BEFORE_TRANSFORM: 'beforeTransform',
|
||||
BEFORE_UPLOAD: 'beforeUpload',
|
||||
AFTER_UPLOAD: 'afterUpload',
|
||||
FINISHED: 'finished',
|
||||
INSTALL: 'install',
|
||||
UNINSTALL: 'uninstall',
|
||||
UPDATE: 'update',
|
||||
NOTIFICATION: 'notification',
|
||||
REMOVE: 'remove'
|
||||
}
|
||||
|
||||
export const IPicGoHelperType = {
|
||||
afterUploadPlugins: 'afterUploadPlugins',
|
||||
beforeTransformPlugins: 'beforeTransformPlugins',
|
||||
beforeUploadPlugins: 'beforeUploadPlugins',
|
||||
uploader: 'uploader',
|
||||
transformer: 'transformer'
|
||||
}
|
||||
|
||||
export const IPasteStyle = {
|
||||
MARKDOWN: 'markdown',
|
||||
HTML: 'HTML',
|
||||
URL: 'URL',
|
||||
UBB: 'UBB',
|
||||
CUSTOM: 'Custom'
|
||||
}
|
||||
|
||||
export const IWindowList = {
|
||||
SETTING_WINDOW: 'SETTING_WINDOW',
|
||||
TRAY_WINDOW: 'TRAY_WINDOW',
|
||||
MINI_WINDOW: 'MINI_WINDOW',
|
||||
RENAME_WINDOW: 'RENAME_WINDOW',
|
||||
TOOLBOX_WINDOW: 'TOOLBOX_WINDOW'
|
||||
}
|
||||
|
||||
export const IRemoteNoticeActionType = {
|
||||
OPEN_URL: 'OPEN_URL',
|
||||
SHOW_NOTICE: 'SHOW_NOTICE', // notification
|
||||
SHOW_DIALOG: 'SHOW_DIALOG', // dialog notice
|
||||
COMMON: 'COMMON',
|
||||
VOID: 'VOID', // do nothing
|
||||
SHOW_MESSAGE_BOX: 'SHOW_MESSAGE_BOX'
|
||||
}
|
||||
|
||||
export const IRemoteNoticeTriggerHook = {
|
||||
APP_START: 'APP_START',
|
||||
SETTING_WINDOW_OPEN: 'SETTING_WINDOW_OPEN'
|
||||
}
|
||||
|
||||
export const IRemoteNoticeTriggerCount = {
|
||||
ONCE: 'ONCE', // default
|
||||
ALWAYS: 'ALWAYS'
|
||||
}
|
||||
|
||||
export const IRPCType = {
|
||||
INVOKE: 'INVOKE',
|
||||
SEND: 'SEND'
|
||||
}
|
||||
|
||||
export const IRPCActionType = {
|
||||
// system rpc
|
||||
RELOAD_APP: 'RELOAD_APP',
|
||||
OPEN_URL: 'OPEN_URL',
|
||||
OPEN_FILE: 'OPEN_FILE',
|
||||
HIDE_DOCK: 'HIDE_DOCK',
|
||||
SET_CURRENT_LANGUAGE: 'SET_CURRENT_LANGUAGE',
|
||||
OPEN_WINDOW: 'OPEN_WINDOW',
|
||||
OPEN_MINI_WINDOW: 'OPEN_MINI_WINDOW',
|
||||
CLOSE_WINDOW: 'CLOSE_WINDOW',
|
||||
MINIMIZE_WINDOW: 'MINIMIZE_WINDOW',
|
||||
SHOW_MINI_PAGE_MENU: 'SHOW_MINI_PAGE_MENU',
|
||||
SHOW_MAIN_PAGE_MENU: 'SHOW_MAIN_PAGE_MENU',
|
||||
SHOW_UPLOAD_PAGE_MENU: 'SHOW_UPLOAD_PAGE_MENU',
|
||||
SHOW_SECOND_UPLOADER_MENU: 'SHOW_SECOND_UPLOADER_MENU',
|
||||
SHOW_PLUGIN_PAGE_MENU: 'SHOW_PLUGIN_PAGE_MENU',
|
||||
SET_MINI_WINDOW_POS: 'SET_MINI_WINDOW_POS',
|
||||
MINI_WINDOW_ON_TOP: 'MINI_WINDOW_ON_TOP',
|
||||
MAIN_WINDOW_ON_TOP: 'MAIN_WINDOW_ON_TOP',
|
||||
UPDATE_MINI_WINDOW_ICON: 'UPDATE_MINI_WINDOW_ICON',
|
||||
REFRESH_SETTING_WINDOW: 'REFRESH_SETTING_WINDOW',
|
||||
// picbed RPC
|
||||
PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG',
|
||||
PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
|
||||
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
|
||||
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
|
||||
UPLOADER_SELECT: 'UPLOADER_SELECT',
|
||||
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',
|
||||
UPLOADER_RESET_CONFIG: 'UPLOADER_RESET_CONFIG',
|
||||
DELETE_ALL_API: 'DELETE_ALL_API',
|
||||
|
||||
// toolbox rpc
|
||||
TOOLBOX_CHECK: 'TOOLBOX_CHECK',
|
||||
TOOLBOX_CHECK_RES: 'TOOLBOX_CHECK_RES',
|
||||
TOOLBOX_CHECK_FIX: 'TOOLBOX_CHECK_FIX',
|
||||
|
||||
// main app setting rpc
|
||||
PICLIST_GET_CONFIG: 'PICLIST_GET_CONFIG',
|
||||
PICLIST_GET_CONFIG_SYNC: 'PICLIST_GET_CONFIG_SYNC',
|
||||
PICLIST_SAVE_CONFIG: 'PICLIST_SAVE_CONFIG',
|
||||
PICLIST_OPEN_FILE: 'PICLIST_OPEN_FILE',
|
||||
PICLIST_OPEN_DIRECTORY: 'PICLIST_OPEN_DIRECTORY',
|
||||
PICLIST_AUTO_START: 'PICLIST_AUTO_START',
|
||||
|
||||
// shortkey setting rpc
|
||||
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
|
||||
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
|
||||
SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE: 'SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE',
|
||||
|
||||
// configuration setting rpc
|
||||
CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO',
|
||||
CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG',
|
||||
CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_CONFIG',
|
||||
CONFIGURE_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG',
|
||||
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
|
||||
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
|
||||
CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG',
|
||||
|
||||
// advanced setting rpc
|
||||
ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER',
|
||||
ADVANCED_STOP_WEB_SERVER: 'ADVANCED_STOP_WEB_SERVER',
|
||||
ADVANCED_RESTART_WEB_SERVER: 'ADVANCED_RESTART_WEB_SERVER',
|
||||
|
||||
// upload and main page rpc
|
||||
MAIN_GET_PICBED: 'MAIN_GET_PICBED',
|
||||
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
|
||||
UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES',
|
||||
|
||||
// gallery rpc
|
||||
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
|
||||
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
|
||||
GALLERY_GET_DB: 'GALLERY_GET_DB',
|
||||
GALLERY_GET_BY_ID_DB: 'GALLERY_GET_BY_ID_DB',
|
||||
GALLERY_UPDATE_BY_ID_DB: 'GALLERY_UPDATE_BY_ID_DB',
|
||||
GALLERY_REMOVE_BY_ID_DB: 'GALLERY_REMOVE_BY_ID_DB',
|
||||
GALLERY_INSERT_DB: 'GALLERY_INSERT_DB',
|
||||
GALLERY_INSERT_DB_BATCH: 'GALLERY_INSERT_DB_BATCH',
|
||||
// plugin rpc
|
||||
PLUGIN_GET_LIST: 'PLUGIN_GET_LIST',
|
||||
PLUGIN_INSTALL: 'PLUGIN_INSTALL',
|
||||
PLUGIN_IMPORT_LOCAL: 'PLUGIN_IMPORT_LOCAL',
|
||||
PLUGIN_UPDATE_ALL: 'PLUGIN_UPDATE_ALL',
|
||||
|
||||
// tray rpc
|
||||
TRAY_SET_TOOL_TIP: 'TRAY_SET_TOOL_TIP',
|
||||
TRAY_GET_SHORT_URL: 'TRAY_GET_SHORT_URL',
|
||||
TRAY_UPLOAD_CLIPBOARD_FILES: 'TRAY_UPLOAD_CLIPBOARD_FILES',
|
||||
|
||||
// manage rpc
|
||||
MANAGE_GET_CONFIG: 'MANAGE_GET_CONFIG',
|
||||
MANAGE_SAVE_CONFIG: 'MANAGE_SAVE_CONFIG',
|
||||
MANAGE_REMOVE_CONFIG: 'MANAGE_REMOVE_CONFIG',
|
||||
MANAGE_GET_BUCKET_LIST: 'MANAGE_GET_BUCKET_LIST',
|
||||
MANAGE_GET_BUCKET_LIST_BACKSTAGE: 'MANAGE_GET_BUCKET_LIST_BACKSTAGE',
|
||||
MANAGE_GET_BUCKET_LIST_RECURSIVELY: 'MANAGE_GET_BUCKET_LIST_RECURSIVELY',
|
||||
MANAGE_CREATE_BUCKET: 'MANAGE_CREATE_BUCKET',
|
||||
MANAGE_GET_BUCKET_FILE_LIST: 'MANAGE_GET_BUCKET_FILE_LIST',
|
||||
MANAGE_GET_BUCKET_DOMAIN: 'MANAGE_GET_BUCKET_DOMAIN',
|
||||
MANAGE_SET_BUCKET_ACL_POLICY: 'MANAGE_SET_BUCKET_ACL_POLICY',
|
||||
MANAGE_RENAME_BUCKET_FILE: 'MANAGE_RENAME_BUCKET_FILE',
|
||||
MANAGE_DELETE_BUCKET_FILE: 'MANAGE_DELETE_BUCKET_FILE',
|
||||
MANAGE_DELETE_BUCKET_FOLDER: 'MANAGE_DELETE_BUCKET_FOLDER',
|
||||
MANAGE_GET_PRE_SIGNED_URL: 'MANAGE_GET_PRE_SIGNED_URL',
|
||||
MANAGE_UPLOAD_BUCKET_FILE: 'MANAGE_UPLOAD_BUCKET_FILE',
|
||||
MANAGE_DOWNLOAD_BUCKET_FILE: 'MANAGE_DOWNLOAD_BUCKET_FILE',
|
||||
MANAGE_CREATE_BUCKET_FOLDER: 'MANAGE_CREATE_BUCKET_FOLDER',
|
||||
MANAGE_OPEN_FILE_SELECT_DIALOG: 'MANAGE_OPEN_FILE_SELECT_DIALOG',
|
||||
MANAGE_GET_UPLOAD_TASK_LIST: 'MANAGE_GET_UPLOAD_TASK_LIST',
|
||||
MANAGE_GET_DOWNLOAD_TASK_LIST: 'MANAGE_GET_DOWNLOAD_TASK_LIST',
|
||||
MANAGE_DELETE_UPLOADED_TASK: 'MANAGE_DELETE_UPLOADED_TASK',
|
||||
MANAGE_DELETE_ALL_UPLOADED_TASK: 'MANAGE_DELETE_ALL_UPLOADED_TASK',
|
||||
MANAGE_DELETE_DOWNLOADED_TASK: 'MANAGE_DELETE_DOWNLOADED_TASK',
|
||||
MANAGE_DELETE_ALL_DOWNLOADED_TASK: 'MANAGE_DELETE_ALL_DOWNLOADED_TASK',
|
||||
MANAGE_SELECT_DOWNLOAD_FOLDER: 'MANAGE_SELECT_DOWNLOAD_FOLDER',
|
||||
MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER: 'MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER',
|
||||
MANAGE_OPEN_DOWNLOADED_FOLDER: 'MANAGE_OPEN_DOWNLOADED_FOLDER',
|
||||
MANAGE_OPEN_LOCAL_FILE: 'MANAGE_OPEN_LOCAL_FILE',
|
||||
MANAGE_DOWNLOAD_FILE_FROM_URL: 'MANAGE_DOWNLOAD_FILE_FROM_URL',
|
||||
MANAGE_CONVERT_PATH_TO_BASE64: 'MANAGE_CONVERT_PATH_TO_BASE64'
|
||||
}
|
||||
|
||||
export const IToolboxItemType = {
|
||||
IS_CONFIG_FILE_BROKEN: 'IS_CONFIG_FILE_BROKEN',
|
||||
IS_GALLERY_FILE_BROKEN: 'IS_GALLERY_FILE_BROKEN',
|
||||
HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 'HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD',
|
||||
HAS_PROBLEM_WITH_PROXY: 'HAS_PROBLEM_WITH_PROXY'
|
||||
}
|
||||
|
||||
export const IToolboxItemCheckStatus = {
|
||||
INIT: 'init',
|
||||
LOADING: 'loading',
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error'
|
||||
}
|
||||
|
||||
export const ISartMode = {
|
||||
QUIET: 'quiet',
|
||||
MINI: 'mini',
|
||||
MAIN: 'main',
|
||||
NO_TRAY: 'no-tray'
|
||||
}
|
||||
|
||||
export const II18nLanguage = {
|
||||
ZH_CN: 'zh-CN',
|
||||
ZH_TW: 'zh-TW',
|
||||
EN: 'en'
|
||||
}
|
||||
|
||||
export const IShortUrlServer = {
|
||||
C1N: 'c1n',
|
||||
YOURLS: 'yourls',
|
||||
CFWORKER: 'cf_worker',
|
||||
SINK: 'sink'
|
||||
}
|
||||
|
||||
export const commonTaskStatus = {
|
||||
queuing: 'queuing',
|
||||
failed: 'failed',
|
||||
canceled: 'canceled',
|
||||
paused: 'paused'
|
||||
}
|
||||
|
||||
// manage task status
|
||||
|
||||
export const uploadTaskSpecialStatus = {
|
||||
uploading: 'uploading',
|
||||
uploaded: 'uploaded'
|
||||
}
|
||||
|
||||
export const downloadTaskSpecialStatus = {
|
||||
downloading: 'downloading',
|
||||
downloaded: 'downloaded'
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const parseVersion = (plist: string) => {
|
||||
return matches[1].replace('10.16', '11')
|
||||
}
|
||||
|
||||
export function macOSVersion (): string {
|
||||
export function macOSVersion(): string {
|
||||
if (!isMacOS) return ''
|
||||
|
||||
if (!version) {
|
||||
@@ -43,7 +43,7 @@ if (process.env.NODE_ENV === 'test') {
|
||||
macOSVersion._parseVersion = parseVersion
|
||||
}
|
||||
|
||||
export function isMacOSVersion (semverRange: string) {
|
||||
export function isMacOSVersion(semverRange: string) {
|
||||
if (!isMacOS) {
|
||||
return false
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export function isMacOSVersion (semverRange: string) {
|
||||
return semver.satisfies(macOSVersion(), clean(semverRange))
|
||||
}
|
||||
|
||||
export function isMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
export function isMacOSVersionGreaterThanOrEqualTo(version: string) {
|
||||
if (!isMacOS) {
|
||||
return false
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export function isMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
return semver.gte(macOSVersion(), clean(version))
|
||||
}
|
||||
|
||||
export function assertMacOSVersion (semverRange: string) {
|
||||
export function assertMacOSVersion(semverRange: string) {
|
||||
semverRange = semverRange.replace('10.16', '11')
|
||||
|
||||
if (!isMacOSVersion(semverRange)) {
|
||||
@@ -71,7 +71,7 @@ export function assertMacOSVersion (semverRange: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function assertMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
export function assertMacOSVersionGreaterThanOrEqualTo(version: string) {
|
||||
version = version.replace('10.16', '11')
|
||||
|
||||
if (!isMacOSVersionGreaterThanOrEqualTo(version)) {
|
||||
@@ -79,7 +79,7 @@ export function assertMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function assertMacOS () {
|
||||
export function assertMacOS() {
|
||||
if (!isMacOS) {
|
||||
throw new Error('Requires macOS')
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import picgo from '@core/picgo'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { IPicGoPluginConfig, IPicGoPluginOriginConfig, IStringKeyMap, IUploaderConfigItem, IUploaderConfigListItem } from '#/types/types'
|
||||
import type {
|
||||
IPicGoPluginConfig,
|
||||
IPicGoPluginOriginConfig,
|
||||
IStringKeyMap,
|
||||
IUploaderConfigItem,
|
||||
IUploaderConfigListItem
|
||||
} from '#/types/types'
|
||||
import { setTrayToolTip, trimValues } from '~/utils/common'
|
||||
import { configPaths } from '~/utils/configPaths'
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { IAppNotification } from '#/types/types'
|
||||
|
||||
export const notificationList: IAppNotification[] = []
|
||||
import type { IAppNotification } from '#/types/types'
|
||||
|
||||
export const notificationList: IAppNotification[] = []
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
export class MemoryMonitor {
|
||||
// eslint-disable-next-line no-undef
|
||||
private static interval: NodeJS.Timeout | null = null
|
||||
|
||||
static start (intervalMs: number = 30000) {
|
||||
if (this.interval) return
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
const memUsage = process.memoryUsage()
|
||||
const mbUsage = {
|
||||
rss: Math.round(memUsage.rss / 1024 / 1024),
|
||||
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
|
||||
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
|
||||
external: Math.round(memUsage.external / 1024 / 1024)
|
||||
}
|
||||
console.log(`[Memory] RSS: ${mbUsage.rss}MB, Heap: ${mbUsage.heapUsed}/${mbUsage.heapTotal}MB, External: ${mbUsage.external}MB`)
|
||||
|
||||
if (mbUsage.heapUsed / mbUsage.heapTotal > 0.8 && global.gc) {
|
||||
console.log('[Memory] Triggering garbage collection')
|
||||
global.gc()
|
||||
}
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
static stop () {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
export class MemoryMonitor {
|
||||
// eslint-disable-next-line no-undef
|
||||
private static interval: NodeJS.Timeout | null = null
|
||||
|
||||
static start(intervalMs: number = 30000) {
|
||||
if (this.interval) return
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
const memUsage = process.memoryUsage()
|
||||
const mbUsage = {
|
||||
rss: Math.round(memUsage.rss / 1024 / 1024),
|
||||
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
|
||||
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
|
||||
external: Math.round(memUsage.external / 1024 / 1024)
|
||||
}
|
||||
console.log(
|
||||
`[Memory] RSS: ${mbUsage.rss}MB, Heap: ${mbUsage.heapUsed}/${mbUsage.heapTotal}MB, External: ${mbUsage.external}MB`
|
||||
)
|
||||
|
||||
if (mbUsage.heapUsed / mbUsage.heapTotal > 0.8 && global.gc) {
|
||||
console.log('[Memory] Triggering garbage collection')
|
||||
global.gc()
|
||||
}
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
static stop() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,194 +1,194 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import logger from '@core/picgo/logger'
|
||||
import fs from 'fs-extra'
|
||||
import { Config, NodeSSH, SSHExecCommandResponse } from 'node-ssh-no-cpu-features'
|
||||
import { ISftpPlistConfig } from 'piclist/dist/types'
|
||||
import { Client } from 'ssh2-no-cpu-features'
|
||||
|
||||
class SSHClient {
|
||||
private static _instance: SSHClient
|
||||
private static _client: NodeSSH
|
||||
private _isConnected = false
|
||||
|
||||
static get instance (): SSHClient {
|
||||
return this._instance || (this._instance = new this())
|
||||
}
|
||||
|
||||
static get client (): NodeSSH {
|
||||
return this._client || (this._client = new NodeSSH())
|
||||
}
|
||||
|
||||
private changeWinStylePathToUnix (path: string): string {
|
||||
return path.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
async connect (config: ISftpPlistConfig): Promise<boolean> {
|
||||
const { username, password, privateKey, passphrase } = config
|
||||
const loginInfo: Config = privateKey
|
||||
? {
|
||||
username,
|
||||
privateKeyPath: privateKey,
|
||||
passphrase: passphrase || undefined
|
||||
}
|
||||
: { username, password }
|
||||
try {
|
||||
await SSHClient.client.connect({
|
||||
host: config.host,
|
||||
port: Number(config.port) || 22,
|
||||
...loginInfo
|
||||
})
|
||||
this._isConnected = true
|
||||
return true
|
||||
} catch (err: any) {
|
||||
throw new Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFileSFTP (config: ISftpPlistConfig, remote: string): Promise<boolean> {
|
||||
try {
|
||||
const client = new Client()
|
||||
const { username, password, privateKey, passphrase } = config
|
||||
const loginInfo: Config = privateKey
|
||||
? {
|
||||
username,
|
||||
privateKey: fs.readFileSync(privateKey),
|
||||
passphrase: passphrase || undefined
|
||||
}
|
||||
: { username, password }
|
||||
remote = this.changeWinStylePathToUnix(remote)
|
||||
if (remote === '/' || remote.includes('*')) return false
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
client
|
||||
.on('ready', () => {
|
||||
client.sftp(
|
||||
(
|
||||
err: any,
|
||||
sftp: {
|
||||
unlink: (arg0: string, arg1: (err: any) => void) => void
|
||||
}
|
||||
) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
sftp.unlink(remote, (err: any) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
client.end()
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
.connect({
|
||||
host: config.host,
|
||||
port: Number(config.port) || 22,
|
||||
...loginInfo
|
||||
})
|
||||
})
|
||||
return (await promise) as boolean
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async exec (script: string): Promise<boolean> {
|
||||
const execResult = await SSHClient.client.execCommand(script)
|
||||
return execResult.code === 0
|
||||
}
|
||||
|
||||
async execCommand (script: string): Promise<SSHExecCommandResponse> {
|
||||
const execResult = await SSHClient.client.execCommand(script)
|
||||
return execResult || { code: 1, stdout: '', stderr: '' }
|
||||
}
|
||||
|
||||
async getFile (local: string, remote: string): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
try {
|
||||
remote = this.changeWinStylePathToUnix(remote)
|
||||
local = this.changeWinStylePathToUnix(local)
|
||||
await SSHClient.client.getFile(local, remote, undefined, {
|
||||
concurrency: 1
|
||||
})
|
||||
return true
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async putFile (
|
||||
local: string,
|
||||
remote: string,
|
||||
config: {
|
||||
fileMode?: string
|
||||
dirMode?: string
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
try {
|
||||
remote = this.changeWinStylePathToUnix(remote)
|
||||
await this.mkdir(path.dirname(remote).replace(/^\/+|\/+$/g, ''), config)
|
||||
await SSHClient.client.putFile(local, remote)
|
||||
const fileMode = config.fileMode || '0644'
|
||||
if (fileMode !== '0644') {
|
||||
const script = `chmod ${fileMode} "${remote}"`
|
||||
return await this.exec(script)
|
||||
}
|
||||
return true
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async mkdir (
|
||||
dirPath: string,
|
||||
config: {
|
||||
dirMode?: string
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
try {
|
||||
const directoryMode = config.dirMode || '0755'
|
||||
if (directoryMode === '0755') {
|
||||
const script = `mkdir -p "${dirPath}"`
|
||||
return await this.exec(script)
|
||||
} else {
|
||||
const dirs = dirPath.split('/')
|
||||
let currentPath = ''
|
||||
for (const dir of dirs) {
|
||||
if (dir) {
|
||||
currentPath += `/${dir}`
|
||||
const script = `mkdir "${currentPath}" && chmod ${directoryMode} "${currentPath}"`
|
||||
const result = await this.exec(script)
|
||||
if (!result) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
get isConnected (): boolean {
|
||||
return SSHClient.client.isConnected()
|
||||
}
|
||||
|
||||
close (): void {
|
||||
SSHClient.client.dispose()
|
||||
this._isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
export default SSHClient
|
||||
import path from 'node:path'
|
||||
|
||||
import logger from '@core/picgo/logger'
|
||||
import fs from 'fs-extra'
|
||||
import { Config, NodeSSH, SSHExecCommandResponse } from 'node-ssh-no-cpu-features'
|
||||
import { ISftpPlistConfig } from 'piclist/dist/types'
|
||||
import { Client } from 'ssh2-no-cpu-features'
|
||||
|
||||
class SSHClient {
|
||||
private static _instance: SSHClient
|
||||
private static _client: NodeSSH
|
||||
private _isConnected = false
|
||||
|
||||
static get instance(): SSHClient {
|
||||
return this._instance || (this._instance = new this())
|
||||
}
|
||||
|
||||
static get client(): NodeSSH {
|
||||
return this._client || (this._client = new NodeSSH())
|
||||
}
|
||||
|
||||
private changeWinStylePathToUnix(path: string): string {
|
||||
return path.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
async connect(config: ISftpPlistConfig): Promise<boolean> {
|
||||
const { username, password, privateKey, passphrase } = config
|
||||
const loginInfo: Config = privateKey
|
||||
? {
|
||||
username,
|
||||
privateKeyPath: privateKey,
|
||||
passphrase: passphrase || undefined
|
||||
}
|
||||
: { username, password }
|
||||
try {
|
||||
await SSHClient.client.connect({
|
||||
host: config.host,
|
||||
port: Number(config.port) || 22,
|
||||
...loginInfo
|
||||
})
|
||||
this._isConnected = true
|
||||
return true
|
||||
} catch (err: any) {
|
||||
throw new Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFileSFTP(config: ISftpPlistConfig, remote: string): Promise<boolean> {
|
||||
try {
|
||||
const client = new Client()
|
||||
const { username, password, privateKey, passphrase } = config
|
||||
const loginInfo: Config = privateKey
|
||||
? {
|
||||
username,
|
||||
privateKey: fs.readFileSync(privateKey),
|
||||
passphrase: passphrase || undefined
|
||||
}
|
||||
: { username, password }
|
||||
remote = this.changeWinStylePathToUnix(remote)
|
||||
if (remote === '/' || remote.includes('*')) return false
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
client
|
||||
.on('ready', () => {
|
||||
client.sftp(
|
||||
(
|
||||
err: any,
|
||||
sftp: {
|
||||
unlink: (arg0: string, arg1: (err: any) => void) => void
|
||||
}
|
||||
) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
sftp.unlink(remote, (err: any) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
client.end()
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
.connect({
|
||||
host: config.host,
|
||||
port: Number(config.port) || 22,
|
||||
...loginInfo
|
||||
})
|
||||
})
|
||||
return (await promise) as boolean
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async exec(script: string): Promise<boolean> {
|
||||
const execResult = await SSHClient.client.execCommand(script)
|
||||
return execResult.code === 0
|
||||
}
|
||||
|
||||
async execCommand(script: string): Promise<SSHExecCommandResponse> {
|
||||
const execResult = await SSHClient.client.execCommand(script)
|
||||
return execResult || { code: 1, stdout: '', stderr: '' }
|
||||
}
|
||||
|
||||
async getFile(local: string, remote: string): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
try {
|
||||
remote = this.changeWinStylePathToUnix(remote)
|
||||
local = this.changeWinStylePathToUnix(local)
|
||||
await SSHClient.client.getFile(local, remote, undefined, {
|
||||
concurrency: 1
|
||||
})
|
||||
return true
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async putFile(
|
||||
local: string,
|
||||
remote: string,
|
||||
config: {
|
||||
fileMode?: string
|
||||
dirMode?: string
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
try {
|
||||
remote = this.changeWinStylePathToUnix(remote)
|
||||
await this.mkdir(path.dirname(remote).replace(/^\/+|\/+$/g, ''), config)
|
||||
await SSHClient.client.putFile(local, remote)
|
||||
const fileMode = config.fileMode || '0644'
|
||||
if (fileMode !== '0644') {
|
||||
const script = `chmod ${fileMode} "${remote}"`
|
||||
return await this.exec(script)
|
||||
}
|
||||
return true
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async mkdir(
|
||||
dirPath: string,
|
||||
config: {
|
||||
dirMode?: string
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
try {
|
||||
const directoryMode = config.dirMode || '0755'
|
||||
if (directoryMode === '0755') {
|
||||
const script = `mkdir -p "${dirPath}"`
|
||||
return await this.exec(script)
|
||||
} else {
|
||||
const dirs = dirPath.split('/')
|
||||
let currentPath = ''
|
||||
for (const dir of dirs) {
|
||||
if (dir) {
|
||||
currentPath += `/${dir}`
|
||||
const script = `mkdir "${currentPath}" && chmod ${directoryMode} "${currentPath}"`
|
||||
const result = await this.exec(script)
|
||||
if (!result) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
get isConnected(): boolean {
|
||||
return SSHClient.client.isConnected()
|
||||
}
|
||||
|
||||
close(): void {
|
||||
SSHClient.client.dispose()
|
||||
this._isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
export default SSHClient
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
export const CLIPBOARD_IMAGE_FOLDER = 'piclist-clipboard-images'
|
||||
|
||||
export const cancelDownloadLoadingFileList = 'cancelDownloadLoadingFileList'
|
||||
export const refreshDownloadFileTransferList = 'refreshDownloadFileTransferList'
|
||||
|
||||
export const picBedsCanbeDeleted = [
|
||||
'aliyun',
|
||||
'alist',
|
||||
'alistplist',
|
||||
'aws-s3',
|
||||
'aws-s3-plist',
|
||||
'dogecloud',
|
||||
'github',
|
||||
'huaweicloud-uploader',
|
||||
'imgur',
|
||||
'local',
|
||||
'lskyplist',
|
||||
'piclist',
|
||||
'qiniu',
|
||||
'sftpplist',
|
||||
'smms',
|
||||
'tcyun',
|
||||
'upyun',
|
||||
'webdavplist'
|
||||
]
|
||||
export const CLIPBOARD_IMAGE_FOLDER = 'piclist-clipboard-images'
|
||||
|
||||
export const cancelDownloadLoadingFileList = 'cancelDownloadLoadingFileList'
|
||||
export const refreshDownloadFileTransferList = 'refreshDownloadFileTransferList'
|
||||
|
||||
export const picBedsCanbeDeleted = [
|
||||
'aliyun',
|
||||
'alist',
|
||||
'alistplist',
|
||||
'aws-s3',
|
||||
'aws-s3-plist',
|
||||
'dogecloud',
|
||||
'github',
|
||||
'huaweicloud-uploader',
|
||||
'imgur',
|
||||
'local',
|
||||
'lskyplist',
|
||||
'piclist',
|
||||
'qiniu',
|
||||
'sftpplist',
|
||||
'smms',
|
||||
'tcyun',
|
||||
'upyun',
|
||||
'webdavplist'
|
||||
]
|
||||
|
||||
@@ -37,16 +37,16 @@ const getSyncConfig = () => {
|
||||
const getProxyagent = (proxy: string | undefined) => {
|
||||
return proxy
|
||||
? new HttpsProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
rejectUnauthorized: false,
|
||||
proxy: proxy.replace('127.0.0.1', 'localhost'),
|
||||
scheduling: 'lifo'
|
||||
})
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
rejectUnauthorized: false,
|
||||
proxy: proxy.replace('127.0.0.1', 'localhost'),
|
||||
scheduling: 'lifo'
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
|
||||
function getOctokit (syncConfig: ISyncConfig) {
|
||||
function getOctokit(syncConfig: ISyncConfig) {
|
||||
const { token, proxy } = syncConfig
|
||||
return new Octokit({
|
||||
auth: token,
|
||||
@@ -83,7 +83,7 @@ const isSyncConfigValidate = ({
|
||||
return type && username && repo && branch && token
|
||||
}
|
||||
|
||||
async function uploadLocalToRemote (syncConfig: ISyncConfig, fileName: string) {
|
||||
async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
||||
const localFilePath = path.join(STORE_PATH, fileName)
|
||||
if (!fs.existsSync(localFilePath)) {
|
||||
return false
|
||||
@@ -161,7 +161,7 @@ async function uploadLocalToRemote (syncConfig: ISyncConfig, fileName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLocalToRemote (syncConfig: ISyncConfig, fileName: string) {
|
||||
async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
||||
const localFilePath = path.join(STORE_PATH, fileName)
|
||||
if (!fs.existsSync(localFilePath)) {
|
||||
return false
|
||||
@@ -277,7 +277,7 @@ async function updateLocalToRemote (syncConfig: ISyncConfig, fileName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile (fileName: string[]): Promise<number> {
|
||||
async function uploadFile(fileName: string[]): Promise<number> {
|
||||
const syncConfig = getSyncConfig()
|
||||
if (!isSyncConfigValidate(syncConfig)) {
|
||||
logger.error('sync config is invalid')
|
||||
@@ -302,7 +302,7 @@ async function uploadFile (fileName: string[]): Promise<number> {
|
||||
return count
|
||||
}
|
||||
|
||||
async function downloadAndWriteFile (url: string, localFilePath: string, config: any, isWriteJson = false) {
|
||||
async function downloadAndWriteFile(url: string, localFilePath: string, config: any, isWriteJson = false) {
|
||||
const res = await axios.get(url, config)
|
||||
if (isHttpResSuccess(res)) {
|
||||
await fs.writeFile(
|
||||
@@ -314,7 +314,7 @@ async function downloadAndWriteFile (url: string, localFilePath: string, config:
|
||||
return false
|
||||
}
|
||||
|
||||
async function downloadRemoteToLocal (syncConfig: ISyncConfig, fileName: string) {
|
||||
async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string) {
|
||||
const localFilePath = path.join(STORE_PATH, fileName)
|
||||
const { username, repo, branch, token, proxy, type } = syncConfig
|
||||
try {
|
||||
@@ -394,7 +394,7 @@ async function downloadRemoteToLocal (syncConfig: ISyncConfig, fileName: string)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile (fileName: string[]): Promise<number> {
|
||||
async function downloadFile(fileName: string[]): Promise<number> {
|
||||
const syncConfig = getSyncConfig()
|
||||
if (!isSyncConfigValidate(syncConfig)) {
|
||||
logger.error('sync config is invalid')
|
||||
|
||||
@@ -1,66 +1,72 @@
|
||||
import db from '@core/datastore'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { screen } from 'electron'
|
||||
|
||||
import { configPaths } from '~/utils/configPaths'
|
||||
import { IWindowList } from '~/utils/enum'
|
||||
|
||||
export function openMiniWindow (hideSettingWindow: boolean = true) {
|
||||
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!
|
||||
|
||||
miniWindow.removeAllListeners('close')
|
||||
miniWindow.removeAllListeners('move')
|
||||
|
||||
if (db.get(configPaths.settings.miniWindowOntop)) {
|
||||
miniWindow.setAlwaysOnTop(true)
|
||||
}
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
const lastPosition = db.get(configPaths.settings.miniWindowPosition)
|
||||
const setPositionFunc = () => {
|
||||
const position = miniWindow.getPosition()
|
||||
db.set(configPaths.settings.miniWindowPosition, position)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
miniWindow.on('close', setPositionFunc)
|
||||
miniWindow.on('move', setPositionFunc)
|
||||
miniWindow.show()
|
||||
miniWindow.focus()
|
||||
if (hideSettingWindow) {
|
||||
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
settingWindow.hide()
|
||||
} else {
|
||||
const autoCloseMainWindow = db.get(configPaths.settings.autoCloseMainWindow) || false
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW) && autoCloseMainWindow) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)!.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const openMainWindow = () => {
|
||||
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)
|
||||
const autoCloseMiniWindow = db.get(configPaths.settings.autoCloseMiniWindow) || false
|
||||
settingWindow!.show()
|
||||
settingWindow!.focus()
|
||||
if (windowManager.has(IWindowList.MINI_WINDOW) && autoCloseMiniWindow) {
|
||||
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
|
||||
}
|
||||
}
|
||||
|
||||
export const hideMiniWindow = () => {
|
||||
if (windowManager.has(IWindowList.MINI_WINDOW)) {
|
||||
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
|
||||
}
|
||||
}
|
||||
import db from '@core/datastore'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { screen } from 'electron'
|
||||
|
||||
import { configPaths } from '~/utils/configPaths'
|
||||
import { IWindowList } from '~/utils/enum'
|
||||
|
||||
export function openMiniWindow(hideSettingWindow: boolean = true) {
|
||||
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!
|
||||
|
||||
miniWindow.removeAllListeners('close')
|
||||
miniWindow.removeAllListeners('move')
|
||||
|
||||
if (db.get(configPaths.settings.miniWindowOntop)) {
|
||||
miniWindow.setAlwaysOnTop(true)
|
||||
}
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
const lastPosition = db.get(configPaths.settings.miniWindowPosition)
|
||||
const setPositionFunc = () => {
|
||||
const position = miniWindow.getPosition()
|
||||
db.set(configPaths.settings.miniWindowPosition, position)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
miniWindow.on('close', setPositionFunc)
|
||||
miniWindow.on('move', setPositionFunc)
|
||||
miniWindow.show()
|
||||
miniWindow.focus()
|
||||
if (hideSettingWindow) {
|
||||
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
settingWindow.hide()
|
||||
} else {
|
||||
const autoCloseMainWindow = db.get(configPaths.settings.autoCloseMainWindow) || false
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW) && autoCloseMainWindow) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)!.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const openMainWindow = () => {
|
||||
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)
|
||||
const autoCloseMiniWindow = db.get(configPaths.settings.autoCloseMiniWindow) || false
|
||||
settingWindow!.show()
|
||||
settingWindow!.focus()
|
||||
if (windowManager.has(IWindowList.MINI_WINDOW) && autoCloseMiniWindow) {
|
||||
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
|
||||
}
|
||||
}
|
||||
|
||||
export const hideMiniWindow = () => {
|
||||
if (windowManager.has(IWindowList.MINI_WINDOW)) {
|
||||
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user