Skip to content

Commit 547e0ab

Browse files
committed
feat: 增加同步配置分批上传与失败后续批次继续机制
- 将 artifact 同步上传流程改为分批执行,新增 artifactSyncBatchSize 配置项(默认 10,可自定义),避免一次上传量过大 - 提取并复用 Gist 响应日志打印与文件链接解析逻辑(兼容 GitHub/GitLab),减少重复代码 - cron 同步和手动同步都改为调用统一批量上传能力,批次失败时仍继续下一批,提升同步稳定性 - 上传成功/失败数量改为按实际成功上传批次统计,并保留失败明细 - 新增/扩展测试:batch size 归一化、批次上传失败后还能继续处理后续批次
1 parent 000b895 commit 547e0ab

6 files changed

Lines changed: 248 additions & 119 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.22.29",
3+
"version": "2.23.0",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

backend/src/products/cron-sync-artifacts.js

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import {
66
COLLECTIONS_KEY,
77
} from '@/constants';
88
import $ from '@/core/app';
9-
import { produceArtifact } from '@/restful/sync';
10-
import { syncToGist } from '@/restful/artifacts';
9+
import { produceArtifact, uploadArtifactBatches } from '@/restful/sync';
1110
import { findByName } from '@/utils/database';
1211
import { hasCronArtifactSyncCredentials } from '@/products/cron-sync-artifacts-eligibility';
1312

@@ -213,56 +212,20 @@ async function doSync(arg = {}) {
213212
);
214213
}
215214

216-
const resp = await syncToGist(files);
217-
const body = JSON.parse(resp.body);
218-
delete body.history;
219-
delete body.forks;
220-
delete body.owner;
221-
Object.values(body.files).forEach((file) => {
222-
delete file.content;
215+
const uploaded = await uploadArtifactBatches({
216+
allArtifacts,
217+
files,
218+
valid,
219+
invalid,
223220
});
224-
$.info('上传配置响应:');
225-
$.info(JSON.stringify(body, null, 2));
226-
227-
for (const artifact of allArtifacts) {
228-
if (
229-
artifact.sync &&
230-
artifact.source &&
231-
valid.includes(artifact.name)
232-
) {
233-
artifact.updated = new Date().getTime();
234-
// extract real url from gist
235-
let files = body.files;
236-
let isGitLab;
237-
if (Array.isArray(files)) {
238-
isGitLab = true;
239-
files = Object.fromEntries(
240-
files.map((item) => [item.path, item]),
241-
);
242-
}
243-
const raw_url =
244-
files[encodeURIComponent(artifact.name)]?.raw_url;
245-
const new_url = isGitLab
246-
? raw_url
247-
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
248-
$.info(
249-
`上传配置完成\n文件列表: ${Object.keys(files).join(
250-
', ',
251-
)}\n当前文件: ${encodeURIComponent(
252-
artifact.name,
253-
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
254-
);
255-
artifact.url = new_url;
256-
}
257-
}
258221

259222
$.write(allArtifacts, ARTIFACTS_KEY);
260223
$.info('上传配置成功');
261224

262225
if (invalid.length > 0) {
263226
$.notify(
264227
'🌍 Sub-Store',
265-
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
228+
`同步配置成功 ${uploaded.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
266229
);
267230
} else if (syncSuccessNotify) {
268231
$.notify('🌍 Sub-Store', '同步配置完成');

backend/src/restful/artifacts.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ const ARTIFACT_GIST_PLACEHOLDER_CONTENT = [
2626
'Sub-Store placeholder',
2727
'This file keeps the Gist alive when all sync configuration files are deleted.',
2828
].join('\n');
29+
const DEFAULT_ARTIFACT_SYNC_BATCH_SIZE = 10;
30+
31+
function normalizeArtifactSyncBatchSize(value) {
32+
const batchSize = Math.floor(Number(value));
33+
34+
if (!isFinite(batchSize) || batchSize <= 0) {
35+
return DEFAULT_ARTIFACT_SYNC_BATCH_SIZE;
36+
}
37+
38+
return batchSize;
39+
}
2940

3041
export default function register($app) {
3142
// Initialization
@@ -411,5 +422,5 @@ function formatBytes(size) {
411422
return `${(size / 1024 / 1024).toFixed(1)} MB`;
412423
}
413424

414-
export { syncToGist };
425+
export { syncToGist, normalizeArtifactSyncBatchSize };
415426
export { createArtifactItem, deleteArtifactItem };

backend/src/restful/settings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ async function updateSettings(req, res) {
6868
[
6969
'defaultTimeout',
7070
'githubApiTimeout',
71+
'artifactSyncBatchSize',
7172
'cacheThreshold',
7273
'resourceCacheTtl',
7374
'headersCacheTtl',

backend/src/restful/sync.js

Lines changed: 131 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import {
55
RULES_KEY,
66
SUBS_KEY,
77
FILES_KEY,
8+
SETTINGS_KEY,
89
} from '@/constants';
910
import { failed, success } from '@/restful/response';
1011
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
1112
import { findByName } from '@/utils/database';
1213
import download from '@/utils/download';
1314
import { ProxyUtils } from '@/core/proxy-utils';
1415
import { RuleUtils } from '@/core/rule-utils';
15-
import { syncToGist } from '@/restful/artifacts';
16+
import {
17+
normalizeArtifactSyncBatchSize,
18+
syncToGist,
19+
} from '@/restful/artifacts';
1620
import {
1721
buildEmptySubscriptionOutput,
1822
handleIgnoreFailedRemoteSubError,
@@ -701,6 +705,122 @@ async function produceArtifact({
701705
}
702706
}
703707

708+
function createArtifactUploadBatches(names, batchSize) {
709+
const batches = [];
710+
for (let index = 0; index < names.length; index += batchSize) {
711+
batches.push(names.slice(index, index + batchSize));
712+
}
713+
return batches;
714+
}
715+
716+
function normalizeUploadResponseFiles(files) {
717+
if (Array.isArray(files)) {
718+
return {
719+
isGitLab: true,
720+
files: Object.fromEntries(files.map((item) => [item.path, item])),
721+
};
722+
}
723+
724+
return {
725+
isGitLab: false,
726+
files: files || {},
727+
};
728+
}
729+
730+
function logUploadResponse(body) {
731+
delete body.history;
732+
delete body.forks;
733+
delete body.owner;
734+
if (body.files) {
735+
Object.values(body.files).forEach((file) => {
736+
delete file.content;
737+
});
738+
}
739+
$.info('上传配置响应:');
740+
$.info(JSON.stringify(body, null, 2));
741+
}
742+
743+
function resolveArtifactUploadUrl(body, artifactName) {
744+
const { files, isGitLab } = normalizeUploadResponseFiles(body.files);
745+
const encodedName = encodeURIComponent(artifactName);
746+
const raw_url = files[encodedName]?.raw_url;
747+
const new_url = isGitLab
748+
? raw_url
749+
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
750+
$.info(
751+
`上传配置完成\n文件列表: ${Object.keys(files).join(
752+
', ',
753+
)}\n当前文件: ${encodedName}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
754+
);
755+
return new_url;
756+
}
757+
758+
async function uploadArtifactBatches({ allArtifacts, files, valid, invalid }) {
759+
const settings = $.read(SETTINGS_KEY) || {};
760+
const batchSize = normalizeArtifactSyncBatchSize(
761+
settings.artifactSyncBatchSize,
762+
);
763+
const batches = createArtifactUploadBatches(valid, batchSize);
764+
const uploaded = [];
765+
766+
$.info(
767+
`准备分批上传同步配置: 共 ${valid.length} 个, 每批 ${batchSize} 个, 批次数 ${batches.length}`,
768+
);
769+
770+
for (let index = 0; index < batches.length; index++) {
771+
const batchNames = batches[index];
772+
const batchFiles = Object.fromEntries(
773+
batchNames.map((name) => [
774+
encodeURIComponent(name),
775+
files[encodeURIComponent(name)],
776+
]),
777+
);
778+
779+
try {
780+
$.info(
781+
`正在上传第 ${index + 1}/${batches.length} 批同步配置: ${batchNames.join(
782+
', ',
783+
)}`,
784+
);
785+
const resp = await syncToGist(batchFiles);
786+
const body = JSON.parse(resp.body);
787+
logUploadResponse(body);
788+
789+
for (const artifact of allArtifacts) {
790+
if (
791+
artifact.sync &&
792+
artifact.source &&
793+
batchNames.includes(artifact.name)
794+
) {
795+
const newUrl = resolveArtifactUploadUrl(
796+
body,
797+
artifact.name,
798+
);
799+
if (newUrl) {
800+
artifact.updated = new Date().getTime();
801+
artifact.url = newUrl;
802+
uploaded.push(artifact.name);
803+
} else {
804+
$.error(
805+
`同步配置 ${artifact.name} 上传成功但响应中未找到文件链接`,
806+
);
807+
invalid.push(artifact.name);
808+
}
809+
}
810+
}
811+
} catch (e) {
812+
$.error(
813+
`第 ${index + 1}/${batches.length} 批同步配置上传失败: ${batchNames.join(
814+
', ',
815+
)}, 原因: ${e.message ?? e}`,
816+
);
817+
invalid.push(...batchNames);
818+
}
819+
}
820+
821+
return uploaded;
822+
}
823+
704824
async function syncArtifacts() {
705825
$.info('开始同步所有远程配置...');
706826
const allArtifacts = $.read(ARTIFACTS_KEY);
@@ -815,59 +935,22 @@ async function syncArtifacts() {
815935
);
816936
}
817937

818-
const resp = await syncToGist(files);
819-
const body = JSON.parse(resp.body);
820-
821-
delete body.history;
822-
delete body.forks;
823-
delete body.owner;
824-
Object.values(body.files).forEach((file) => {
825-
delete file.content;
938+
const uploaded = await uploadArtifactBatches({
939+
allArtifacts,
940+
files,
941+
valid,
942+
invalid,
826943
});
827-
$.info('上传配置响应:');
828-
$.info(JSON.stringify(body, null, 2));
829-
830-
for (const artifact of allArtifacts) {
831-
if (
832-
artifact.sync &&
833-
artifact.source &&
834-
valid.includes(artifact.name)
835-
) {
836-
artifact.updated = new Date().getTime();
837-
// extract real url from gist
838-
let files = body.files;
839-
let isGitLab;
840-
if (Array.isArray(files)) {
841-
isGitLab = true;
842-
files = Object.fromEntries(
843-
files.map((item) => [item.path, item]),
844-
);
845-
}
846-
const raw_url =
847-
files[encodeURIComponent(artifact.name)]?.raw_url;
848-
const new_url = isGitLab
849-
? raw_url
850-
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
851-
$.info(
852-
`上传配置完成\n文件列表: ${Object.keys(files).join(
853-
', ',
854-
)}\n当前文件: ${encodeURIComponent(
855-
artifact.name,
856-
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
857-
);
858-
artifact.url = new_url;
859-
}
860-
}
861944

862945
$.write(allArtifacts, ARTIFACTS_KEY);
863946
$.info('上传配置成功');
864947

865948
if (invalid.length > 0) {
866949
throw new Error(
867-
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
950+
`同步配置成功 ${uploaded.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
868951
);
869952
} else {
870-
$.info(`同步配置成功 ${valid.length} 个`);
953+
$.info(`同步配置成功 ${uploaded.length} 个`);
871954
}
872955
} catch (e) {
873956
$.error(`同步配置失败,原因:${e.message ?? e}`);
@@ -958,32 +1041,8 @@ async function syncArtifact(req, res) {
9581041
artifact.updated = new Date().getTime();
9591042
const body = JSON.parse(resp.body);
9601043

961-
delete body.history;
962-
delete body.forks;
963-
delete body.owner;
964-
Object.values(body.files).forEach((file) => {
965-
delete file.content;
966-
});
967-
$.info('上传配置响应:');
968-
$.info(JSON.stringify(body, null, 2));
969-
970-
let files = body.files;
971-
let isGitLab;
972-
if (Array.isArray(files)) {
973-
isGitLab = true;
974-
files = Object.fromEntries(files.map((item) => [item.path, item]));
975-
}
976-
const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url;
977-
const new_url = isGitLab
978-
? raw_url
979-
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
980-
$.info(
981-
`上传配置完成\n文件列表: ${Object.keys(files).join(
982-
', ',
983-
)}\n当前文件: ${encodeURIComponent(
984-
artifact.name,
985-
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
986-
);
1044+
logUploadResponse(body);
1045+
const new_url = resolveArtifactUploadUrl(body, artifact.name);
9871046
artifact.url = new_url;
9881047
$.write(allArtifacts, ARTIFACTS_KEY);
9891048
success(res, artifact);
@@ -1000,4 +1059,4 @@ async function syncArtifact(req, res) {
10001059
}
10011060
}
10021061

1003-
export { produceArtifact, syncArtifacts };
1062+
export { produceArtifact, syncArtifacts, uploadArtifactBatches };

0 commit comments

Comments
 (0)