refactor: Download API

Now the download APIs are moved into a new file
This commit is contained in:
Peng-YM
2022-06-30 12:19:43 +08:00
parent 8647f9fd59
commit 818e94f41d
14 changed files with 231 additions and 220 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.2.2",
"version": "2.2.3",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
+40 -39
View File
@@ -29,6 +29,14 @@ export default function register($app) {
$app.get('/api/cron/sync-artifacts', cronSyncArtifacts);
}
function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
res.json({
status: 'success',
data: allArtifacts,
});
}
async function getArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
@@ -152,6 +160,37 @@ function updateArtifact(req, res) {
}
}
async function deleteArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除远程配置:${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY);
try {
const artifact = allArtifacts[name];
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) {
// delete gist
const files = {};
files[encodeURIComponent(artifact.name)] = {
content: '',
};
await syncArtifact(files);
}
// delete local cache
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success',
});
} catch (err) {
$.error(`无法删除远程配置:${name},原因:${err}`);
res.status(500).json({
status: 'failed',
message: `无法删除远程配置:${name}, 原因:${err}`,
});
}
}
async function cronSyncArtifacts(_, res) {
$.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY);
@@ -210,45 +249,6 @@ async function cronSyncArtifacts(_, res) {
}
}
async function deleteArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除远程配置:${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY);
try {
const artifact = allArtifacts[name];
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) {
// delete gist
const files = {};
files[encodeURIComponent(artifact.name)] = {
content: '',
};
await syncArtifact(files);
}
// delete local cache
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success',
});
} catch (err) {
$.error(`无法删除远程配置:${name},原因:${err}`);
res.status(500).json({
status: 'failed',
message: `无法删除远程配置:${name}, 原因:${err}`,
});
}
}
function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
res.json({
status: 'success',
data: allArtifacts,
});
}
async function syncArtifact(files) {
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
@@ -276,6 +276,7 @@ async function produceArtifact({ type, item, platform, noProcessor }) {
// parse proxies
let proxies = ProxyUtils.parse(raw);
if (!noProcessor) {
console.log('Processing proxy...');
// apply processors
proxies = await ProxyUtils.process(
proxies,
+1 -68
View File
@@ -1,13 +1,9 @@
import { getPlatformFromHeaders, getFlowHeaders } from './subscriptions';
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
import { produceArtifact } from './artifacts';
import { COLLECTIONS_KEY } from './constants';
import $ from '@/core/app';
export default function register($app) {
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
$app.get('/download/collection/:name', downloadCollection);
$app.route('/api/collection/:name')
.get(getCollection)
.patch(updateCollection)
@@ -19,69 +15,6 @@ export default function register($app) {
}
// collection API
async function downloadCollection(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const { raw } = req.query || 'false';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCollections = $.read(COLLECTIONS_KEY);
const collection = allCollections[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) {
try {
const output = await produceArtifact({
type: 'collection',
item: collection,
platform,
noProcessor: raw,
});
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 下载组合订阅错误:${name}`,
`🤔 原因:${err}`,
);
res.status(500).json({
status: 'failed',
message: err,
});
}
} else {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
res.status(404).json({
status: 'failed',
});
}
}
function createCollection(req, res) {
const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`);
View File
+129
View File
@@ -0,0 +1,129 @@
import { getPlatformFromHeaders } from '@/utils/platform';
import { COLLECTIONS_KEY, SUBS_KEY } from './constants';
import { getFlowHeaders } from '@/utils/flow';
import { produceArtifact } from './artifacts';
import $ from '@/core/app';
export default function register($app) {
$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name', downloadSubscription);
}
async function downloadSubscription(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const raw = req.query.raw || false;
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
const allSubs = $.read(SUBS_KEY);
const sub = allSubs[name];
if (sub) {
try {
const output = await produceArtifact({
type: 'subscription',
item: sub,
platform,
noProcessor: raw,
});
if (sub.source !== 'local') {
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`,
);
$.error(JSON.stringify(err));
res.status(500).json({
status: 'failed',
message: err,
});
}
} else {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}`);
res.status(404).json({
status: 'failed',
});
}
}
async function downloadCollection(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const { raw } = req.query || 'false';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCollections = $.read(COLLECTIONS_KEY);
const collection = allCollections[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) {
try {
const output = await produceArtifact({
type: 'collection',
item: collection,
platform,
noProcessor: raw,
});
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 下载组合订阅错误:${name}`,
`🤔 原因:${err}`,
);
res.status(500).json({
status: 'failed',
message: err,
});
}
} else {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
res.status(404).json({
status: 'failed',
});
}
}
+2
View File
@@ -12,6 +12,7 @@ import $ from '@/core/app';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings';
export default function serve() {
@@ -20,6 +21,7 @@ export default function serve() {
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
registerDownloadRoutes($app);
registerSettingRoutes($app);
registerArtifactRoutes($app);
View File
+1 -97
View File
@@ -4,15 +4,12 @@ import {
ResourceNotFoundError,
} from './errors';
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
import { produceArtifact } from './artifacts';
import { getFlowHeaders } from '@/utils/flow';
import { success, failed } from './response';
import $ from '@/core/app';
export default function register($app) {
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
$app.get('/download/:name', downloadSubscription);
$app.get('/api/sub/flow/:name', getFlowInfo);
$app.route('/api/sub/:name')
@@ -24,62 +21,6 @@ export default function register($app) {
}
// subscriptions API
async function downloadSubscription(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const { raw } = req.query || 'false';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
const allSubs = $.read(SUBS_KEY);
const sub = allSubs[name];
if (sub) {
try {
const output = await produceArtifact({
type: 'subscription',
item: sub,
platform,
noProcessor: raw,
});
if (sub.source !== 'local') {
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`,
);
$.error(JSON.stringify(err));
res.status(500).json({
status: 'failed',
message: err,
});
}
} else {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}`);
res.status(404).json({
status: 'failed',
});
}
}
async function getFlowInfo(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
@@ -231,40 +172,3 @@ function getAllSubscriptions(req, res) {
data: allSubs,
});
}
export async function getFlowHeaders(url) {
const { headers } = await $.http.get({
url,
headers: {
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
},
});
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
}
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
break;
}
}
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (UA.indexOf('Surge') !== -1) {
return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Shadowrocket') !== -1) {
return 'Clash';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else {
return null;
}
}
+10 -6
View File
@@ -16,12 +16,16 @@ export default async function download(url, ua) {
});
const result = new Promise((resolve, reject) => {
http.get(url).then((resp) => {
const body = resp.body;
if (body.replace(/\s/g, '').length === 0)
reject(new Error('订阅内容为空!'));
else resolve(body);
});
http.get(url)
.then((resp) => {
const body = resp.body;
if (body.replace(/\s/g, '').length === 0)
reject(new Error('订阅内容为空!'));
else resolve(body);
})
.catch(() => {
reject(new Error(`无法下载 URL${url}`));
});
});
cache.set(id, result);
+15
View File
@@ -0,0 +1,15 @@
import { HTTP } from '@/vendor/open-api';
export async function getFlowHeaders(url) {
const http = HTTP();
const { headers } = await http.get({
url,
headers: {
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
},
});
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
}
+23
View File
@@ -0,0 +1,23 @@
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
break;
}
}
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (UA.indexOf('Surge') !== -1) {
return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Shadowrocket') !== -1) {
return 'Clash';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else {
return 'JSON';
}
}
+3 -3
View File
File diff suppressed because one or more lines are too long