mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-06-04 11:13:59 +08:00
321 lines
9.7 KiB
JavaScript
321 lines
9.7 KiB
JavaScript
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
|
||
import { FILES_KEY, ARTIFACTS_KEY } from '@/constants';
|
||
import { failed, success } from '@/restful/response';
|
||
import $ from '@/core/app';
|
||
import {
|
||
RequestInvalidError,
|
||
ResourceNotFoundError,
|
||
InternalServerError,
|
||
} from '@/restful/errors';
|
||
import { produceArtifact } from '@/restful/sync';
|
||
import { formatDateTime } from '@/utils';
|
||
|
||
export default function register($app) {
|
||
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||
|
||
$app.get('/share/file/:name', getFile);
|
||
|
||
$app.route('/api/file/:name')
|
||
.get(getFile)
|
||
.patch(updateFile)
|
||
.delete(deleteFile);
|
||
|
||
$app.route('/api/wholeFile/:name').get(getWholeFile);
|
||
|
||
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
||
$app.route('/api/wholeFiles').get(getAllWholeFiles);
|
||
}
|
||
|
||
// file API
|
||
function createFile(req, res) {
|
||
const file = req.body;
|
||
file.name = `${file.name ?? Date.now()}`;
|
||
$.info(`正在创建文件:${file.name}`);
|
||
const allFiles = $.read(FILES_KEY);
|
||
if (findByName(allFiles, file.name)) {
|
||
return failed(
|
||
res,
|
||
new RequestInvalidError(
|
||
'DUPLICATE_KEY',
|
||
req.body.name
|
||
? `已存在 name 为 ${file.name} 的文件`
|
||
: `无法同时创建相同的文件 可稍后重试`,
|
||
),
|
||
);
|
||
}
|
||
allFiles.push(file);
|
||
$.write(allFiles, FILES_KEY);
|
||
success(res, file, 201);
|
||
}
|
||
|
||
async function getFile(req, res) {
|
||
let { name } = req.params;
|
||
name = decodeURIComponent(name);
|
||
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
|
||
$.info(`正在下载文件:${name}\n请求 User-Agent: ${reqUA}`);
|
||
let {
|
||
url,
|
||
subInfoUrl,
|
||
subInfoUserAgent,
|
||
ua,
|
||
content,
|
||
mergeSources,
|
||
ignoreFailedRemoteFile,
|
||
proxy,
|
||
noCache,
|
||
produceType,
|
||
} = req.query;
|
||
let $options = {
|
||
_req: {
|
||
method: req.method,
|
||
url: req.url,
|
||
path: req.path,
|
||
query: req.query,
|
||
params: req.params,
|
||
headers: req.headers,
|
||
body: req.body,
|
||
},
|
||
};
|
||
if (req.query.$options) {
|
||
try {
|
||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||
$options = JSON.parse(decodeURIComponent(req.query.$options));
|
||
} catch (e) {
|
||
for (const pair of req.query.$options.split('&')) {
|
||
const key = pair.split('=')[0];
|
||
const value = pair.split('=')[1];
|
||
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||
$options[key] =
|
||
value == null || value === ''
|
||
? true
|
||
: decodeURIComponent(value);
|
||
}
|
||
}
|
||
$.info(`传入 $options: ${JSON.stringify($options)}`);
|
||
}
|
||
if (url) {
|
||
url = decodeURIComponent(url);
|
||
$.info(`指定远程文件 URL: ${url}`);
|
||
}
|
||
if (proxy) {
|
||
proxy = decodeURIComponent(proxy);
|
||
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
|
||
}
|
||
if (ua) {
|
||
ua = decodeURIComponent(ua);
|
||
$.info(`指定远程文件 User-Agent: ${ua}`);
|
||
}
|
||
if (subInfoUrl) {
|
||
subInfoUrl = decodeURIComponent(subInfoUrl);
|
||
$.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
|
||
}
|
||
if (subInfoUserAgent) {
|
||
subInfoUserAgent = decodeURIComponent(subInfoUserAgent);
|
||
$.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
|
||
}
|
||
if (content) {
|
||
content = decodeURIComponent(content);
|
||
$.info(`指定本地文件: ${content}`);
|
||
}
|
||
if (mergeSources) {
|
||
mergeSources = decodeURIComponent(mergeSources);
|
||
$.info(`指定合并来源: ${mergeSources}`);
|
||
}
|
||
if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
|
||
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
|
||
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
|
||
}
|
||
if (noCache) {
|
||
$.info(`指定不使用缓存: ${noCache}`);
|
||
}
|
||
if (produceType) {
|
||
produceType = decodeURIComponent(produceType);
|
||
$.info(`指定生产类型: ${produceType}`);
|
||
}
|
||
|
||
const allFiles = $.read(FILES_KEY);
|
||
const file = findByName(allFiles, name);
|
||
if (file) {
|
||
try {
|
||
const output = await produceArtifact({
|
||
type: 'file',
|
||
name,
|
||
url,
|
||
ua,
|
||
content,
|
||
mergeSources,
|
||
ignoreFailedRemoteFile,
|
||
$options,
|
||
proxy,
|
||
noCache,
|
||
produceType,
|
||
});
|
||
|
||
try {
|
||
subInfoUrl = subInfoUrl || file.subInfoUrl;
|
||
if (subInfoUrl) {
|
||
// forward flow headers
|
||
const flowInfo = await getFlowHeaders(
|
||
subInfoUrl,
|
||
subInfoUserAgent || file.subInfoUserAgent,
|
||
undefined,
|
||
proxy || file.proxy,
|
||
);
|
||
if (flowInfo) {
|
||
res.set(
|
||
'subscription-userinfo',
|
||
normalizeFlowHeader(flowInfo),
|
||
);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
$.error(
|
||
`文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(
|
||
err,
|
||
)}`,
|
||
);
|
||
}
|
||
if (file.download) {
|
||
res.set(
|
||
'Content-Disposition',
|
||
`attachment; filename*=UTF-8''${encodeURIComponent(
|
||
file.displayName || file.name,
|
||
)}`,
|
||
);
|
||
}
|
||
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||
output ?? '',
|
||
);
|
||
} catch (err) {
|
||
$.notify(
|
||
`🌍 Sub-Store 下载文件失败`,
|
||
`❌ 无法下载文件:${name}!`,
|
||
`🤔 原因:${err.message ?? err}`,
|
||
);
|
||
$.error(err.message ?? err);
|
||
failed(
|
||
res,
|
||
new InternalServerError(
|
||
'INTERNAL_SERVER_ERROR',
|
||
`Failed to download file: ${name}`,
|
||
`Reason: ${err.message ?? err}`,
|
||
),
|
||
);
|
||
}
|
||
} else {
|
||
$.error(`🌍 Sub-Store 下载文件失败\n❌ 未找到文件:${name}!`);
|
||
failed(
|
||
res,
|
||
new ResourceNotFoundError(
|
||
'RESOURCE_NOT_FOUND',
|
||
`File ${name} does not exist!`,
|
||
),
|
||
404,
|
||
);
|
||
}
|
||
}
|
||
function getWholeFile(req, res) {
|
||
let { name } = req.params;
|
||
let { raw } = req.query;
|
||
name = decodeURIComponent(name);
|
||
const allFiles = $.read(FILES_KEY);
|
||
const file = findByName(allFiles, name);
|
||
if (file) {
|
||
if (raw) {
|
||
res.set('content-type', 'application/json')
|
||
.set(
|
||
'content-disposition',
|
||
`attachment; filename="${encodeURIComponent(
|
||
`sub-store_file_${name}_${formatDateTime(
|
||
new Date(),
|
||
)}.json`,
|
||
)}"`,
|
||
)
|
||
.send(JSON.stringify(file));
|
||
} else {
|
||
success(res, file);
|
||
}
|
||
} else {
|
||
failed(
|
||
res,
|
||
new ResourceNotFoundError(
|
||
`FILE_NOT_FOUND`,
|
||
`File ${name} does not exist`,
|
||
404,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
function updateFile(req, res) {
|
||
let { name } = req.params;
|
||
name = decodeURIComponent(name);
|
||
let file = req.body;
|
||
const allFiles = $.read(FILES_KEY);
|
||
const oldFile = findByName(allFiles, name);
|
||
if (oldFile) {
|
||
const newFile = {
|
||
...oldFile,
|
||
...file,
|
||
};
|
||
$.info(`正在更新文件:${name}...`);
|
||
|
||
if (name !== newFile.name) {
|
||
// update all artifacts referring this collection
|
||
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
|
||
for (const artifact of allArtifacts) {
|
||
if (
|
||
artifact.type === 'file' &&
|
||
artifact.source === oldFile.name
|
||
) {
|
||
artifact.source = newFile.name;
|
||
}
|
||
}
|
||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||
}
|
||
|
||
updateByName(allFiles, name, newFile);
|
||
$.write(allFiles, FILES_KEY);
|
||
success(res, newFile);
|
||
} else {
|
||
failed(
|
||
res,
|
||
new ResourceNotFoundError(
|
||
'RESOURCE_NOT_FOUND',
|
||
`File ${name} does not exist!`,
|
||
),
|
||
404,
|
||
);
|
||
}
|
||
}
|
||
|
||
function deleteFile(req, res) {
|
||
let { name } = req.params;
|
||
name = decodeURIComponent(name);
|
||
$.info(`正在删除文件:${name}`);
|
||
let allFiles = $.read(FILES_KEY);
|
||
deleteByName(allFiles, name);
|
||
$.write(allFiles, FILES_KEY);
|
||
success(res);
|
||
}
|
||
|
||
function getAllFiles(req, res) {
|
||
const allFiles = $.read(FILES_KEY);
|
||
success(
|
||
res, // eslint-disable-next-line no-unused-vars
|
||
allFiles.map(({ content, ...rest }) => rest),
|
||
);
|
||
}
|
||
|
||
function getAllWholeFiles(req, res) {
|
||
const allFiles = $.read(FILES_KEY);
|
||
success(res, allFiles);
|
||
}
|
||
|
||
function replaceFile(req, res) {
|
||
const allFiles = req.body;
|
||
$.write(allFiles, FILES_KEY);
|
||
success(res);
|
||
}
|