karin-plugin-qgroup-file2openlist 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/config/config.json +3 -0
- package/lib/apps/groupFiles.backup.js +3 -3
- package/lib/apps/groupFiles.js +3 -3
- package/lib/apps/groupSyncConfig.js +4 -4
- package/lib/apps/opCommands.js +2 -2
- package/lib/apps/ownerUi.js +2 -2
- package/lib/apps/scheduler.js +5 -5
- package/lib/{chunk-WJNM5RHT.js → chunk-5QW7XYQX.js} +1 -1
- package/lib/{chunk-BU2GD6GJ.js → chunk-BWVJMEKK.js} +2 -2
- package/lib/{chunk-KFOQIZ6H.js → chunk-I2I6ZPJU.js} +2 -2
- package/lib/{chunk-TL3HO4ZL.js → chunk-WQFR5LF3.js} +1 -1
- package/lib/{chunk-N5HMQFRM.js → chunk-YVFRUAZO.js} +233 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
# Karin
|
|
1
|
+
# Karin Oplist Plugin
|
|
2
|
+
|
|
3
|
+
省流:群文件自动备份、网盘自动备份
|
|
4
|
+
```
|
|
5
|
+
pnpm add karin-plugin-qgroup-file2openlist -w
|
|
6
|
+
pnpm install
|
|
7
|
+
```
|
|
2
8
|
|
|
3
9
|
## 📖 目录
|
|
4
10
|
|
|
@@ -44,6 +50,7 @@ TypeScript 插件开发流程现在更加简单,无需手动克隆模板仓库
|
|
|
44
50
|
- `openlistBaseUrl`:例如 `http://127.0.0.1:5244`
|
|
45
51
|
- `openlistUsername` / `openlistPassword`:用于 WebDAV BasicAuth 登录
|
|
46
52
|
- `openlistTargetDir`:目标目录(例:`/挂载目录/QQ群文件`)
|
|
53
|
+
- `resourceLimits.transferConcurrency`:全局传输并发上限(所有下载+上传共享;生产环境建议 1,避免同时传输吃满内存;<=0 不限制)
|
|
47
54
|
- `groupSyncDefaults`:群同步默认策略(并发、单文件超时、重试等)
|
|
48
55
|
- `groupSyncTargets`:同步对象群配置(每群可单独覆盖目录/并发/同步时段等)
|
|
49
56
|
|
package/config/config.json
CHANGED
|
@@ -6,12 +6,12 @@ import "../chunk-PBBZ5KAD.js";
|
|
|
6
6
|
import "../chunk-QB3GSENE.js";
|
|
7
7
|
import {
|
|
8
8
|
handleGroupFileUploadedAutoBackup
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import "../chunk-
|
|
9
|
+
} from "../chunk-BWVJMEKK.js";
|
|
10
|
+
import "../chunk-5QW7XYQX.js";
|
|
11
11
|
import {
|
|
12
12
|
backupOpenListToOpenListCore,
|
|
13
13
|
formatErrorMessage
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-YVFRUAZO.js";
|
|
15
15
|
import {
|
|
16
16
|
config
|
|
17
17
|
} from "../chunk-DA4U55JC.js";
|
package/lib/apps/groupFiles.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
exportGroupFilesToDisk,
|
|
3
3
|
syncGroupFilesToOpenListCore
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-BWVJMEKK.js";
|
|
5
|
+
import "../chunk-5QW7XYQX.js";
|
|
6
6
|
import {
|
|
7
7
|
formatErrorMessage,
|
|
8
8
|
normalizePosixPath
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-YVFRUAZO.js";
|
|
10
10
|
import {
|
|
11
11
|
config
|
|
12
12
|
} from "../chunk-DA4U55JC.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleGroupSyncConfigCommand
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-I2I6ZPJU.js";
|
|
4
|
+
import "../chunk-BWVJMEKK.js";
|
|
5
|
+
import "../chunk-5QW7XYQX.js";
|
|
6
|
+
import "../chunk-YVFRUAZO.js";
|
|
7
7
|
import "../chunk-DA4U55JC.js";
|
|
8
8
|
import "../chunk-IZS467MR.js";
|
|
9
9
|
|
package/lib/apps/opCommands.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
resolveOpltMapping,
|
|
6
6
|
withOpltUser,
|
|
7
7
|
writeOpltData
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-WQFR5LF3.js";
|
|
9
9
|
import {
|
|
10
10
|
buildActionCard,
|
|
11
11
|
replyImages
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
normalizePosixPath,
|
|
21
21
|
readJsonSafe,
|
|
22
22
|
writeJsonSafe
|
|
23
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-YVFRUAZO.js";
|
|
24
24
|
import {
|
|
25
25
|
config
|
|
26
26
|
} from "../chunk-DA4U55JC.js";
|
package/lib/apps/ownerUi.js
CHANGED
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
import "../chunk-QB3GSENE.js";
|
|
5
5
|
import {
|
|
6
6
|
readGroupSyncState
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-5QW7XYQX.js";
|
|
8
8
|
import {
|
|
9
9
|
backupOpenListToOpenListCore,
|
|
10
10
|
formatErrorMessage,
|
|
11
11
|
normalizePosixPath,
|
|
12
12
|
readJsonSafe,
|
|
13
13
|
writeJsonSafe
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-YVFRUAZO.js";
|
|
15
15
|
import "../chunk-DA4U55JC.js";
|
|
16
16
|
import {
|
|
17
17
|
dir
|
package/lib/apps/scheduler.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runNightlyOpltBackup
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-WQFR5LF3.js";
|
|
4
4
|
import {
|
|
5
5
|
runNightlyGroupBackup
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-I2I6ZPJU.js";
|
|
7
|
+
import "../chunk-BWVJMEKK.js";
|
|
8
|
+
import "../chunk-5QW7XYQX.js";
|
|
9
|
+
import "../chunk-YVFRUAZO.js";
|
|
10
10
|
import {
|
|
11
11
|
config
|
|
12
12
|
} from "../chunk-DA4U55JC.js";
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
readGroupSyncState,
|
|
3
3
|
withGroupSyncLock,
|
|
4
4
|
writeGroupSyncState
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-5QW7XYQX.js";
|
|
6
6
|
import {
|
|
7
7
|
buildOpenListAuthHeader,
|
|
8
8
|
buildOpenListDavBaseUrl,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
runWithConcurrency,
|
|
17
17
|
safePathSegment,
|
|
18
18
|
webdavPropfindListNames
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-YVFRUAZO.js";
|
|
20
20
|
import {
|
|
21
21
|
config,
|
|
22
22
|
time
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
syncGroupFilesToOpenListCore
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BWVJMEKK.js";
|
|
4
4
|
import {
|
|
5
5
|
normalizePosixPath,
|
|
6
6
|
readJsonSafe,
|
|
7
7
|
writeJsonSafe
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-YVFRUAZO.js";
|
|
9
9
|
import {
|
|
10
10
|
config
|
|
11
11
|
} from "./chunk-DA4U55JC.js";
|
|
@@ -51,7 +51,7 @@ var fetchTextSafely = async (res, maxLen = 500) => {
|
|
|
51
51
|
|
|
52
52
|
// src/model/openlist/backup.ts
|
|
53
53
|
import path from "path";
|
|
54
|
-
import { logger } from "node-karin";
|
|
54
|
+
import { logger as logger2 } from "node-karin";
|
|
55
55
|
|
|
56
56
|
// src/model/shared/concurrency.ts
|
|
57
57
|
var runWithConcurrency = async (items, concurrency, fn) => {
|
|
@@ -178,6 +178,76 @@ var buildOpenListAuthHeader = (username, password) => {
|
|
|
178
178
|
// src/model/openlist/api.ts
|
|
179
179
|
import { Readable } from "stream";
|
|
180
180
|
import { setTimeout as sleep } from "timers/promises";
|
|
181
|
+
|
|
182
|
+
// src/model/shared/transferLimiter.ts
|
|
183
|
+
import { logger } from "node-karin";
|
|
184
|
+
var active = 0;
|
|
185
|
+
var queue = [];
|
|
186
|
+
var lastQueueLogAt = 0;
|
|
187
|
+
var clampInt = (value, min, max) => Math.min(max, Math.max(min, Math.floor(value)));
|
|
188
|
+
var cachedLimit = 1;
|
|
189
|
+
var cachedAt = 0;
|
|
190
|
+
var CACHE_MS = 1e3;
|
|
191
|
+
var getGlobalTransferConcurrencyLimit = () => {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (now - cachedAt < CACHE_MS) return cachedLimit;
|
|
194
|
+
cachedAt = now;
|
|
195
|
+
try {
|
|
196
|
+
const cfg = config();
|
|
197
|
+
const raw = cfg?.resourceLimits?.transferConcurrency;
|
|
198
|
+
if (raw === void 0 || raw === null || raw === "") return cachedLimit = 1;
|
|
199
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
200
|
+
if (!Number.isFinite(n)) return cachedLimit = 1;
|
|
201
|
+
if (n <= 0) return cachedLimit = Number.POSITIVE_INFINITY;
|
|
202
|
+
return cachedLimit = clampInt(n, 1, 50);
|
|
203
|
+
} catch {
|
|
204
|
+
return cachedLimit = 1;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var logQueue = () => {
|
|
208
|
+
if (!queue.length) return;
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
if (now - lastQueueLogAt < 3e4) return;
|
|
211
|
+
lastQueueLogAt = now;
|
|
212
|
+
const limit = getGlobalTransferConcurrencyLimit();
|
|
213
|
+
const limitText = Number.isFinite(limit) ? String(limit) : "\u65E0\u9650\u5236";
|
|
214
|
+
const first = queue[0];
|
|
215
|
+
const waitedMs = first ? Math.max(0, now - first.enqueuedAt) : 0;
|
|
216
|
+
logger.warn(`[\u4F20\u8F93\u9650\u6D41] \u961F\u5217\u4E2D=${queue.length} \u8FD0\u884C\u4E2D=${active} \u4E0A\u9650=${limitText} \u6700\u65E9\u7B49\u5F85=${waitedMs}ms${first?.label ? ` label=${first.label}` : ""}`);
|
|
217
|
+
};
|
|
218
|
+
var drain = () => {
|
|
219
|
+
const limit = getGlobalTransferConcurrencyLimit();
|
|
220
|
+
while (active < limit && queue.length) {
|
|
221
|
+
active++;
|
|
222
|
+
queue.shift().resolve();
|
|
223
|
+
}
|
|
224
|
+
logQueue();
|
|
225
|
+
};
|
|
226
|
+
var acquire = async (label) => {
|
|
227
|
+
const limit = getGlobalTransferConcurrencyLimit();
|
|
228
|
+
if (active < limit && queue.length === 0) {
|
|
229
|
+
active++;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
await new Promise((resolve) => {
|
|
233
|
+
queue.push({ resolve, label, enqueuedAt: Date.now() });
|
|
234
|
+
drain();
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
var release = () => {
|
|
238
|
+
active = Math.max(0, active - 1);
|
|
239
|
+
drain();
|
|
240
|
+
};
|
|
241
|
+
var withGlobalTransferLimit = async (label, fn) => {
|
|
242
|
+
await acquire(label);
|
|
243
|
+
try {
|
|
244
|
+
return await fn();
|
|
245
|
+
} finally {
|
|
246
|
+
release();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// src/model/openlist/api.ts
|
|
181
251
|
var openlistApiReadJson = async (res) => {
|
|
182
252
|
try {
|
|
183
253
|
return await res.json();
|
|
@@ -374,62 +444,64 @@ var downloadAndUploadByOpenListApiPut = async (params) => {
|
|
|
374
444
|
const { sourceUrl, sourceHeaders, targetBaseUrl, targetToken, targetPath, timeoutMs } = params;
|
|
375
445
|
const apiBaseUrl = buildOpenListApiBaseUrl(targetBaseUrl);
|
|
376
446
|
if (!apiBaseUrl) throw new Error("\u76EE\u6807 OpenList API \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5\u76EE\u6807 OpenList \u5730\u5740");
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
let downloadRes;
|
|
447
|
+
await withGlobalTransferLimit(`downloadAndUploadByOpenListApiPut:${targetPath}`, async () => {
|
|
448
|
+
const controller = new AbortController();
|
|
449
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
381
450
|
try {
|
|
382
|
-
downloadRes
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
"
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
putRes
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
451
|
+
let downloadRes;
|
|
452
|
+
try {
|
|
453
|
+
downloadRes = await fetch(sourceUrl, {
|
|
454
|
+
headers: sourceHeaders,
|
|
455
|
+
redirect: "follow",
|
|
456
|
+
signal: controller.signal
|
|
457
|
+
});
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (isAbortError(error)) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u8D85\u65F6");
|
|
460
|
+
throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
461
|
+
}
|
|
462
|
+
if (!downloadRes.ok) {
|
|
463
|
+
const body = await fetchTextSafely(downloadRes);
|
|
464
|
+
throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${body ? ` - ${body}` : ""}`);
|
|
465
|
+
}
|
|
466
|
+
if (!downloadRes.body) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
467
|
+
const headers = {
|
|
468
|
+
"File-Path": encodeURIComponent(normalizePosixPath(targetPath))
|
|
469
|
+
};
|
|
470
|
+
const authToken = String(targetToken ?? "").trim();
|
|
471
|
+
if (authToken) headers.Authorization = authToken;
|
|
472
|
+
const contentType = downloadRes.headers.get("content-type");
|
|
473
|
+
const contentLength = downloadRes.headers.get("content-length");
|
|
474
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
475
|
+
if (contentLength) headers["Content-Length"] = contentLength;
|
|
476
|
+
const sourceStream = Readable.fromWeb(downloadRes.body);
|
|
477
|
+
let putRes;
|
|
478
|
+
try {
|
|
479
|
+
putRes = await fetch(`${apiBaseUrl}/fs/put`, {
|
|
480
|
+
method: "PUT",
|
|
481
|
+
headers,
|
|
482
|
+
body: sourceStream,
|
|
483
|
+
// @ts-expect-error Node fetch streaming body requires duplex
|
|
484
|
+
duplex: "half",
|
|
485
|
+
redirect: "follow",
|
|
486
|
+
signal: controller.signal
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (isAbortError(error)) throw new Error("\u76EE\u6807\u7AEF\u5199\u5165\u8D85\u65F6\uFF08\u8BF7\u68C0\u67E5\u5BF9\u7AEF OpenList \u8FDE\u63A5/\u6743\u9650\uFF09");
|
|
490
|
+
throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
491
|
+
}
|
|
492
|
+
if (!putRes.ok) {
|
|
493
|
+
const body = await fetchTextSafely(putRes);
|
|
494
|
+
throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${putRes.status} ${putRes.statusText}${body ? ` - ${body}` : ""}`);
|
|
495
|
+
}
|
|
496
|
+
const json = await openlistApiReadJson(putRes);
|
|
497
|
+
if (typeof json?.code === "number" && json.code !== 200) {
|
|
498
|
+
const msg = json?.message ? ` - ${json.message}` : "";
|
|
499
|
+
throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: code=${json.code}${msg}`);
|
|
500
|
+
}
|
|
501
|
+
} finally {
|
|
502
|
+
clearTimeout(timer);
|
|
429
503
|
}
|
|
430
|
-
}
|
|
431
|
-
clearTimeout(timer);
|
|
432
|
-
}
|
|
504
|
+
});
|
|
433
505
|
};
|
|
434
506
|
|
|
435
507
|
// src/model/openlist/webdav.ts
|
|
@@ -588,59 +660,61 @@ var webdavHeadExists = async (params) => {
|
|
|
588
660
|
};
|
|
589
661
|
var copyWebDavToWebDav = async (params) => {
|
|
590
662
|
const { sourceDavBaseUrl, sourceAuth, sourcePath, targetDavBaseUrl, targetAuth, targetPath, timeoutMs } = params;
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const sourceUrl = `${sourceDavBaseUrl}${encodePathForUrl(sourcePath)}`;
|
|
595
|
-
const targetUrl = `${targetDavBaseUrl}${encodePathForUrl(targetPath)}`;
|
|
596
|
-
let downloadRes;
|
|
597
|
-
try {
|
|
598
|
-
const downloadHeaders = {};
|
|
599
|
-
const authHeader = String(sourceAuth ?? "").trim();
|
|
600
|
-
if (authHeader) downloadHeaders.Authorization = authHeader;
|
|
601
|
-
downloadRes = await fetch(sourceUrl, {
|
|
602
|
-
method: "GET",
|
|
603
|
-
headers: downloadHeaders,
|
|
604
|
-
redirect: "follow",
|
|
605
|
-
signal: controller.signal
|
|
606
|
-
});
|
|
607
|
-
} catch (error) {
|
|
608
|
-
if (isAbortError(error)) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u8D85\u65F6");
|
|
609
|
-
throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
610
|
-
}
|
|
611
|
-
if (!downloadRes.ok) {
|
|
612
|
-
const body = await fetchTextSafely(downloadRes);
|
|
613
|
-
throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${body ? ` - ${body}` : ""}`);
|
|
614
|
-
}
|
|
615
|
-
if (!downloadRes.body) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
616
|
-
const headers = { Authorization: targetAuth };
|
|
617
|
-
const contentType = downloadRes.headers.get("content-type");
|
|
618
|
-
const contentLength = downloadRes.headers.get("content-length");
|
|
619
|
-
if (contentType) headers["Content-Type"] = contentType;
|
|
620
|
-
if (contentLength) headers["Content-Length"] = contentLength;
|
|
621
|
-
const sourceStream = Readable2.fromWeb(downloadRes.body);
|
|
622
|
-
let putRes;
|
|
663
|
+
await withGlobalTransferLimit(`copyWebDavToWebDav:${sourcePath}=>${targetPath}`, async () => {
|
|
664
|
+
const controller = new AbortController();
|
|
665
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
623
666
|
try {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
667
|
+
const sourceUrl = `${sourceDavBaseUrl}${encodePathForUrl(sourcePath)}`;
|
|
668
|
+
const targetUrl = `${targetDavBaseUrl}${encodePathForUrl(targetPath)}`;
|
|
669
|
+
let downloadRes;
|
|
670
|
+
try {
|
|
671
|
+
const downloadHeaders = {};
|
|
672
|
+
const authHeader = String(sourceAuth ?? "").trim();
|
|
673
|
+
if (authHeader) downloadHeaders.Authorization = authHeader;
|
|
674
|
+
downloadRes = await fetch(sourceUrl, {
|
|
675
|
+
method: "GET",
|
|
676
|
+
headers: downloadHeaders,
|
|
677
|
+
redirect: "follow",
|
|
678
|
+
signal: controller.signal
|
|
679
|
+
});
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (isAbortError(error)) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u8D85\u65F6");
|
|
682
|
+
throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
683
|
+
}
|
|
684
|
+
if (!downloadRes.ok) {
|
|
685
|
+
const body = await fetchTextSafely(downloadRes);
|
|
686
|
+
throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${body ? ` - ${body}` : ""}`);
|
|
687
|
+
}
|
|
688
|
+
if (!downloadRes.body) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
689
|
+
const headers = { Authorization: targetAuth };
|
|
690
|
+
const contentType = downloadRes.headers.get("content-type");
|
|
691
|
+
const contentLength = downloadRes.headers.get("content-length");
|
|
692
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
693
|
+
if (contentLength) headers["Content-Length"] = contentLength;
|
|
694
|
+
const sourceStream = Readable2.fromWeb(downloadRes.body);
|
|
695
|
+
let putRes;
|
|
696
|
+
try {
|
|
697
|
+
putRes = await fetch(targetUrl, {
|
|
698
|
+
method: "PUT",
|
|
699
|
+
headers,
|
|
700
|
+
body: sourceStream,
|
|
701
|
+
// @ts-expect-error Node fetch streaming body requires duplex
|
|
702
|
+
duplex: "half",
|
|
703
|
+
redirect: "follow",
|
|
704
|
+
signal: controller.signal
|
|
705
|
+
});
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (isAbortError(error)) throw new Error("\u76EE\u6807\u7AEF\u5199\u5165\u8D85\u65F6\uFF08\u8BF7\u68C0\u67E5\u5BF9\u7AEF OpenList \u8FDE\u63A5/\u6743\u9650\uFF09");
|
|
708
|
+
throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
709
|
+
}
|
|
710
|
+
if (!putRes.ok) {
|
|
711
|
+
const body = await fetchTextSafely(putRes);
|
|
712
|
+
throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${putRes.status} ${putRes.statusText}${body ? ` - ${body}` : ""}`);
|
|
713
|
+
}
|
|
714
|
+
} finally {
|
|
715
|
+
clearTimeout(timer);
|
|
640
716
|
}
|
|
641
|
-
}
|
|
642
|
-
clearTimeout(timer);
|
|
643
|
-
}
|
|
717
|
+
});
|
|
644
718
|
};
|
|
645
719
|
var createWebDavDirEnsurer = (davBaseUrl, auth, timeoutMs) => {
|
|
646
720
|
const ensured = /* @__PURE__ */ new Map();
|
|
@@ -695,58 +769,60 @@ var createWebDavDirEnsurer = (davBaseUrl, auth, timeoutMs) => {
|
|
|
695
769
|
};
|
|
696
770
|
var downloadAndUploadByWebDav = async (params) => {
|
|
697
771
|
const { sourceUrl, sourceHeaders, targetUrl, auth, timeoutMs, rateLimitBytesPerSec } = params;
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
let downloadRes;
|
|
772
|
+
await withGlobalTransferLimit(`downloadAndUploadByWebDav:${targetUrl}`, async () => {
|
|
773
|
+
const controller = new AbortController();
|
|
774
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
702
775
|
try {
|
|
703
|
-
downloadRes
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
putRes
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
776
|
+
let downloadRes;
|
|
777
|
+
try {
|
|
778
|
+
downloadRes = await fetch(sourceUrl, {
|
|
779
|
+
headers: sourceHeaders,
|
|
780
|
+
redirect: "follow",
|
|
781
|
+
signal: controller.signal
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
if (isAbortError(error)) throw new Error("\u4E0B\u8F7D\u8D85\u65F6");
|
|
785
|
+
throw new Error(`\u4E0B\u8F7D\u8BF7\u6C42\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
786
|
+
}
|
|
787
|
+
if (!downloadRes.ok) {
|
|
788
|
+
const body = await fetchTextSafely(downloadRes);
|
|
789
|
+
throw new Error(`\u4E0B\u8F7D\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${body ? ` - ${body}` : ""}`);
|
|
790
|
+
}
|
|
791
|
+
if (!downloadRes.body) throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
792
|
+
const headers = {
|
|
793
|
+
Authorization: auth
|
|
794
|
+
};
|
|
795
|
+
const contentType = downloadRes.headers.get("content-type");
|
|
796
|
+
const contentLength = downloadRes.headers.get("content-length");
|
|
797
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
798
|
+
if (contentLength) headers["Content-Length"] = contentLength;
|
|
799
|
+
let bodyStream = Readable2.fromWeb(downloadRes.body);
|
|
800
|
+
const throttle = createThrottleTransform(rateLimitBytesPerSec || 0);
|
|
801
|
+
if (throttle) bodyStream = bodyStream.pipe(throttle);
|
|
802
|
+
let putRes;
|
|
803
|
+
try {
|
|
804
|
+
putRes = await fetch(targetUrl, {
|
|
805
|
+
method: "PUT",
|
|
806
|
+
headers,
|
|
807
|
+
body: bodyStream,
|
|
808
|
+
// @ts-expect-error Node fetch streaming body requires duplex
|
|
809
|
+
duplex: "half",
|
|
810
|
+
redirect: "follow",
|
|
811
|
+
signal: controller.signal
|
|
812
|
+
});
|
|
813
|
+
} catch (error) {
|
|
814
|
+
if (isAbortError(error)) throw new Error("\u4E0A\u4F20\u8D85\u65F6\uFF08\u8BF7\u68C0\u67E5 OpenList \u8FDE\u63A5/\u6743\u9650\uFF09");
|
|
815
|
+
throw new Error(`\u4E0A\u4F20\u8BF7\u6C42\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
816
|
+
}
|
|
817
|
+
if (!putRes.ok) {
|
|
818
|
+
const body = await fetchTextSafely(putRes);
|
|
819
|
+
const hint = putRes.status === 401 ? "\uFF08\u8D26\u53F7/\u5BC6\u7801\u9519\u8BEF\uFF0C\u6216\u672A\u5F00\u542F WebDAV\uFF09" : putRes.status === 403 ? "\uFF08\u6CA1\u6709 WebDAV \u7BA1\u7406/\u5199\u5165\u6743\u9650\uFF0C\u6216\u76EE\u6807\u76EE\u5F55\u4E0D\u53EF\u5199/\u4E0D\u5728\u7528\u6237\u53EF\u8BBF\u95EE\u8303\u56F4\uFF09" : "";
|
|
820
|
+
throw new Error(`\u4E0A\u4F20\u5931\u8D25: ${putRes.status} ${putRes.statusText}${hint}${body ? ` - ${body}` : ""}`);
|
|
821
|
+
}
|
|
822
|
+
} finally {
|
|
823
|
+
clearTimeout(timer);
|
|
746
824
|
}
|
|
747
|
-
}
|
|
748
|
-
clearTimeout(timer);
|
|
749
|
-
}
|
|
825
|
+
});
|
|
750
826
|
};
|
|
751
827
|
|
|
752
828
|
// src/model/openlist/backup.ts
|
|
@@ -1153,7 +1229,7 @@ var backupOpenListToOpenListCore = async (params) => {
|
|
|
1153
1229
|
ok++;
|
|
1154
1230
|
} catch (error) {
|
|
1155
1231
|
fail++;
|
|
1156
|
-
|
|
1232
|
+
logger2.error(error);
|
|
1157
1233
|
if (allowAutoFallback) {
|
|
1158
1234
|
const msg = formatErrorMessage(error);
|
|
1159
1235
|
if (targetTransport === "webdav" && /401|403|MKCOL|PUT/i.test(msg)) targetTransport = "api";
|