Skip to content

Commit 0d206cd

Browse files
committed
feat: 支持归档(回收站)
1 parent ec54404 commit 0d206cd

17 files changed

Lines changed: 1193 additions & 302 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.21.65",
3+
"version": "2.21.66",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {

backend/src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const MODULES_KEY = 'modules';
77
export const ARTIFACTS_KEY = 'artifacts';
88
export const RULES_KEY = 'rules';
99
export const TOKENS_KEY = 'tokens';
10+
export const ARCHIVES_KEY = 'archives';
1011
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
1112
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
1213
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';

backend/src/core/proxy-utils/parsers/peggy/loon.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as peggy from 'peggy';
1+
import peggy from 'peggy';
22
const grammars = String.raw`
33
// global initializer
44
{{

backend/src/core/proxy-utils/parsers/peggy/qx.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as peggy from 'peggy';
1+
import peggy from 'peggy';
22
const grammars = String.raw`
33
// global initializer
44
{{

backend/src/core/proxy-utils/parsers/peggy/surge.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as peggy from 'peggy';
1+
import peggy from 'peggy';
22
const grammars = String.raw`
33
// global initializer
44
{{

backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as peggy from 'peggy';
1+
import peggy from 'peggy';
22
const grammars = String.raw`
33
// global initializer
44
{{

backend/src/restful/archives.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { failed, success } from '@/restful/response';
2+
import {
3+
InternalServerError,
4+
RequestInvalidError,
5+
ResourceNotFoundError,
6+
} from '@/restful/errors';
7+
import {
8+
ensureArchiveStore,
9+
getArchiveEntries,
10+
getRequiredArchiveEntry,
11+
removeArchiveEntry,
12+
} from '@/utils/archive';
13+
import { createSubscriptionItem } from '@/restful/subscriptions';
14+
import { createCollectionItem } from '@/restful/collections';
15+
import { createFileItem } from '@/restful/file';
16+
import { createArtifactItem } from '@/restful/artifacts';
17+
import { createTokenItem } from '@/restful/token';
18+
19+
export default function register($app) {
20+
ensureArchiveStore();
21+
22+
$app.route('/api/archives').get(getAllArchiveEntries);
23+
$app.route('/api/archives/:id')
24+
.get(getArchiveDetail)
25+
.delete(deleteArchiveEntry);
26+
$app.post('/api/archives/:id/restore', restoreArchiveEntry);
27+
}
28+
29+
function getAllArchiveEntries(_, res) {
30+
success(res, getArchiveEntries());
31+
}
32+
33+
function getArchiveDetail(req, res) {
34+
try {
35+
success(res, getRequiredArchiveEntry(req.params.id));
36+
} catch (error) {
37+
failed(res, error, error instanceof ResourceNotFoundError ? 404 : 500);
38+
}
39+
}
40+
41+
function deleteArchiveEntry(req, res) {
42+
try {
43+
const entry = removeArchiveEntry(req.params.id);
44+
success(res, entry);
45+
} catch (error) {
46+
failed(res, error, error instanceof ResourceNotFoundError ? 404 : 500);
47+
}
48+
}
49+
50+
function restoreArchiveEntry(req, res) {
51+
try {
52+
const entry = getRequiredArchiveEntry(req.params.id);
53+
const restored = restoreArchivedEntry(entry);
54+
removeArchiveEntry(req.params.id);
55+
success(res, restored);
56+
} catch (error) {
57+
const mappedError =
58+
error instanceof RequestInvalidError ||
59+
error instanceof ResourceNotFoundError
60+
? error
61+
: new InternalServerError(
62+
'ARCHIVE_RESTORE_FAILED',
63+
'Failed to restore archive entry',
64+
`Reason: ${error.message ?? error}`,
65+
);
66+
const statusCode =
67+
mappedError instanceof ResourceNotFoundError
68+
? 404
69+
: mappedError instanceof RequestInvalidError
70+
? 400
71+
: 500;
72+
failed(
73+
res,
74+
mappedError,
75+
statusCode,
76+
);
77+
}
78+
}
79+
80+
function restoreArchivedEntry(entry) {
81+
const snapshot = JSON.parse(JSON.stringify(entry.snapshot));
82+
switch (entry.itemType) {
83+
case 'sub':
84+
return createSubscriptionItem(snapshot);
85+
case 'col':
86+
return createCollectionItem(snapshot);
87+
case 'file':
88+
return createFileItem(snapshot);
89+
case 'artifact':
90+
return createArtifactItem(normalizeArtifactSnapshotForRestore(snapshot));
91+
case 'share':
92+
return createTokenItem(snapshot, {
93+
expiresIn: snapshot.expiresIn,
94+
});
95+
default:
96+
throw new RequestInvalidError(
97+
'INVALID_ARCHIVE_TYPE',
98+
`Unsupported archive item type: ${entry.itemType}`,
99+
);
100+
}
101+
}
102+
103+
function normalizeArtifactSnapshotForRestore(snapshot) {
104+
const nextSnapshot = {
105+
...snapshot,
106+
};
107+
delete nextSnapshot.updated;
108+
delete nextSnapshot.url;
109+
return nextSnapshot;
110+
}
111+
112+
export { restoreArchivedEntry };

backend/src/restful/artifacts.js

Lines changed: 85 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ResourceNotFoundError,
2020
} from '@/restful/errors';
2121
import Gist from '@/utils/gist';
22+
import { archiveArtifact } from '@/utils/archive';
2223

2324
export default function register($app) {
2425
// Initialization
@@ -136,32 +137,11 @@ async function getArtifact(req, res) {
136137
}
137138

138139
function createArtifact(req, res) {
139-
const artifact = req.body;
140-
if (!validateArtifactName(artifact.name)) {
141-
failed(
142-
res,
143-
new RequestInvalidError(
144-
'INVALID_ARTIFACT_NAME',
145-
`Artifact name ${artifact.name} is invalid.`,
146-
),
147-
);
148-
return;
149-
}
150-
151-
$.info(`正在创建远程配置:${artifact.name}`);
152-
const allArtifacts = $.read(ARTIFACTS_KEY);
153-
if (findByName(allArtifacts, artifact.name)) {
154-
failed(
155-
res,
156-
new RequestInvalidError(
157-
'DUPLICATE_KEY',
158-
`Artifact ${artifact.name} already exists.`,
159-
),
160-
);
161-
} else {
162-
insertByPosition(allArtifacts, artifact, getCreateItemPosition());
163-
$.write(allArtifacts, ARTIFACTS_KEY);
140+
try {
141+
const artifact = createArtifactItem(req.body);
164142
success(res, artifact, 201);
143+
} catch (error) {
144+
failed(res, error);
165145
}
166146
}
167147

@@ -202,44 +182,27 @@ function updateArtifact(req, res) {
202182
}
203183

204184
async function deleteArtifact(req, res) {
205-
let { name } = req.params;
206-
$.info(`正在删除远程配置:${name}`);
207-
const allArtifacts = $.read(ARTIFACTS_KEY);
208185
try {
209-
const artifact = findByName(allArtifacts, name);
210-
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
211-
if (artifact.updated) {
212-
// delete gist
213-
const files = {};
214-
files[encodeURIComponent(artifact.name)] = {
215-
content: '',
216-
};
217-
if (encodeURIComponent(artifact.name) !== artifact.name) {
218-
files[artifact.name] = {
219-
content: '',
220-
};
221-
}
222-
223-
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
224-
try {
225-
await syncToGist(files);
226-
} catch (i) {
227-
$.error(`Function syncToGist: ${name} : ${i}`);
228-
}
186+
let { name } = req.params;
187+
$.info(`正在删除远程配置:${name}`);
188+
if (shouldArchiveDeletion(req.query.mode)) {
189+
archiveArtifact(name);
229190
}
230-
// delete local cache
231-
deleteByName(allArtifacts, name);
232-
$.write(allArtifacts, ARTIFACTS_KEY);
191+
await deleteArtifactItem(name);
233192
success(res);
234193
} catch (err) {
235-
$.error(`无法删除远程配置:${name},原因:${err}`);
194+
$.error(`无法删除远程配置:${req.params.name},原因:${err}`);
236195
failed(
237196
res,
238-
new InternalServerError(
239-
`FAILED_TO_DELETE_ARTIFACT`,
240-
`Failed to delete artifact ${name}`,
241-
`Reason: ${err}`,
242-
),
197+
err instanceof InternalServerError ||
198+
err instanceof RequestInvalidError ||
199+
err instanceof ResourceNotFoundError
200+
? err
201+
: new InternalServerError(
202+
`FAILED_TO_DELETE_ARTIFACT`,
203+
`Failed to delete artifact ${req.params.name}`,
204+
`Reason: ${err}`,
205+
),
243206
);
244207
}
245208
}
@@ -248,6 +211,70 @@ function validateArtifactName(name) {
248211
return /^[a-zA-Z0-9._-]*$/.test(name);
249212
}
250213

214+
function createArtifactItem(artifact) {
215+
if (!validateArtifactName(artifact.name)) {
216+
throw new RequestInvalidError(
217+
'INVALID_ARTIFACT_NAME',
218+
`Artifact name ${artifact.name} is invalid.`,
219+
);
220+
}
221+
222+
$.info(`正在创建远程配置:${artifact.name}`);
223+
const allArtifacts = $.read(ARTIFACTS_KEY);
224+
if (findByName(allArtifacts, artifact.name)) {
225+
throw new RequestInvalidError(
226+
'DUPLICATE_KEY',
227+
`Artifact ${artifact.name} already exists.`,
228+
);
229+
}
230+
insertByPosition(allArtifacts, artifact, getCreateItemPosition());
231+
$.write(allArtifacts, ARTIFACTS_KEY);
232+
return artifact;
233+
}
234+
235+
async function deleteArtifactItem(name) {
236+
const allArtifacts = $.read(ARTIFACTS_KEY);
237+
const artifact = findByName(allArtifacts, name);
238+
if (!artifact) {
239+
throw new ResourceNotFoundError(
240+
'RESOURCE_NOT_FOUND',
241+
`Artifact ${name} does not exist!`,
242+
);
243+
}
244+
if (artifact.updated) {
245+
const files = {};
246+
files[encodeURIComponent(artifact.name)] = {
247+
content: '',
248+
};
249+
if (encodeURIComponent(artifact.name) !== artifact.name) {
250+
files[artifact.name] = {
251+
content: '',
252+
};
253+
}
254+
try {
255+
await syncToGist(files);
256+
} catch (error) {
257+
$.error(`Function syncToGist: ${name} : ${error}`);
258+
}
259+
}
260+
deleteByName(allArtifacts, name);
261+
$.write(allArtifacts, ARTIFACTS_KEY);
262+
return artifact;
263+
}
264+
265+
function shouldArchiveDeletion(mode) {
266+
if (mode == null || mode === '' || mode === 'permanent') {
267+
return false;
268+
}
269+
if (mode === 'archive') {
270+
return true;
271+
}
272+
throw new RequestInvalidError(
273+
'INVALID_DELETE_MODE',
274+
`Unsupported delete mode: ${mode}`,
275+
);
276+
}
277+
251278
async function syncToGist(files) {
252279
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
253280
if (!gistToken) {
@@ -280,3 +307,4 @@ async function syncToGist(files) {
280307
}
281308

282309
export { syncToGist };
310+
export { createArtifactItem, deleteArtifactItem };

0 commit comments

Comments
 (0)