picgo-plugin-alist
Version:
使用alist作为中转并实现对各个网盘的图床应用
364 lines (357 loc) • 12.6 kB
JavaScript
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