UNPKG

picgo-plugin-alist

Version:

使用alist作为中转并实现对各个网盘的图床应用

364 lines (357 loc) 12.6 kB
import FormData from 'form-data'; import 'js-sha256'; const uploaderName = "alist"; const bedName = `picBed.${uploaderName}`; function getConfig(ctx) { let userConfig = ctx.getConfig(bedName); if (!userConfig) { userConfig = {}; } const config = [ { name: "version", type: "input", default: userConfig.version ?? "", message: "\u4F60\u7684alist\u7248\u672C\uFF0C2\u62163", required: true, alias: "alist\u7248\u672C" }, { name: "url", type: "input", default: userConfig.url ?? "", message: "\u4F60\u7684alist\u5730\u5740\uFF0C\u5982https://alist.example.com\u3002", required: true, alias: "alist\u5730\u5740" }, { name: "uploadPath", type: "input", default: userConfig.uploadPath ?? "", message: "\u4E0A\u4F20\u7684\u76F8\u5BF9\u8DEF\u5F84\uFF0C\u5982assets\u3002", required: true, alias: "\u4E0A\u4F20\u8DEF\u5F84" }, { name: "token", type: "password", default: userConfig.token ?? "", message: "\u586B\u5199\u7528\u6237token\uFF0C\u83B7\u53D6\u8BF7\u53C2\u8003alist\u6587\u6863\u3002", required: false, alias: "\u7528\u6237token" }, { name: "username", type: "input", default: userConfig.username ?? "", message: "\u586B\u5199\u7528\u6237\u540D\uFF0C\u4E0E\u7528\u6237token\u4E8C\u9009\u4E00\u3002", required: false, alias: "\u7528\u6237\u540D" }, { name: "password", type: "password", default: userConfig.password ?? "", message: "\u586B\u5199\u5BC6\u7801\uFF0C\u4E0E\u7528\u6237token\u4E8C\u9009\u4E00\u3002", required: false, alias: "\u5BC6\u7801" }, { name: "accessPath", type: "input", default: userConfig.accessPath ?? "", message: "\u82E5\u7559\u7A7A\uFF0C\u5219\u8BBF\u95EE\u8DEF\u5F84\u4E0E\u4E0A\u4F20\u8DEF\u5F84\u4E00\u81F4\u3002", required: false, alias: "\u8BBF\u95EE\u8DEF\u5F84" }, { name: "accessDomain", type: "input", default: userConfig.accessDomain ?? "", message: "\u81EA\u5B9A\u4E49\u8BBF\u95EE\u57DF\u540D\uFF0C\u82E5\u7559\u7A7A\uFF0C\u5219\u4E0Ealist\u5730\u5740\u4E00\u81F4\u3002", required: false, alias: "\u8BBF\u95EE\u57DF\u540D" }, { name: "accessFileNameTemplate", type: "input", default: userConfig.accessFileNameTemplate ?? "", message: `\u7528\u4E8Ealist\u4F1A\u91CD\u6620\u5C04\u6587\u4EF6\u540D\u7684\u60C5\u51B5\uFF0C\u8BE6\u89C1github\u9875\u9762\u3002\u4F8B\uFF1Aprefix_\${fileName}_suffix`, required: false, alias: "\u8BBF\u95EE\u6587\u4EF6\u540D\u6A21\u677F" } ]; return config; } const defaultOptions = { save: false }; function setConfig(ctx, configs, options) { const originalConfig = ctx.getConfig(bedName); const { save, action, internal } = { ...defaultOptions, ...options }; let newConfig; if (action === "set") { newConfig = { ...configs }; } else if (action === "add") { if (internal) { newConfig = { ...originalConfig, ...Object.fromEntries( Object.entries(configs).map(([key, value]) => [`sys_${key}`, value]) ) }; } else { newConfig = { ...originalConfig, ...configs }; } } if (save) { ctx.saveConfig({ [bedName]: newConfig }); } else { ctx.setConfig({ [bedName]: newConfig }); } } function setToken(ctx, token, refreshedAt) { setConfig(ctx, { token, tokenRefreshedAt: String(refreshedAt) }, { save: true, action: "add", internal: true }); } function getToken(ctx) { const token = ctx.getConfig(`${bedName}.sys_token`); const refreshedAt = ctx.getConfig(`${bedName}.sys_tokenRefreshedAt`); return { token, refreshedAt: Number(refreshedAt) }; } function getRefreshOptions(options) { const { url, token, uploadPath, version } = options; if (version === 2) { const v2options = { method: "POST", url: `${url}/api/admin/refresh`, resolveWithFullResponse: true, headers: { "User-Agent": "PicGo", "Authorization": token }, data: { path: `/${uploadPath}` } }; return v2options; } else if (version === 3) { const v3options = { method: "POST", url: `${url}/api/fs/list`, resolveWithFullResponse: true, headers: { "User-Agent": "PicGo", "Authorization": token }, data: { page: 1, password: "", path: `/${uploadPath}`, per_page: 0, refresh: true } }; return v3options; } } function getPostOptions(options) { const { url, files, token, uploadPath, version, fileName } = options; if (version === 2) { const formData = new FormData(); formData.append("files", files, { filename: fileName }); formData.append("path", uploadPath); const v2options = { method: "POST", url: `${url}/api/public/upload`, resolveWithFullResponse: true, headers: { "User-Agent": "PicGo", "Authorization": token }, data: formData }; return v2options; } else if (version === 3) { const formData = new FormData(); formData.append("file", files, { filename: fileName }); const v3options = { method: "PUT", url: `${url}/api/fs/form`, resolveWithFullResponse: true, headers: { "User-Agent": "PicGo", "Authorization": token, "file-path": encodeURIComponent(`/${uploadPath}/${fileName}`) }, data: formData }; return v3options; } } function rmEndSlashes(str) { return str?.replace(/\/*\\*$/g, ""); } function rmBeginSlashes(str) { return str?.replace(/^\/*\\*/g, ""); } function rmBothEndSlashes(str) { return rmBeginSlashes(rmEndSlashes(str)); } const UPLOAD_AUTH_RETRY_LIMIT_TIMES = 1; function parseAccessFileNameTemplate(template, vars) { const fileNameParts = vars.fileName.split("."); const extension = fileNameParts.length > 1 ? `.${fileNameParts.pop()}` : ""; const nameWithoutExt = fileNameParts.join("."); let result = template.replace(/\$\{fileName\}/g, nameWithoutExt); if (extension) { result += extension; } return result; } function handleFileName(fileName) { const fileNameParts = fileName.split("/"); return { fileName: fileNameParts[fileNameParts.length - 1], prefixPath: fileNameParts.slice(0, -1).join("/") }; } async function handleSingleUpload(ctx, image, options) { const { url, token: originalToken, uploadPath: originalUploadPath, accessPath: originalAccessPath, version, accessDomain, accessFileNameTemplate } = options; const handledFileName = handleFileName(image.fileName); const fileName = handledFileName.fileName; const uploadPath = handledFileName.prefixPath ? `${originalUploadPath}/${handledFileName.prefixPath}` : originalUploadPath; const accessPath = handledFileName.prefixPath ? `${originalAccessPath}/${handledFileName.prefixPath}` : originalAccessPath; const accessFileName = accessFileNameTemplate ? parseAccessFileNameTemplate(accessFileNameTemplate, { fileName }) : fileName; ctx.log.info(`[\u4FE1\u606F] version:${version}, uploadPath:${uploadPath}, fileName:${fileName}`); ctx.log.info(`[\u5F00\u59CB\u4E0A\u4F20] ${image.fileName}`); let retryTimes = 0; let token = originalToken; while (retryTimes <= UPLOAD_AUTH_RETRY_LIMIT_TIMES) { const postOptions = getPostOptions({ url, token, uploadPath, files: image.buffer, version, fileName }); ctx.log.info(`[\u5F00\u59CB\u4E0A\u4F20] ${image.fileName} \u7B2C${retryTimes + 1}\u6B21\u5C1D\u8BD5`); const uploadRes = await ctx.request(postOptions); const authFailed = uploadRes.status === 401 || uploadRes.status === 200 && uploadRes.data.code === 401; if (authFailed && options.authMode === "username-password" && retryTimes < UPLOAD_AUTH_RETRY_LIMIT_TIMES) { ctx.log.warn(`[\u8BA4\u8BC1\u5931\u8D25] \u6B63\u5728\u5C1D\u8BD5\u91CD\u65B0\u83B7\u53D6token (${retryTimes + 1}/${UPLOAD_AUTH_RETRY_LIMIT_TIMES})`); token = await getTokenByAuth(ctx, url, options.username, options.password, { forceRefresh: true }); retryTimes++; continue; } if (uploadRes.status !== 200) throw new Error(`[\u4E0A\u4F20\u5931\u8D25] \u6587\u4EF6: ${fileName} \u7ED3\u679C: ${uploadRes.statusCode} ${uploadRes.statusText}`); if (!uploadRes.data || uploadRes.data.code !== 200) throw new Error(`[\u4E0A\u4F20\u5931\u8D25] \u6587\u4EF6: ${fileName} \u7ED3\u679C: ${JSON.stringify(uploadRes.data)}`); ctx.log.info(`[\u4E0A\u4F20\u8BF7\u6C42\u7ED3\u679C] ${JSON.stringify(uploadRes.data)}`); const refreshOptions = getRefreshOptions({ url, uploadPath, version, token }); const refreshRes = await ctx.request(refreshOptions); if (refreshRes.status !== 200) throw new Error(`[\u5237\u65B0\u5931\u8D25] ${refreshRes.statusCode} ${refreshRes.statusText}`); if (!refreshRes.data || refreshRes.data.code !== 200) throw new Error(`[\u5237\u65B0\u5931\u8D25] ${JSON.stringify(refreshRes.data)}`); ctx.log.info(`[\u5237\u65B0\u8BF7\u6C42\u7ED3\u679C] ${JSON.stringify({ code: refreshRes.data.code, message: refreshRes.data.message })}`); const targetImgUrl = `${accessDomain}/${accessPath}/${accessFileName}`; image.imgUrl = targetImgUrl; ctx.log.info(`[\u4E0A\u4F20\u6210\u529F] ${image.fileName} -> ${targetImgUrl}`); delete image.base64Image; delete image.buffer; break; } } async function getTokenByAuth(ctx, url, username, password, options) { const storedToken = getToken(ctx); if (!options?.forceRefresh && storedToken?.token && storedToken?.refreshedAt && storedToken?.refreshedAt > Date.now() - 1e3 * 60 * 60 * 24 * 1) { ctx.log.info("[\u4FE1\u606F] \u4ECE\u7F13\u5B58\u4E2D\u83B7\u53D6token"); return storedToken.token; } const refreshedAt = Date.now(); ctx.log.info("[\u4FE1\u606F] \u5C1D\u8BD5\u4F7F\u7528\u7528\u6237\u540D\u548C\u5BC6\u7801\u8BF7\u6C42API\u83B7\u53D6token"); const res = await ctx.request({ method: "POST", url: `${url}/api/auth/login`, resolveWithFullResponse: true, data: { username, password } }); if (res.status !== 200 || !res.data || res.data.code !== 200) { throw new Error(`[\u83B7\u53D6token\u5931\u8D25] \u8BF7\u68C0\u67E5\u7528\u6237\u540D\u548C\u5BC6\u7801\u662F\u5426\u6B63\u786E\u3002 \u8BF7\u6C42\u7ED3\u679C\uFF1A ${res.statusCode} ${res.statusText} ${JSON.stringify(res.data ?? {})}`); } const token = res.data.data.token; setToken(ctx, token, refreshedAt); return token; } async function handle(ctx) { const userConfig = ctx.getConfig(bedName); const url = rmEndSlashes(userConfig.url); const username = userConfig.username; const password = userConfig.password; if (!userConfig) throw new Error("\u627E\u4E0D\u5230\u4E0A\u4F20\u5668\u914D\u7F6E"); if (!userConfig.token && (!username || !password)) { throw new Error("\u8BF7\u586B\u5199\u7528\u6237\u540D\u548C\u5BC6\u7801\u6216\u8005token"); } let token; const authMode = username && password ? "username-password" : "token"; if (authMode === "username-password") { ctx.log.info("[\u4FE1\u606F] \u7528\u6237\u540D\u4E0E\u5BC6\u7801\u6A21\u5F0F"); token = await getTokenByAuth(ctx, url, username, password); } else { token = userConfig.token; } const options = { url, token, uploadPath: rmBothEndSlashes(userConfig.uploadPath), accessPath: userConfig.accessPath ? rmBothEndSlashes(userConfig.accessPath) : `d/${rmBothEndSlashes(userConfig.uploadPath)}`, version: Number(userConfig.version), accessDomain: userConfig.accessDomain ? rmBothEndSlashes(userConfig.accessDomain) : rmBothEndSlashes(userConfig.url), accessFileNameTemplate: userConfig.accessFileNameTemplate, authMode, username, password }; const uploads = ctx.output.map(async (image) => { try { await handleSingleUpload(ctx, image, options); } catch (error) { ctx.log.error(error); ctx.emit("notification", { title: "\u4E0A\u4F20\u5931\u8D25", body: error.message }); } }); await Promise.all(uploads); return ctx; } const index = (ctx) => { const register = () => { ctx.helper.uploader.register(uploaderName, { handle, name: "alist", config: getConfig }); }; return { uploader: uploaderName, register }; }; export { index as default }; //# sourceMappingURL=index.mjs.map