mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2026-01-14 06:32:54 +08:00
225 lines
6.7 KiB
JavaScript
225 lines
6.7 KiB
JavaScript
import {
|
|
SETTINGS_KEY,
|
|
GIST_BACKUP_KEY,
|
|
GIST_BACKUP_FILE_NAME,
|
|
} from '@/constants';
|
|
import { version as substoreVersion } from '../../package.json';
|
|
import { ENV, HTTP } from '@/vendor/open-api';
|
|
import express from '@/vendor/express';
|
|
import Gist from '@/utils/gist';
|
|
import migrate from '@/utils/migration';
|
|
import $ from '@/core/app';
|
|
|
|
import registerSubscriptionRoutes from './subscriptions';
|
|
import registerCollectionRoutes from './collections';
|
|
import registerArtifactRoutes from './artifacts';
|
|
import registerDownloadRoutes from './download';
|
|
import registerSettingRoutes, {
|
|
updateArtifactStore,
|
|
updateGitHubAvatar,
|
|
} from './settings';
|
|
import registerPreviewRoutes from './preview';
|
|
import registerSortingRoutes from './sort';
|
|
import { failed, success } from '@/restful/response';
|
|
import {
|
|
InternalServerError,
|
|
NetworkError,
|
|
RequestInvalidError,
|
|
} from '@/restful/errors';
|
|
import resourceCache from '@/utils/resource-cache';
|
|
import producer from '@/core/proxy-utils/producers';
|
|
|
|
export default function serve() {
|
|
const $app = express({ substore: $ });
|
|
|
|
// register routes
|
|
registerCollectionRoutes($app);
|
|
registerSubscriptionRoutes($app);
|
|
registerDownloadRoutes($app);
|
|
registerPreviewRoutes($app);
|
|
registerSortingRoutes($app);
|
|
registerSettingRoutes($app);
|
|
registerArtifactRoutes($app);
|
|
|
|
// utils
|
|
$app.post('/api/utils/node-info', getNodeInfo);
|
|
$app.get('/api/utils/env', getEnv); // get runtime environment
|
|
$app.get('/api/utils/backup', gistBackup); // gist backup actions
|
|
$app.get('/api/utils/refresh', refresh);
|
|
|
|
// Storage management
|
|
$app.route('/api/storage')
|
|
.get((req, res) => {
|
|
res.json($.read('#sub-store'));
|
|
})
|
|
.post((req, res) => {
|
|
const data = req.body;
|
|
$.write(JSON.stringify(data), '#sub-store');
|
|
res.end();
|
|
});
|
|
|
|
// Redirect sub.store to vercel webpage
|
|
$app.get('/', async (req, res) => {
|
|
// 302 redirect
|
|
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
|
|
});
|
|
|
|
// handle preflight request for QX
|
|
if (ENV().isQX) {
|
|
$app.options('/', async (req, res) => {
|
|
res.status(200).end();
|
|
});
|
|
}
|
|
|
|
$app.all('/', (_, res) => {
|
|
res.send('Hello from sub-store, made with ❤️ by Peng-YM');
|
|
});
|
|
|
|
$app.start();
|
|
}
|
|
|
|
function getEnv(req, res) {
|
|
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
|
|
let backend = 'Node';
|
|
if (isNode) backend = 'Node';
|
|
if (isQX) backend = 'QX';
|
|
if (isLoon) backend = 'Loon';
|
|
if (isSurge) backend = 'Surge';
|
|
if (isStash) backend = 'Stash';
|
|
if (isShadowRocket) backend = 'ShadowRocket';
|
|
|
|
success(res, {
|
|
backend,
|
|
version: substoreVersion,
|
|
});
|
|
}
|
|
|
|
async function refresh(_, res) {
|
|
// 1. get GitHub avatar and artifact store
|
|
await updateGitHubAvatar();
|
|
await updateArtifactStore();
|
|
|
|
// 2. clear resource cache
|
|
resourceCache.revokeAll();
|
|
success(res);
|
|
}
|
|
|
|
async function gistBackup(req, res) {
|
|
const { action } = req.query;
|
|
// read token
|
|
const { gistToken } = $.read(SETTINGS_KEY);
|
|
if (!gistToken) {
|
|
failed(
|
|
res,
|
|
new RequestInvalidError(
|
|
'GIST_TOKEN_NOT_FOUND',
|
|
`GitHub Token is required for backup!`,
|
|
),
|
|
);
|
|
} else {
|
|
const gist = new Gist({
|
|
token: gistToken,
|
|
key: GIST_BACKUP_KEY,
|
|
});
|
|
try {
|
|
let content;
|
|
const settings = $.read(SETTINGS_KEY);
|
|
const updated = settings.syncTime;
|
|
switch (action) {
|
|
case 'upload':
|
|
// update syncTime
|
|
settings.syncTime = new Date().getTime();
|
|
$.write(settings, SETTINGS_KEY);
|
|
content = $.read('#sub-store');
|
|
if ($.env.isNode)
|
|
content = JSON.stringify($.cache, null, ` `);
|
|
$.info(`上传备份中...`);
|
|
try {
|
|
await gist.upload({
|
|
[GIST_BACKUP_FILE_NAME]: { content },
|
|
});
|
|
} catch (err) {
|
|
// restore syncTime if upload failed
|
|
settings.syncTime = updated;
|
|
$.write(settings, SETTINGS_KEY);
|
|
throw err;
|
|
}
|
|
break;
|
|
case 'download':
|
|
$.info(`还原备份中...`);
|
|
content = await gist.download(GIST_BACKUP_FILE_NAME);
|
|
// restore settings
|
|
$.write(content, '#sub-store');
|
|
if ($.env.isNode) {
|
|
content = JSON.parse(content);
|
|
$.cache = content;
|
|
$.persistCache();
|
|
}
|
|
// perform migration after restoring from gist
|
|
migrate();
|
|
break;
|
|
}
|
|
success(res);
|
|
} catch (err) {
|
|
failed(
|
|
res,
|
|
new InternalServerError(
|
|
'BACKUP_FAILED',
|
|
`Failed to ${action} data to gist!`,
|
|
`Reason: ${JSON.stringify(err)}`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getNodeInfo(req, res) {
|
|
const proxy = req.body;
|
|
const lang = req.query.lang || 'zh-CN';
|
|
let shareUrl;
|
|
try {
|
|
shareUrl = producer.URI.produce(proxy);
|
|
} catch (err) {
|
|
// do nothing
|
|
}
|
|
|
|
try {
|
|
const $http = HTTP();
|
|
const info = await $http
|
|
.get({
|
|
url: `http://ip-api.com/json/${encodeURIComponent(
|
|
proxy.server,
|
|
)}?lang=${lang}`,
|
|
headers: {
|
|
'User-Agent':
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15',
|
|
},
|
|
})
|
|
.then((resp) => {
|
|
const data = JSON.parse(resp.body);
|
|
if (data.status !== 'success') {
|
|
throw new Error(data.message);
|
|
}
|
|
|
|
// remove unnecessary fields
|
|
delete data.status;
|
|
delete data.query;
|
|
|
|
return data;
|
|
});
|
|
success(res, {
|
|
shareUrl,
|
|
info,
|
|
});
|
|
} catch (err) {
|
|
failed(
|
|
res,
|
|
new NetworkError(
|
|
'FAILED_TO_GET_NODE_INFO',
|
|
`Failed to get node info`,
|
|
`Reason: ${err}`,
|
|
),
|
|
);
|
|
}
|
|
}
|