mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-17 09:47:36 +08:00
🐛 Fix(custom): fix drag upload of upload page
This commit is contained in:
@@ -1,335 +1,324 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import db from '@core/datastore'
|
||||
import logger from '@core/picgo/logger'
|
||||
import axios from 'axios'
|
||||
import { clipboard, dialog, 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, IShowMessageBoxResult, 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
|
||||
// icon: options.icon || undefined
|
||||
})
|
||||
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()
|
||||
}
|
||||
|
||||
export const showMessageBox = (options: any) => {
|
||||
return new Promise<IShowMessageBoxResult>(resolve => {
|
||||
dialog.showMessageBox(options).then(res => {
|
||||
resolve({
|
||||
result: res.response,
|
||||
checkboxChecked: res.checkboxChecked
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 generateC1NShortUrl = 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 generateYOURLSShortUrl = 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 generateCFWORKERShortUrl = 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 generateSinkShortUrl = 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 generateC1NShortUrl(url)
|
||||
case IShortUrlServer.YOURLS:
|
||||
return generateYOURLSShortUrl(url)
|
||||
case IShortUrlServer.CFWORKER:
|
||||
return generateCFWORKERShortUrl(url)
|
||||
case IShortUrlServer.SINK:
|
||||
return generateSinkShortUrl(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
|
||||
// icon: options.icon || undefined
|
||||
})
|
||||
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,262 +1,263 @@
|
||||
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 { DeleteObjectCommand, S3Client, S3ClientConfig } from '@aws-sdk/client-s3'
|
||||
import { NodeHttpHandler } from '@smithy/node-http-handler'
|
||||
import axios from 'axios'
|
||||
import { 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) {
|
||||
console.log(err)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) {
|
||||
try {
|
||||
const {
|
||||
url: rawUrl,
|
||||
type,
|
||||
config: { accessKeyID, secretAccessKey, bucketName, endpoint, pathStyleAccess, rejectUnauthorized, proxy }
|
||||
} = configMap
|
||||
let {
|
||||
imgUrl,
|
||||
config: { region }
|
||||
} = configMap
|
||||
if (type === 'aws-s3' || type === 'aws-s3-plist') {
|
||||
imgUrl = rawUrl || imgUrl || ''
|
||||
}
|
||||
const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl)
|
||||
let fileKey = url.pathname.replace(/^\/+/, '')
|
||||
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) {
|
||||
console.log(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) {
|
||||
console.log(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) {
|
||||
console.log(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) {
|
||||
console.log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
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 { 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) {
|
||||
console.log(err)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) {
|
||||
try {
|
||||
const {
|
||||
url: rawUrl,
|
||||
type,
|
||||
config: { accessKeyID, secretAccessKey, bucketName, endpoint, pathStyleAccess, rejectUnauthorized, proxy }
|
||||
} = configMap
|
||||
let {
|
||||
imgUrl,
|
||||
config: { region }
|
||||
} = configMap
|
||||
if (type === 'aws-s3' || type === 'aws-s3-plist') {
|
||||
imgUrl = rawUrl || imgUrl || ''
|
||||
}
|
||||
const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl)
|
||||
let fileKey = url.pathname.replace(/^\/+/, '')
|
||||
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) {
|
||||
console.log(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) {
|
||||
console.log(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) {
|
||||
console.log(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) {
|
||||
console.log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user