refactor: Migrate to API v2

- Added auto schema migration
- Refactored /api/subs, /api/collections, /api/artifacts. Now these APIs will return array instead of object. This enables sorting items in the future.
This commit is contained in:
Peng-YM 2022-07-05 10:59:40 +08:00
parent b1151859b3
commit 84b4dba425
19 changed files with 335 additions and 277 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
backend/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.3.2", "version": "2.4.0",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
@ -18,6 +18,7 @@
"js-base64": "^3.7.2", "js-base64": "^3.7.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"request": "^2.88.2", "request": "^2.88.2",
"semver": "^7.3.7",
"static-js-yaml": "^1.0.0" "static-js-yaml": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,3 +1,4 @@
export const SCHEMA_VERSION_KEY = 'schemaVersion';
export const SETTINGS_KEY = 'settings'; export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs'; export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections'; export const COLLECTIONS_KEY = 'collections';

View File

@ -4,19 +4,32 @@ import { getFlag } from '@/utils/geo';
import lodash from 'lodash'; import lodash from 'lodash';
import $ from '@/core/app'; import $ from '@/core/app';
// force to set some properties (e.g., skip-cert-verify, udp, tfo, etc.) function QuickSettingOperator(args) {
function SetPropertyOperator({ key, value }) {
return { return {
name: 'Set Property Operator', name: 'Quick Setting Operator',
func: (proxies) => { func: (proxies) => {
return proxies.map((p) => { return proxies.map((proxy) => {
if ((key == 'aead' && p.type === 'vmess') || key !== 'aead') { proxy.udp = convert(args.udp);
p[key] = value; proxy.tfo = convert(args.tfo);
proxy['skip-cert-verify'] = convert(args.scert);
if (proxy.type === 'vmess') {
proxy.aead = convert(args['vmess aead']);
} }
return p; return proxy;
}); });
}, },
}; };
function convert(value) {
switch (value) {
case 'ENABLED':
return true;
case 'DISABLED':
return false;
default:
return undefined;
}
}
} }
// add or remove flag for proxies // add or remove flag for proxies
@ -389,7 +402,7 @@ function RegexFilter({ regex = [], keep = true }) {
function buildRegex(str, ...options) { function buildRegex(str, ...options) {
options = options.join(''); options = options.join('');
if (str.startsWith('(?i)')) { if (str.startsWith('(?i)')) {
str = str.substr(4); str = str.substring(4);
return new RegExp(str, 'i' + options); return new RegExp(str, 'i' + options);
} else { } else {
return new RegExp(str, options); return new RegExp(str, options);
@ -444,7 +457,7 @@ export default {
'Type Filter': TypeFilter, 'Type Filter': TypeFilter,
'Script Filter': ScriptFilter, 'Script Filter': ScriptFilter,
'Set Property Operator': SetPropertyOperator, 'Quick Setting Operator': QuickSettingOperator,
'Flag Operator': FlagOperator, 'Flag Operator': FlagOperator,
'Sort Operator': SortOperator, 'Sort Operator': SortOperator,
'Regex Sort Operator': RegexSortOperator, 'Regex Sort Operator': RegexSortOperator,
@ -462,7 +475,7 @@ async function ApplyFilter(filter, objs) {
selected = await filter.func(objs); selected = await filter.func(objs);
} catch (err) { } catch (err) {
// print log and skip this filter // print log and skip this filter
console.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`); $.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
} }
return objs.filter((_, i) => selected[i]); return objs.filter((_, i) => selected[i]);
} }
@ -474,7 +487,7 @@ async function ApplyOperator(operator, objs) {
if (output_) output = output_; if (output_) output = output_;
} catch (err) { } catch (err) {
// print log and skip this operator // print log and skip this operator
console.log(`Cannot apply operator ${operator.name}! Reason: ${err}`); $.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
} }
return output; return output;
} }

View File

@ -14,11 +14,13 @@ import { version } from '../package.json';
console.log( console.log(
` `
Sub-Store © Peng-YM -- v${version} Sub-Store -- v${version}
`, `,
); );
import migrate from '@/utils/migration';
import serve from '@/restful'; import serve from '@/restful';
migrate();
serve(); serve();

View File

@ -1,17 +1,12 @@
import {
ARTIFACTS_KEY,
SUBS_KEY,
COLLECTIONS_KEY,
RULES_KEY,
} from '@/restful/constants';
import { syncArtifact, produceArtifact } from '@/restful/artifacts'; import { syncArtifact, produceArtifact } from '@/restful/artifacts';
import { version } from '../../package.json'; import { version } from '../../package.json';
import { ARTIFACTS_KEY } from '@/constants';
import $ from '@/core/app'; import $ from '@/core/app';
console.log( console.log(
` `
Sub-Store © Peng-YM -- v${version} Sub-Store -- v${version}
`, `,
); );
@ -22,24 +17,12 @@ console.log(
try { try {
await Promise.all( await Promise.all(
Object.values(allArtifacts).map(async (artifact) => { allArtifacts.map(async (artifact) => {
if (artifact.sync) { if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`); $.info(`正在同步云配置:${artifact.name}...`);
let item;
switch (artifact.type) {
case 'subscription':
item = $.read(SUBS_KEY)[artifact.source];
break;
case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source];
break;
case 'rule':
item = $.read(RULES_KEY)[artifact.source];
break;
}
const output = await produceArtifact({ const output = await produceArtifact({
type: artifact.type, type: artifact.type,
item, name: artifact.source,
platform: artifact.platform, platform: artifact.platform,
}); });
@ -53,7 +36,7 @@ console.log(
const resp = await syncArtifact(files); const resp = await syncArtifact(files);
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
for (const artifact of Object.values(allArtifacts)) { for (const artifact of allArtifacts) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace( artifact.url = body.files[artifact.name].raw_url.replace(

View File

@ -6,7 +6,7 @@ import { version } from '../../package.json';
console.log( console.log(
` `
Sub-Store © Peng-YM -- v${version} Sub-Store -- v${version}
`, `,
); );

View File

@ -11,7 +11,9 @@ import {
COLLECTIONS_KEY, COLLECTIONS_KEY,
RULES_KEY, RULES_KEY,
SETTINGS_KEY, SETTINGS_KEY,
} from './constants'; } from '@/constants';
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { success } from '@/restful/response';
export default function register($app) { export default function register($app) {
// Initialization // Initialization
@ -31,10 +33,7 @@ export default function register($app) {
function getAllArtifacts(req, res) { function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
res.json({ success(res, allArtifacts);
status: 'success',
data: allArtifacts,
});
} }
async function getArtifact(req, res) { async function getArtifact(req, res) {
@ -42,32 +41,25 @@ async function getArtifact(req, res) {
name = decodeURIComponent(name); name = decodeURIComponent(name);
const action = req.query.action; const action = req.query.action;
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = allArtifacts[name]; const artifact = findByName(allArtifacts, name);
if (artifact) { if (artifact) {
if (action) { if (action) {
let item;
switch (artifact.type) {
case 'subscription':
item = $.read(SUBS_KEY)[artifact.source];
break;
case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source];
break;
case 'rule':
item = $.read(RULES_KEY)[artifact.source];
break;
}
const output = await produceArtifact({ const output = await produceArtifact({
type: artifact.type, type: artifact.type,
item, name: artifact.source,
platform: artifact.platform, platform: artifact.platform,
}); });
if (action === 'preview') { if (action === 'preview') {
res.send(output); res.send(output);
} else if (action === 'sync') { } else if (action === 'sync') {
$.info(`正在上传配置:${artifact.name}\n>>>`); $.info(
console.log(JSON.stringify(artifact, null, 2)); `正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
try { try {
const resp = await syncArtifact({ const resp = await syncArtifact({
[encodeURIComponent(artifact.name)]: { [encodeURIComponent(artifact.name)]: {
@ -80,9 +72,7 @@ async function getArtifact(req, res) {
encodeURIComponent(artifact.name) encodeURIComponent(artifact.name)
].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); ].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
res.json({ success(res);
status: 'success',
});
} catch (err) { } catch (err) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
@ -91,10 +81,7 @@ async function getArtifact(req, res) {
} }
} }
} else { } else {
res.json({ success(res, artifact);
status: 'success',
data: artifact,
});
} }
} else { } else {
res.status(404).json({ res.status(404).json({
@ -108,18 +95,15 @@ function createArtifact(req, res) {
const artifact = req.body; const artifact = req.body;
$.info(`正在创建远程配置:${artifact.name}`); $.info(`正在创建远程配置:${artifact.name}`);
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
if (allArtifacts[artifact.name]) { if (findByName(allArtifacts, artifact.name)) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `远程配置${artifact.name}已存在!`, message: `远程配置${artifact.name}已存在!`,
}); });
} else { } else {
allArtifacts[artifact.name] = artifact; allArtifacts.push(artifact);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
res.status(201).json({ success(res, artifact, 201);
status: 'success',
data: artifact,
});
} }
} }
@ -127,31 +111,16 @@ function updateArtifact(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
let oldName = req.params.name; let oldName = req.params.name;
oldName = decodeURIComponent(oldName); oldName = decodeURIComponent(oldName);
const artifact = allArtifacts[oldName]; const artifact = findByName(allArtifacts, oldName);
if (artifact) { if (artifact) {
$.info(`正在更新远程配置:${artifact.name}`); $.info(`正在更新远程配置:${artifact.name}`);
const newArtifact = req.body; const newArtifact = {
if ( ...artifact,
typeof newArtifact.name !== 'undefined' && ...req.body,
!/^[\w-_.]*$/.test(newArtifact.name) };
) { updateByName(allArtifacts, oldName, newArtifact);
res.status(500).json({ $.write(allArtifacts, ARTIFACTS_KEY);
status: 'failed', success(res, newArtifact);
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
} else {
const merged = {
...artifact,
...newArtifact,
};
allArtifacts[merged.name] = merged;
if (merged.name !== oldName) delete allArtifacts[oldName];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success',
data: merged,
});
}
} else { } else {
res.status(404).json({ res.status(404).json({
status: 'failed', status: 'failed',
@ -166,7 +135,7 @@ async function deleteArtifact(req, res) {
$.info(`正在删除远程配置:${name}`); $.info(`正在删除远程配置:${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
try { try {
const artifact = allArtifacts[name]; const artifact = findByName(allArtifacts, name);
if (!artifact) throw new Error(`远程配置:${name}不存在!`); if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) { if (artifact.updated) {
// delete gist // delete gist
@ -177,11 +146,9 @@ async function deleteArtifact(req, res) {
await syncArtifact(files); await syncArtifact(files);
} }
// delete local cache // delete local cache
delete allArtifacts[name]; deleteByName(allArtifacts, name);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
res.json({ success(res);
status: 'success',
});
} catch (err) { } catch (err) {
$.error(`无法删除远程配置:${name},原因:${err}`); $.error(`无法删除远程配置:${name},原因:${err}`);
res.status(500).json({ res.status(500).json({
@ -198,24 +165,12 @@ async function cronSyncArtifacts(_, res) {
try { try {
await Promise.all( await Promise.all(
Object.values(allArtifacts).map(async (artifact) => { allArtifacts.map(async (artifact) => {
if (artifact.sync) { if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`); $.info(`正在同步云配置:${artifact.name}...`);
let item;
switch (artifact.type) {
case 'subscription':
item = $.read(SUBS_KEY)[artifact.source];
break;
case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source];
break;
case 'rule':
item = $.read(RULES_KEY)[artifact.source];
break;
}
const output = await produceArtifact({ const output = await produceArtifact({
type: artifact.type, type: artifact.type,
item, name: artifact.source,
platform: artifact.platform, platform: artifact.platform,
}); });
@ -229,7 +184,7 @@ async function cronSyncArtifacts(_, res) {
const resp = await syncArtifact(files); const resp = await syncArtifact(files);
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
for (const artifact of Object.values(allArtifacts)) { for (const artifact of allArtifacts) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace( artifact.url = body.files[artifact.name].raw_url.replace(
@ -240,7 +195,7 @@ async function cronSyncArtifacts(_, res) {
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!'); $.info('全部订阅同步成功!');
res.status(200).end(); success(res);
} catch (err) { } catch (err) {
res.status(500).json({ res.status(500).json({
error: err, error: err,
@ -261,12 +216,12 @@ async function syncArtifact(files) {
return manager.upload(files); return manager.upload(files);
} }
async function produceArtifact({ type, item, platform, noProcessor }) { async function produceArtifact({ type, name, platform }) {
platform = platform || 'JSON'; platform = platform || 'JSON';
noProcessor = noProcessor || false;
if (type === 'subscription') { if (type === 'subscription') {
const sub = item; const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
let raw; let raw;
if (sub.source === 'local') { if (sub.source === 'local') {
raw = sub.content; raw = sub.content;
@ -275,14 +230,12 @@ async function produceArtifact({ type, item, platform, noProcessor }) {
} }
// parse proxies // parse proxies
let proxies = ProxyUtils.parse(raw); let proxies = ProxyUtils.parse(raw);
if (!noProcessor) { // apply processors
// apply processors proxies = await ProxyUtils.process(
proxies = await ProxyUtils.process( proxies,
proxies, sub.process || [],
sub.process || [], platform,
platform, );
);
}
// check duplicate // check duplicate
const exist = {}; const exist = {};
for (const proxy of proxies) { for (const proxy of proxies) {
@ -304,14 +257,15 @@ async function produceArtifact({ type, item, platform, noProcessor }) {
return ProxyUtils.produce(proxies, platform); return ProxyUtils.produce(proxies, platform);
} else if (type === 'collection') { } else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const collection = item; const allCols = $.read(COLLECTIONS_KEY);
const subnames = collection['subscriptions']; const collection = findByName(allCols, name);
const subnames = collection.subscriptions;
const results = {}; const results = {};
let processed = 0; let processed = 0;
await Promise.all( await Promise.all(
subnames.map(async (name) => { subnames.map(async (name) => {
const sub = allSubs[name]; const sub = findByName(allSubs, name);
try { try {
$.info(`正在处理子订阅:${sub.name}...`); $.info(`正在处理子订阅:${sub.name}...`);
let raw; let raw;
@ -322,14 +276,12 @@ async function produceArtifact({ type, item, platform, noProcessor }) {
} }
// parse proxies // parse proxies
let currentProxies = ProxyUtils.parse(raw); let currentProxies = ProxyUtils.parse(raw);
if (!noProcessor) { // apply processors
// apply processors currentProxies = await ProxyUtils.process(
currentProxies = await ProxyUtils.process( currentProxies,
currentProxies, sub.process || [],
sub.process || [], platform,
platform, );
);
}
results[name] = currentProxies; results[name] = currentProxies;
processed++; processed++;
$.info( $.info(
@ -356,14 +308,12 @@ async function produceArtifact({ type, item, platform, noProcessor }) {
subnames.map((name) => results[name]), subnames.map((name) => results[name]),
); );
if (!noProcessor) { // apply own processors
// apply own processors proxies = await ProxyUtils.process(
proxies = await ProxyUtils.process( proxies,
proxies, collection.process || [],
collection.process || [], platform,
platform, );
);
}
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点!`); throw new Error(`组合订阅中不含有效节点!`);
} }
@ -386,7 +336,8 @@ async function produceArtifact({ type, item, platform, noProcessor }) {
} }
return ProxyUtils.produce(proxies, platform); return ProxyUtils.produce(proxies, platform);
} else if (type === 'rule') { } else if (type === 'rule') {
const rule = item; const allRules = $.read(RULES_KEY);
const rule = findByName(allRules, name);
let rules = []; let rules = [];
for (let i = 0; i < rule.urls.length; i++) { for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i]; const url = rule.urls[i];

View File

@ -1,4 +1,6 @@
import { COLLECTIONS_KEY } from './constants'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { COLLECTIONS_KEY } from '@/constants';
import { success } from '@/restful/response';
import $ from '@/core/app'; import $ from '@/core/app';
export default function register($app) { export default function register($app) {
@ -18,30 +20,25 @@ export default function register($app) {
function createCollection(req, res) { function createCollection(req, res) {
const collection = req.body; const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`); $.info(`正在创建组合订阅:${collection.name}`);
const allCol = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
if (allCol[collection.name]) { if (findByName(allCols, collection.name)) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅集${collection.name}已存在!`, message: `订阅集${collection.name}已存在!`,
}); });
} }
allCol[collection.name] = collection; allCols.push(collection);
$.write(allCol, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
res.status(201).json({ success(res, collection, 201);
status: 'success',
data: collection,
});
} }
function getCollection(req, res) { function getCollection(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const collection = $.read(COLLECTIONS_KEY)[name]; const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
if (collection) { if (collection) {
res.json({ success(res, collection);
status: 'success',
data: collection,
});
} else { } else {
res.status(404).json({ res.status(404).json({
status: 'failed', status: 'failed',
@ -54,21 +51,17 @@ function updateCollection(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
let collection = req.body; let collection = req.body;
const allCol = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
if (allCol[name]) { const oldCol = findByName(allCols, name);
if (oldCol) {
const newCol = { const newCol = {
...allCol[name], ...oldCol,
...collection, ...collection,
}; };
$.info(`正在更新组合订阅:${name}...`); $.info(`正在更新组合订阅:${name}...`);
// allow users to update collection name updateByName(allCols, name, newCol);
delete allCol[name]; $.write(allCols, COLLECTIONS_KEY);
allCol[collection.name || name] = newCol; success(res, newCol);
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: 'success',
data: newCol,
});
} else { } else {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
@ -81,18 +74,13 @@ function deleteCollection(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
$.info(`正在删除组合订阅:${name}`); $.info(`正在删除组合订阅:${name}`);
let allCol = $.read(COLLECTIONS_KEY); let allCols = $.read(COLLECTIONS_KEY);
delete allCol[name]; deleteByName(allCols, name);
$.write(allCol, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
res.json({ success(res);
status: 'success',
});
} }
function getAllCollections(req, res) { function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
res.json({ success(res, allCols);
status: 'success',
data: allCols,
});
} }

View File

@ -1,5 +1,6 @@
import { getPlatformFromHeaders } from '@/utils/platform'; import { getPlatformFromHeaders } from '@/utils/platform';
import { COLLECTIONS_KEY, SUBS_KEY } from './constants'; import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow'; import { getFlowHeaders } from '@/utils/flow';
import { produceArtifact } from './artifacts'; import { produceArtifact } from './artifacts';
import $ from '@/core/app'; import $ from '@/core/app';
@ -13,21 +14,19 @@ async function downloadSubscription(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const raw = req.query.raw || false;
const platform = const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`); $.info(`正在下载订阅:${name}`);
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = allSubs[name]; const sub = findByName(allSubs, name);
if (sub) { if (sub) {
try { try {
const output = await produceArtifact({ const output = await produceArtifact({
type: 'subscription', type: 'subscription',
item: sub, name,
platform, platform,
noProcessor: raw,
}); });
if (sub.source !== 'local') { if (sub.source !== 'local') {
@ -69,36 +68,35 @@ async function downloadCollection(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const { raw } = req.query || 'false';
const platform = const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCollections = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const collection = allCollections[name]; const collection = findByName(allCols, name);
$.info(`正在下载组合订阅:${name}`); $.info(`正在下载组合订阅:${name}`);
// forward flow header from the first subscription in this collection
const allSubs = $.read(SUBS_KEY);
const subs = collection['subscriptions'];
if (subs.length > 0) {
const sub = allSubs[subs[0]];
if (sub.source !== 'local') {
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
}
if (collection) { if (collection) {
try { try {
const output = await produceArtifact({ const output = await produceArtifact({
type: 'collection', type: 'collection',
item: collection, name,
platform, platform,
noProcessor: raw,
}); });
// forward flow header from the first subscription in this collection
const allSubs = $.read(SUBS_KEY);
const subnames = collection.subscriptions;
if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') {
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
}
if (platform === 'JSON') { if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send( res.set('Content-Type', 'application/json;charset=utf-8').send(
output, output,

View File

@ -2,7 +2,7 @@ import {
SETTINGS_KEY, SETTINGS_KEY,
GIST_BACKUP_KEY, GIST_BACKUP_KEY,
GIST_BACKUP_FILE_NAME, GIST_BACKUP_FILE_NAME,
} from './constants'; } from '@/constants';
import { version as substoreVersion } from '../../package.json'; import { version as substoreVersion } from '../../package.json';
import { ENV, HTTP } from '@/vendor/open-api'; import { ENV, HTTP } from '@/vendor/open-api';
import express from '@/vendor/express'; import express from '@/vendor/express';
@ -15,6 +15,7 @@ import registerArtifactRoutes from './artifacts';
import registerDownloadRoutes from './download'; import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings'; import registerSettingRoutes from './settings';
import registerPreviewRoutes from './preview'; import registerPreviewRoutes from './preview';
import { success } from '@/restful/response';
export default function serve() { export default function serve() {
const $app = express({ substore: $ }); const $app = express({ substore: $ });
@ -117,15 +118,12 @@ async function gistBackup(req, res) {
$.write(content, '#sub-store'); $.write(content, '#sub-store');
if ($.env.isNode) { if ($.env.isNode) {
content = JSON.parse(content); content = JSON.parse(content);
Object.keys(content).forEach((key) => { $.cache = content;
$.write(content[key], key); $.persistCache();
});
} }
break; break;
} }
res.json({ success(res);
status: 'success',
});
} catch (err) { } catch (err) {
const msg = `${ const msg = `${
action === 'upload' ? '上传' : '下载' action === 'upload' ? '上传' : '下载'

View File

@ -1,8 +1,9 @@
import { InternalServerError, NetworkError } from './errors'; import { InternalServerError, NetworkError } from './errors';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import { findByName } from '@/utils/database';
import { success, failed } from './response'; import { success, failed } from './response';
import download from '@/utils/download'; import download from '@/utils/download';
import { SUBS_KEY } from './constants'; import { SUBS_KEY } from '@/constants';
import $ from '@/core/app'; import $ from '@/core/app';
export default function register($app) { export default function register($app) {
@ -53,12 +54,12 @@ async function compareSub(req, res) {
async function compareCollection(req, res) { async function compareCollection(req, res) {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const collection = req.body; const collection = req.body;
const subnames = collection['subscriptions']; const subnames = collection.subscriptions;
const results = {}; const results = {};
await Promise.all( await Promise.all(
subnames.map(async (name) => { subnames.map(async (name) => {
const sub = allSubs[name]; const sub = findByName(allSubs, name);
try { try {
let raw; let raw;
if (sub.source === 'local') { if (sub.source === 'local') {

View File

@ -1,4 +1,5 @@
import { SETTINGS_KEY } from './constants'; import { SETTINGS_KEY } from '@/constants';
import { success } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
export default function register($app) { export default function register($app) {
@ -8,20 +9,15 @@ export default function register($app) {
function getSettings(req, res) { function getSettings(req, res) {
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
res.json(settings); success(res, settings);
} }
function updateSettings(req, res) { function updateSettings(req, res) {
const data = req.body;
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
$.write( const newSettings = {
{ ...settings,
...settings, ...req.body,
...data, };
}, $.write(newSettings, SETTINGS_KEY);
SETTINGS_KEY, success(res, newSettings);
);
res.json({
status: 'success',
});
} }

View File

@ -3,13 +3,15 @@ import {
InternalServerError, InternalServerError,
ResourceNotFoundError, ResourceNotFoundError,
} from './errors'; } from './errors';
import { SUBS_KEY, COLLECTIONS_KEY } from './constants'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY } from '@/constants';
import { getFlowHeaders } from '@/utils/flow'; import { getFlowHeaders } from '@/utils/flow';
import { success, failed } from './response'; import { success, failed } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
export default function register($app) { export default function register($app) {
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
$app.get('/api/sub/flow/:name', getFlowInfo); $app.get('/api/sub/flow/:name', getFlowInfo);
$app.route('/api/sub/:name') $app.route('/api/sub/:name')
@ -24,8 +26,8 @@ export default function register($app) {
async function getFlowInfo(req, res) { async function getFlowInfo(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const sub = $.read(SUBS_KEY)[name]; const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (!sub) { if (!sub) {
failed( failed(
res, res,
@ -71,31 +73,26 @@ async function getFlowInfo(req, res) {
function createSubscription(req, res) { function createSubscription(req, res) {
const sub = req.body; const sub = req.body;
const allSubs = $.read(SUBS_KEY);
$.info(`正在创建订阅: ${sub.name}`); $.info(`正在创建订阅: ${sub.name}`);
if (allSubs[sub.name]) { const allSubs = $.read(SUBS_KEY);
if (findByName(allSubs, sub.name)) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅${sub.name}已存在!`, message: `订阅${sub.name}已存在!`,
}); });
} }
allSubs[sub.name] = sub; allSubs.push(sub);
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
res.status(201).json({ success(res, sub, 201);
status: 'success',
data: sub,
});
} }
function getSubscription(req, res) { function getSubscription(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const sub = $.read(SUBS_KEY)[name]; const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (sub) { if (sub) {
res.json({ success(res, sub);
status: 'success',
data: sub,
});
} else { } else {
res.status(404).json({ res.status(404).json({
status: 'failed', status: 'failed',
@ -106,12 +103,13 @@ function getSubscription(req, res) {
function updateSubscription(req, res) { function updateSubscription(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name); // the original name
let sub = req.body; let sub = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) { const oldSub = findByName(allSubs, name);
if (oldSub) {
const newSub = { const newSub = {
...allSubs[name], ...oldSub,
...sub, ...sub,
}; };
$.info(`正在更新订阅: ${name}`); $.info(`正在更新订阅: ${name}`);
@ -119,23 +117,16 @@ function updateSubscription(req, res) {
if (name !== sub.name) { if (name !== sub.name) {
// we need to find out all collections refer to this name // we need to find out all collections refer to this name
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) { for (const collection of allCols) {
const idx = allCols[k].subscriptions.indexOf(name); const idx = collection.subscriptions.indexOf(name);
if (idx !== -1) { if (idx !== -1) {
allCols[k].subscriptions[idx] = sub.name; collection.subscriptions[idx] = sub.name;
} }
} }
// update subscriptions
delete allSubs[name];
allSubs[sub.name] = newSub;
} else {
allSubs[name] = newSub;
} }
updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
res.json({ success(res, newSub);
status: 'success',
data: newSub,
});
} else { } else {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
@ -150,25 +141,20 @@ function deleteSubscription(req, res) {
$.info(`删除订阅:${name}...`); $.info(`删除订阅:${name}...`);
// delete from subscriptions // delete from subscriptions
let allSubs = $.read(SUBS_KEY); let allSubs = $.read(SUBS_KEY);
delete allSubs[name]; deleteByName(allSubs, name);
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
// delete from collections // delete from collections
let allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) { for (const collection of allCols) {
allCols[k].subscriptions = allCols[k].subscriptions.filter( collection.subscriptions = collection.subscriptions.filter(
(s) => s !== name, (s) => s !== name,
); );
} }
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
res.json({ success(res);
status: 'success',
});
} }
function getAllSubscriptions(req, res) { function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
res.json({ success(res, allSubs);
status: 'success',
data: allSubs,
});
} }

View File

@ -0,0 +1,17 @@
export function findByName(list, name) {
return list.find((item) => item.name === name);
}
export function findIndexByName(list, name) {
return list.find((item) => item.name === name);
}
export function deleteByName(list, name) {
const idx = findIndexByName(list, name);
list.splice(idx, 1);
}
export function updateByName(list, name, newItem) {
const idx = findIndexByName(list, name);
list[idx] = newItem;
}

View File

@ -0,0 +1,115 @@
import {
SUBS_KEY,
COLLECTIONS_KEY,
SCHEMA_VERSION_KEY,
ARTIFACTS_KEY,
RULES_KEY,
} from '@/constants';
import $ from '@/core/app';
export default function migrate() {
migrateV2();
}
function migrateV2() {
const version = $.read(SCHEMA_VERSION_KEY);
if (!version) doMigrationV2();
// write the current version
if (version !== '2.0') {
$.write('2.0', SCHEMA_VERSION_KEY);
}
}
function doMigrationV2() {
$.info('Start migrating...');
// 1. migrate subscriptions
const subs = $.read(SUBS_KEY) || {};
const newSubs = Object.values(subs).map((sub) => {
migrateDisplayName(sub);
migrateProcesses(sub);
return sub;
});
$.write(newSubs, SUBS_KEY);
// 2. migrate collections
const collections = $.read(COLLECTIONS_KEY) || {};
const newCollections = Object.values(collections).map((collection) => {
delete collection.ua;
migrateDisplayName(collection);
migrateProcesses(collection);
return collection;
});
$.write(newCollections, COLLECTIONS_KEY);
// 3. migrate artifacts
const artifacts = $.read(ARTIFACTS_KEY) || {};
const newArtifacts = Object.values(artifacts);
$.write(newArtifacts, ARTIFACTS_KEY);
// 4. migrate rules
const rules = $.read(RULES_KEY) || {};
const newRules = Object.values(rules);
$.write(newRules, RULES_KEY);
// 5. delete builtin rules
delete $.cache.builtin;
$.info('Migration complete!');
function migrateDisplayName(item) {
const displayName = item['display-name'];
if (displayName) {
item.displayName = displayName;
delete item['display-name'];
}
}
function migrateProcesses(item) {
const processes = item.process;
if (!processes || processes.length === 0) return;
const newProcesses = [];
const quickSettingOperator = {
type: 'Quick Setting Operator',
args: {
udp: 'DEFAULT',
tfo: 'DEFAULT',
scert: 'DEFAULT',
'vmess aead': 'DEFAULT',
},
};
processes.forEach((p) => {
delete p.id;
if (p.type === 'Set Property Operator') {
const { key, value } = p.args;
switch (key) {
case 'udp':
quickSettingOperator.args.udp = value
? 'ENABLED'
: 'DISABLED';
break;
case 'tfo':
quickSettingOperator.args.tfo = value
? 'ENABLED'
: 'DISABLED';
break;
case 'skip-cert-verify':
quickSettingOperator.args.scert = value
? 'ENABLED'
: 'DISABLED';
break;
case 'aead':
quickSettingOperator.args['vmess aead'] = value
? 'ENABLED'
: 'DISABLED';
break;
}
} else if (p.type.indexOf('Keyword') !== -1) {
// do nothing
} else {
newProcesses.push(p);
}
});
newProcesses.unshift(quickSettingOperator);
item.process = newProcesses;
}
}

File diff suppressed because one or more lines are too long