From c7f56593226871cf6a5836396577670978fb3c65 Mon Sep 17 00:00:00 2001 From: Kuingsmile Date: Tue, 2 Jul 2024 17:57:37 +0800 Subject: [PATCH] :sparkles: Feature(custom): add buildin alist --- src/i18n/en.ts | 17 ++ src/i18n/zh-CN.ts | 17 ++ src/i18n/zh-TW.ts | 17 ++ src/plugins/uploader/alist.ts | 285 ++++++++++++++++++++++++++++++++++ src/plugins/uploader/index.ts | 2 + src/types/index.ts | 10 ++ 6 files changed, 348 insertions(+) create mode 100644 src/plugins/uploader/alist.ts diff --git a/src/i18n/en.ts b/src/i18n/en.ts index f03e8c3..a7afd6b 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -24,6 +24,23 @@ export const EN: ILocales = { PICBED_LOCAL_WEBPATH: 'Set Web Path', PICBED_LOCAL_MESSAGE_WEBPATH: 'Used to generate url path', + // alist + PICBED_ALIST_PLIST: 'AList', + PICBED_ALIST_URL: 'Set URL', + PICBED_ALIST_MESSAGE_URL: 'Ex. https://alist.example.com', + PICBED_ALIST_TOKEN: 'Set Token', + PICBED_ALIST_MESSAGE_TOKEN: 'Please enter the token(choose one of either username-password or token)', + PICBED_ALIST_USERNAME: 'Set Username', + PICBED_ALIST_MESSAGE_USERNAME: 'Please enter the username(choose one of either username-password or token)', + PICBED_ALIST_PASSWORD: 'Set Password', + PICBED_ALIST_MESSAGE_PASSWORD: 'Please enter the password', + PICBED_ALIST_UPLOAD_PATH: 'Set Upload Path', + PICBED_ALIST_MESSAGE_UPLOAD_PATH: 'Ex. /test/', + PICBED_ALIST_WEB_PATH: 'Set Web Path', + PICBED_ALIST_MESSAGE_WEB_PATH: 'Used to generate url path', + PICBED_ALIST_CUSTOMURL: 'Set Custom URL', + PICBED_ALIST_MESSAGE_CUSTOMURL: 'Ex. https://test.com', + // Ali-cloud PICBED_ALICLOUD: 'Ali Cloud', PICBED_ALICLOUD_ACCESSKEYID: 'Set accessKeyId', diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index dce760f..769351d 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -22,6 +22,23 @@ export const ZH_CN = { PICBED_LOCAL_WEBPATH: '设定网站路径', PICBED_LOCAL_MESSAGE_WEBPATH: '用于拼接网址路径', + // alist + PICBED_ALIST_PLIST: 'AList', + PICBED_ALIST_URL: '设定URL', + PICBED_ALIST_MESSAGE_URL: '例如:https://alist.example.com', + PICBED_ALIST_TOKEN: '设定Token', + PICBED_ALIST_MESSAGE_TOKEN: '请输入Token(与用户名-密码二选一)', + PICBED_ALIST_USERNAME: '设定用户名', + PICBED_ALIST_MESSAGE_USERNAME: '请输入用户名(与Token二选一)', + PICBED_ALIST_PASSWORD: '设定密码', + PICBED_ALIST_MESSAGE_PASSWORD: '请输入密码', + PICBED_ALIST_UPLOAD_PATH: '设定上传路径', + PICBED_ALIST_MESSAGE_UPLOAD_PATH: '例如:/test', + PICBED_ALIST_WEB_PATH: '设定网站路径', + PICBED_ALIST_MESSAGE_WEB_PATH: '用于拼接网址路径', + PICBED_ALIST_CUSTOMURL: '设定自定义域名', + PICBED_ALIST_MESSAGE_CUSTOMURL: '例如:https://test.com', + // Ali-cloud PICBED_ALICLOUD: '阿里云OSS', PICBED_ALICLOUD_ACCESSKEYID: '设定accessKeyId', diff --git a/src/i18n/zh-TW.ts b/src/i18n/zh-TW.ts index 2200cd8..dd4335b 100644 --- a/src/i18n/zh-TW.ts +++ b/src/i18n/zh-TW.ts @@ -24,6 +24,23 @@ export const ZH_TW: ILocales = { PICBED_LOCAL_WEBPATH: '設定網址路徑', PICBED_LOCAL_MESSAGE_WEBPATH: '用于網頁顯示的路徑', + // alist + PICBED_ALIST_PLIST: 'AList', + PICBED_ALIST_URL: '設定URL', + PICBED_ALIST_MESSAGE_URL: '例如:https://alist.example.com', + PICBED_ALIST_TOKEN: '設定Token', + PICBED_ALIST_MESSAGE_TOKEN: '請輸入Token(與用戶名-密碼二選一)', + PICBED_ALIST_USERNAME: '設定用戶名', + PICBED_ALIST_MESSAGE_USERNAME: '請輸入用戶名(與Token二選一)', + PICBED_ALIST_PASSWORD: '設定密碼', + PICBED_ALIST_MESSAGE_PASSWORD: '請輸入密碼', + PICBED_ALIST_UPLOAD_PATH: '設定上傳路徑', + PICBED_ALIST_MESSAGE_UPLOAD_PATH: '例如:/test', + PICBED_ALIST_WEB_PATH: '設定網址路徑', + PICBED_ALIST_MESSAGE_WEB_PATH: '用于網頁顯示的路徑', + PICBED_ALIST_CUSTOMURL: '設定自訂網址', + PICBED_ALIST_MESSAGE_CUSTOMURL: '例如:https://test.com', + // Ali-cloud PICBED_ALICLOUD: '阿里云OSS', PICBED_ALICLOUD_ACCESSKEYID: '設定accessKeyId', diff --git a/src/plugins/uploader/alist.ts b/src/plugins/uploader/alist.ts new file mode 100644 index 0000000..d60a03f --- /dev/null +++ b/src/plugins/uploader/alist.ts @@ -0,0 +1,285 @@ +import axios from 'axios' +import { IPicGo, IPluginConfig, IAlistConfig, IOldReqOptions, IFullResponse } from '../../types' +import { IBuildInEvent } from '../../utils/enum' +import { ILocalesKey } from '../../i18n/zh-CN' +import { encodePath, formatPathHelper } from './utils' +import path from 'path' + +interface IAlistTokenStore { + token: string + refreshedAt: number +} + +const getAlistToken = async (ctx: IPicGo, url: string, username: string, password: string): Promise => { + const tokenStore = ctx.getConfig('picgo-plugin-buildin-alistplist') + if (tokenStore && tokenStore.refreshedAt && Date.now() - tokenStore.refreshedAt < 3600000 && tokenStore.token) { + return tokenStore.token + } + const res = await axios.post(`${url}/api/auth/login`, { + username, + password + }) + if (res.data.code === 200 && res.data.message === 'success') { + const token = res.data.data.token + ctx.saveConfig({ + 'picgo-plugin-buildin-alistplist': { + token, + refreshedAt: Date.now() + } + }) + return token + } + throw new Error('Get token failed') +} + +const postOptions = (url: string, token: string, fileName: string, filePath: string, image: Buffer): IOldReqOptions => { + return { + method: 'PUT', + url: `${url}/api/fs/form`, + headers: { + contentType: 'multipart/form-data', + 'User-Agent': 'PicList', + Authorization: token, + 'File-Path': encodeURIComponent(filePath) + }, + formData: { + file: { + value: image, + options: { + filename: fileName + } + } + }, + resolveWithFullResponse: true + } +} + +const handleResError = (ctx: IPicGo, res: IFullResponse): void => { + if (res.statusCode !== 200 || res.body.code !== 200 || res.body.message !== 'success') { + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate('UPLOAD_FAILED'), + body: ctx.i18n.translate('CHECK_SETTINGS_AND_NETWORK'), + text: res.body.message + }) + throw new Error(res.body.message) + } +} + +const extractConfig = (config: IAlistConfig) => { + const { url, token, username, password, uploadPath, webPath, customUrl } = config + return { + url: (url || '').replace(/\/$/, ''), + token: token || '', + username: username || '', + password: password || '', + uploadPath: formatPathHelper({ + path: uploadPath || '', + startSlash: true, + endSlash: true, + rootToEmpty: false + }), + webPath: webPath + ? formatPathHelper({ + path: webPath || '', + startSlash: true, + endSlash: true, + rootToEmpty: false + }) + : '', + customUrl: (customUrl || '').replace(/\/$/, '') + } +} + +const handle = async (ctx: IPicGo): Promise => { + const alistConfig = ctx.getConfig('picBed.alistplist') + if (!alistConfig) throw new Error('Can not find alist config!') + const { url, username, password, uploadPath, webPath, customUrl } = extractConfig(alistConfig) + let { token } = extractConfig(alistConfig) + if (!token) { + token = await getAlistToken(ctx, url, username, password) + } + if (!url || !(token || (username && password))) throw new Error('Please check your alist config!') + const imgList = ctx.output + for (const img of imgList) { + if (img.fileName && img.buffer) { + let image = img.buffer + if (!image && img.base64Image) { + image = Buffer.from(img.base64Image, 'base64') + } + const fullUploadPath = `${uploadPath}${img.fileName}` + const postConfig = postOptions(url, token, img.fileName, fullUploadPath, image) + try { + const uploadRes = (await ctx.request(postConfig)) as unknown as IFullResponse + handleResError(ctx, uploadRes) + const refreshUrl = `${url}/api/fs/list` + const getInfoUrl = `${url}/api/fs/get` + const refreshRes = (await ctx.request({ + method: 'POST', + url: refreshUrl, + headers: { + Authorization: token, + 'Content-Type': 'application/json' + }, + body: { + password: '', + page: 1, + per_page: 1, + refresh: true, + path: path.dirname(fullUploadPath) + }, + resolveWithFullResponse: true + })) as unknown as IFullResponse + handleResError(ctx, refreshRes) + const getInfoRes = (await ctx.request({ + method: 'POST', + url: getInfoUrl, + headers: { + Authorization: token, + 'Content-Type': 'application/json' + }, + body: { + password: '', + path: fullUploadPath, + page: 1, + per_page: 1, + refresh: true + }, + resolveWithFullResponse: true + })) as unknown as IFullResponse + handleResError(ctx, getInfoRes) + const sign = getInfoRes.body.data.sign + const encodedPath = encodePath(`${webPath || uploadPath}${img.fileName}`) + img.imgUrl = `${customUrl || url}/d${encodedPath}` + img.imgUrl += sign ? `?sign=${sign}` : '' + delete img.base64Image + delete img.buffer + } catch (e: any) { + ctx.log.error(e) + throw e + } + } + } + return ctx +} + +const config = (ctx: IPicGo): IPluginConfig[] => { + const userConfig = ctx.getConfig('picBed.alistplist') || {} + const config: IPluginConfig[] = [ + { + name: 'url', + type: 'input', + get prefix() { + return ctx.i18n.translate('PICBED_ALIST_URL') + }, + get alias() { + return ctx.i18n.translate('PICBED_ALIST_URL') + }, + get message() { + return ctx.i18n.translate('PICBED_ALIST_MESSAGE_URL') + }, + default: userConfig.url || '', + required: true + }, + { + name: 'token', + type: 'input', + get prefix() { + return ctx.i18n.translate('PICBED_ALIST_TOKEN') + }, + get alias() { + return ctx.i18n.translate('PICBED_ALIST_TOKEN') + }, + get message() { + return ctx.i18n.translate('PICBED_ALIST_MESSAGE_TOKEN') + }, + default: userConfig.token || '', + required: false + }, + { + name: 'username', + type: 'input', + get prefix() { + return ctx.i18n.translate('PICBED_ALIST_USERNAME') + }, + get alias() { + return ctx.i18n.translate('PICBED_ALIST_USERNAME') + }, + get message() { + return ctx.i18n.translate('PICBED_ALIST_MESSAGE_USERNAME') + }, + default: userConfig.username || '', + required: false + }, + { + name: 'password', + type: 'input', + get prefix() { + return ctx.i18n.translate('PICBED_ALIST_PASSWORD') + }, + get alias() { + return ctx.i18n.translate('PICBED_ALIST_PASSWORD') + }, + get message() { + return ctx.i18n.translate('PICBED_ALIST_MESSAGE_PASSWORD') + }, + default: userConfig.password || '', + required: false + }, + { + name: 'uploadPath', + type: 'input', + get prefix() { + return ctx.i18n.translate('PICBED_ALIST_UPLOAD_PATH') + }, + get alias() { + return ctx.i18n.translate('PICBED_ALIST_UPLOAD_PATH') + }, + get message() { + return ctx.i18n.translate('PICBED_ALIST_MESSAGE_UPLOAD_PATH') + }, + default: userConfig.uploadPath || '', + required: false + }, + { + name: 'webPath', + type: 'input', + get prefix() { + return ctx.i18n.translate('PICBED_ALIST_WEB_PATH') + }, + get alias() { + return ctx.i18n.translate('PICBED_ALIST_WEB_PATH') + }, + get message() { + return ctx.i18n.translate('PICBED_ALIST_MESSAGE_WEB_PATH') + }, + default: userConfig.webPath || '', + required: false + }, + { + name: 'customUrl', + type: 'input', + get prefix() { + return ctx.i18n.translate('PICBED_ALIST_CUSTOMURL') + }, + get alias() { + return ctx.i18n.translate('PICBED_ALIST_CUSTOMURL') + }, + get message() { + return ctx.i18n.translate('PICBED_ALIST_MESSAGE_CUSTOMURL') + }, + default: userConfig.customUrl || '', + required: false + } + ] + return config +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register('alistplist', { + get name() { + return ctx.i18n.translate('PICBED_ALIST_PLIST') + }, + handle, + config + }) +} diff --git a/src/plugins/uploader/index.ts b/src/plugins/uploader/index.ts index 4e05b72..ee13c92 100644 --- a/src/plugins/uploader/index.ts +++ b/src/plugins/uploader/index.ts @@ -13,10 +13,12 @@ import telegraphUploader from './telegraph' import piclistUploader from './piclist' import lskyUploader from './lsky' import awss3plistUploader from './awss3plist' +import alistUploader from './alist' const buildInUploaders: IPicGoPlugin = () => { return { register(ctx: IPicGo) { + alistUploader(ctx) aliYunUploader(ctx) awss3plistUploader(ctx) githubUploader(ctx) diff --git a/src/types/index.ts b/src/types/index.ts index eae279f..15c116c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -307,6 +307,16 @@ export interface ISmmsConfig { token: string backupDomain?: string } +/** 内置alist 图床配置项 */ +export interface IAlistConfig { + url: string + token?: string + username?: string + password?: string + uploadPath?: string + webPath?: string + customUrl?: string +} /** 本地图床配置项 */ export interface ILocalConfig { path: string