karin-plugin-qgroup-file2openlist 0.0.26 → 0.0.28
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 +1 -0
- package/config/config.json +2 -1
- 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-5QW7XYQX.js → chunk-AAKFRFSO.js} +1 -1
- package/lib/{chunk-WQFR5LF3.js → chunk-GAVQ23XT.js} +1 -1
- package/lib/{chunk-I2I6ZPJU.js → chunk-H2TAMQ7I.js} +2 -2
- package/lib/{chunk-YVFRUAZO.js → chunk-HSFFULJR.js} +380 -225
- package/lib/{chunk-BWVJMEKK.js → chunk-ZXZMBJN3.js} +22 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ TypeScript 插件开发流程现在更加简单,无需手动克隆模板仓库
|
|
|
51
51
|
- `openlistUsername` / `openlistPassword`:用于 WebDAV BasicAuth 登录
|
|
52
52
|
- `openlistTargetDir`:目标目录(例:`/挂载目录/QQ群文件`)
|
|
53
53
|
- `resourceLimits.transferConcurrency`:全局传输并发上限(所有下载+上传共享;生产环境建议 1,避免同时传输吃满内存;<=0 不限制)
|
|
54
|
+
- `resourceLimits.largeFileSpoolThresholdMB`:大文件落盘阈值(MB,默认 200;<=0 禁用;大文件会先下载到本地临时文件再上传,降低内存/缓冲压力)
|
|
54
55
|
- `groupSyncDefaults`:群同步默认策略(并发、单文件超时、重试等)
|
|
55
56
|
- `groupSyncTargets`:同步对象群配置(每群可单独覆盖目录/并发/同步时段等)
|
|
56
57
|
|
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-ZXZMBJN3.js";
|
|
10
|
+
import "../chunk-AAKFRFSO.js";
|
|
11
11
|
import {
|
|
12
12
|
backupOpenListToOpenListCore,
|
|
13
13
|
formatErrorMessage
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-HSFFULJR.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-ZXZMBJN3.js";
|
|
5
|
+
import "../chunk-AAKFRFSO.js";
|
|
6
6
|
import {
|
|
7
7
|
formatErrorMessage,
|
|
8
8
|
normalizePosixPath
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-HSFFULJR.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-H2TAMQ7I.js";
|
|
4
|
+
import "../chunk-ZXZMBJN3.js";
|
|
5
|
+
import "../chunk-AAKFRFSO.js";
|
|
6
|
+
import "../chunk-HSFFULJR.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-GAVQ23XT.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-HSFFULJR.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-AAKFRFSO.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-HSFFULJR.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-GAVQ23XT.js";
|
|
4
4
|
import {
|
|
5
5
|
runNightlyGroupBackup
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-H2TAMQ7I.js";
|
|
7
|
+
import "../chunk-ZXZMBJN3.js";
|
|
8
|
+
import "../chunk-AAKFRFSO.js";
|
|
9
|
+
import "../chunk-HSFFULJR.js";
|
|
10
10
|
import {
|
|
11
11
|
config
|
|
12
12
|
} from "../chunk-DA4U55JC.js";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
syncGroupFilesToOpenListCore
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZXZMBJN3.js";
|
|
4
4
|
import {
|
|
5
5
|
normalizePosixPath,
|
|
6
6
|
readJsonSafe,
|
|
7
7
|
writeJsonSafe
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-HSFFULJR.js";
|
|
9
9
|
import {
|
|
10
10
|
config
|
|
11
11
|
} from "./chunk-DA4U55JC.js";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
config
|
|
3
3
|
} from "./chunk-DA4U55JC.js";
|
|
4
|
+
import {
|
|
5
|
+
dir
|
|
6
|
+
} from "./chunk-IZS467MR.js";
|
|
4
7
|
|
|
5
8
|
// src/model/shared/path.ts
|
|
6
9
|
var normalizePosixPath = (inputPath, { ensureLeadingSlash = true, stripTrailingSlash = true } = {}) => {
|
|
@@ -40,18 +43,84 @@ var formatErrorMessage = (error) => {
|
|
|
40
43
|
var isAbortError = (error) => {
|
|
41
44
|
return Boolean(error && typeof error === "object" && "name" in error && error.name === "AbortError");
|
|
42
45
|
};
|
|
46
|
+
var drainResponseBody = async (res) => {
|
|
47
|
+
try {
|
|
48
|
+
const body = res.body;
|
|
49
|
+
if (!body) return;
|
|
50
|
+
const reader = body.getReader();
|
|
51
|
+
while (true) {
|
|
52
|
+
const { done } = await reader.read();
|
|
53
|
+
if (done) break;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
try {
|
|
57
|
+
await res.body?.cancel();
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
43
62
|
var fetchTextSafely = async (res, maxLen = 500) => {
|
|
44
63
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
64
|
+
const maxBytes = Math.max(0, Math.floor(maxLen) || 0);
|
|
65
|
+
if (!maxBytes) {
|
|
66
|
+
await res.body?.cancel();
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
const body = res.body;
|
|
70
|
+
if (!body) return "";
|
|
71
|
+
const reader = body.getReader();
|
|
72
|
+
const chunks = [];
|
|
73
|
+
let total = 0;
|
|
74
|
+
while (total < maxBytes) {
|
|
75
|
+
const { value, done } = await reader.read();
|
|
76
|
+
if (done) break;
|
|
77
|
+
if (!value || value.byteLength <= 0) continue;
|
|
78
|
+
const remaining = maxBytes - total;
|
|
79
|
+
if (value.byteLength <= remaining) {
|
|
80
|
+
chunks.push(value);
|
|
81
|
+
total += value.byteLength;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
chunks.push(value.slice(0, remaining));
|
|
85
|
+
total += remaining;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await reader.cancel();
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
if (!chunks.length) return "";
|
|
93
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
|
|
47
94
|
} catch {
|
|
95
|
+
try {
|
|
96
|
+
await res.body?.cancel();
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
48
99
|
return "";
|
|
49
100
|
}
|
|
50
101
|
};
|
|
51
102
|
|
|
52
|
-
// src/model/
|
|
103
|
+
// src/model/shared/fsJson.ts
|
|
104
|
+
import fs from "fs";
|
|
53
105
|
import path from "path";
|
|
54
|
-
|
|
106
|
+
var ensureDir = (dirPath) => fs.mkdirSync(dirPath, { recursive: true });
|
|
107
|
+
var readJsonSafe = (filePath) => {
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(filePath)) return {};
|
|
110
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
111
|
+
return raw ? JSON.parse(raw) : {};
|
|
112
|
+
} catch {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var writeJsonSafe = (filePath, data) => {
|
|
117
|
+
ensureDir(path.dirname(filePath));
|
|
118
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/model/openlist/backup.ts
|
|
122
|
+
import path3 from "path";
|
|
123
|
+
import { logger as logger4 } from "node-karin";
|
|
55
124
|
|
|
56
125
|
// src/model/shared/concurrency.ts
|
|
57
126
|
var runWithConcurrency = async (items, concurrency, fn) => {
|
|
@@ -176,6 +245,7 @@ var buildOpenListAuthHeader = (username, password) => {
|
|
|
176
245
|
};
|
|
177
246
|
|
|
178
247
|
// src/model/openlist/api.ts
|
|
248
|
+
import fs3 from "fs";
|
|
179
249
|
import { Readable } from "stream";
|
|
180
250
|
import { setTimeout as sleep } from "timers/promises";
|
|
181
251
|
|
|
@@ -247,7 +317,85 @@ var withGlobalTransferLimit = async (label, fn) => {
|
|
|
247
317
|
}
|
|
248
318
|
};
|
|
249
319
|
|
|
320
|
+
// src/model/shared/transferSpool.ts
|
|
321
|
+
import fs2 from "fs";
|
|
322
|
+
import path2 from "path";
|
|
323
|
+
import { randomUUID } from "crypto";
|
|
324
|
+
import { pipeline } from "stream/promises";
|
|
325
|
+
var DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB = 200;
|
|
326
|
+
var MAX_THRESHOLD_MB = 5e4;
|
|
327
|
+
var cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
|
|
328
|
+
var cachedAt2 = 0;
|
|
329
|
+
var CACHE_MS2 = 1e3;
|
|
330
|
+
var clampInt2 = (value, min, max) => Math.min(max, Math.max(min, Math.floor(value)));
|
|
331
|
+
var parseContentLengthHeader = (value) => {
|
|
332
|
+
const raw = String(value ?? "").trim();
|
|
333
|
+
if (!raw) return void 0;
|
|
334
|
+
const n = Number(raw);
|
|
335
|
+
if (!Number.isFinite(n)) return void 0;
|
|
336
|
+
const v = Math.floor(n);
|
|
337
|
+
if (v <= 0) return void 0;
|
|
338
|
+
return v;
|
|
339
|
+
};
|
|
340
|
+
var getLargeFileSpoolThresholdBytes = () => {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
if (now - cachedAt2 < CACHE_MS2) return cachedThresholdBytes;
|
|
343
|
+
cachedAt2 = now;
|
|
344
|
+
try {
|
|
345
|
+
const cfg = config();
|
|
346
|
+
const raw = cfg?.resourceLimits?.largeFileSpoolThresholdMB;
|
|
347
|
+
if (raw === void 0 || raw === null || raw === "") {
|
|
348
|
+
cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
|
|
349
|
+
return cachedThresholdBytes;
|
|
350
|
+
}
|
|
351
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
352
|
+
if (!Number.isFinite(n)) {
|
|
353
|
+
cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
|
|
354
|
+
return cachedThresholdBytes;
|
|
355
|
+
}
|
|
356
|
+
if (n <= 0) {
|
|
357
|
+
cachedThresholdBytes = 0;
|
|
358
|
+
return cachedThresholdBytes;
|
|
359
|
+
}
|
|
360
|
+
const mb = clampInt2(n, 1, MAX_THRESHOLD_MB);
|
|
361
|
+
cachedThresholdBytes = mb * 1024 * 1024;
|
|
362
|
+
return cachedThresholdBytes;
|
|
363
|
+
} catch {
|
|
364
|
+
cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
|
|
365
|
+
return cachedThresholdBytes;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var shouldSpoolToDisk = (sizeBytes) => {
|
|
369
|
+
const threshold = getLargeFileSpoolThresholdBytes();
|
|
370
|
+
if (!threshold) return false;
|
|
371
|
+
if (typeof sizeBytes !== "number" || !Number.isFinite(sizeBytes)) return false;
|
|
372
|
+
return Math.floor(sizeBytes) >= threshold;
|
|
373
|
+
};
|
|
374
|
+
var safeFileName = (value) => {
|
|
375
|
+
return String(value ?? "").replaceAll("\0", "").replace(/[^\w.-]+/g, "_").replace(/^_+/, "").slice(0, 50) || "transfer";
|
|
376
|
+
};
|
|
377
|
+
var getTransferTmpDir = () => {
|
|
378
|
+
const tmpDir = path2.join(dir.DataDir, "transfer-tmp");
|
|
379
|
+
ensureDir(tmpDir);
|
|
380
|
+
return tmpDir;
|
|
381
|
+
};
|
|
382
|
+
var createTransferTempFilePath = (prefix) => {
|
|
383
|
+
const tmpDir = getTransferTmpDir();
|
|
384
|
+
const name = `${safeFileName(prefix)}-${Date.now()}-${randomUUID()}.tmp`;
|
|
385
|
+
return path2.join(tmpDir, name);
|
|
386
|
+
};
|
|
387
|
+
var streamToFile = async (readable, filePath, options) => {
|
|
388
|
+
await pipeline(readable, fs2.createWriteStream(filePath), { signal: options?.signal });
|
|
389
|
+
};
|
|
390
|
+
var safeUnlink = async (filePath) => {
|
|
391
|
+
try {
|
|
392
|
+
await fs2.promises.unlink(filePath);
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
250
397
|
// src/model/openlist/api.ts
|
|
398
|
+
import { logger as logger2 } from "node-karin";
|
|
251
399
|
var openlistApiReadJson = async (res) => {
|
|
252
400
|
try {
|
|
253
401
|
return await res.json();
|
|
@@ -441,12 +589,14 @@ var createOpenListApiDirEnsurer = (baseUrl, token, timeoutMs) => {
|
|
|
441
589
|
return { ensureDir: ensureDir2 };
|
|
442
590
|
};
|
|
443
591
|
var downloadAndUploadByOpenListApiPut = async (params) => {
|
|
444
|
-
const { sourceUrl, sourceHeaders, targetBaseUrl, targetToken, targetPath, timeoutMs } = params;
|
|
592
|
+
const { sourceUrl, sourceHeaders, targetBaseUrl, targetToken, targetPath, timeoutMs, expectedSize } = params;
|
|
445
593
|
const apiBaseUrl = buildOpenListApiBaseUrl(targetBaseUrl);
|
|
446
594
|
if (!apiBaseUrl) throw new Error("\u76EE\u6807 OpenList API \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5\u76EE\u6807 OpenList \u5730\u5740");
|
|
447
595
|
await withGlobalTransferLimit(`downloadAndUploadByOpenListApiPut:${targetPath}`, async () => {
|
|
448
596
|
const controller = new AbortController();
|
|
449
597
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
598
|
+
let tmpFilePath;
|
|
599
|
+
let uploadFileStream;
|
|
450
600
|
try {
|
|
451
601
|
let downloadRes;
|
|
452
602
|
try {
|
|
@@ -470,16 +620,29 @@ var downloadAndUploadByOpenListApiPut = async (params) => {
|
|
|
470
620
|
const authToken = String(targetToken ?? "").trim();
|
|
471
621
|
if (authToken) headers.Authorization = authToken;
|
|
472
622
|
const contentType = downloadRes.headers.get("content-type");
|
|
473
|
-
const contentLength = downloadRes.headers.get("content-length");
|
|
623
|
+
const contentLength = parseContentLengthHeader(downloadRes.headers.get("content-length"));
|
|
474
624
|
if (contentType) headers["Content-Type"] = contentType;
|
|
475
|
-
if (contentLength) headers["Content-Length"] = contentLength;
|
|
476
|
-
const
|
|
625
|
+
if (contentLength) headers["Content-Length"] = String(contentLength);
|
|
626
|
+
const sizeForDecision = typeof expectedSize === "number" && Number.isFinite(expectedSize) && expectedSize > 0 ? Math.floor(expectedSize) : contentLength;
|
|
627
|
+
const spoolToDisk = shouldSpoolToDisk(sizeForDecision);
|
|
477
628
|
let putRes;
|
|
478
629
|
try {
|
|
630
|
+
let body;
|
|
631
|
+
if (spoolToDisk) {
|
|
632
|
+
tmpFilePath = createTransferTempFilePath("openlist-api-put");
|
|
633
|
+
logger2.info(`[\u4F20\u8F93][\u843D\u76D8] \u68C0\u6D4B\u5230\u5927\u6587\u4EF6\uFF0C\u5C06\u5148\u4E0B\u8F7D\u843D\u76D8\u518D\u4E0A\u4F20(API): ${targetPath}`);
|
|
634
|
+
await streamToFile(Readable.fromWeb(downloadRes.body), tmpFilePath, { signal: controller.signal });
|
|
635
|
+
const stat = await fs3.promises.stat(tmpFilePath);
|
|
636
|
+
headers["Content-Length"] = String(stat.size);
|
|
637
|
+
uploadFileStream = fs3.createReadStream(tmpFilePath);
|
|
638
|
+
body = uploadFileStream;
|
|
639
|
+
} else {
|
|
640
|
+
body = Readable.fromWeb(downloadRes.body);
|
|
641
|
+
}
|
|
479
642
|
putRes = await fetch(`${apiBaseUrl}/fs/put`, {
|
|
480
643
|
method: "PUT",
|
|
481
644
|
headers,
|
|
482
|
-
body
|
|
645
|
+
body,
|
|
483
646
|
// @ts-expect-error Node fetch streaming body requires duplex
|
|
484
647
|
duplex: "half",
|
|
485
648
|
redirect: "follow",
|
|
@@ -498,13 +661,30 @@ var downloadAndUploadByOpenListApiPut = async (params) => {
|
|
|
498
661
|
const msg = json?.message ? ` - ${json.message}` : "";
|
|
499
662
|
throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: code=${json.code}${msg}`);
|
|
500
663
|
}
|
|
664
|
+
} catch (error) {
|
|
665
|
+
try {
|
|
666
|
+
controller.abort();
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
throw error;
|
|
501
670
|
} finally {
|
|
502
671
|
clearTimeout(timer);
|
|
672
|
+
if (uploadFileStream) {
|
|
673
|
+
await new Promise((resolve) => {
|
|
674
|
+
try {
|
|
675
|
+
uploadFileStream.close(() => resolve());
|
|
676
|
+
} catch {
|
|
677
|
+
resolve();
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
if (tmpFilePath) await safeUnlink(tmpFilePath);
|
|
503
682
|
}
|
|
504
683
|
});
|
|
505
684
|
};
|
|
506
685
|
|
|
507
686
|
// src/model/openlist/webdav.ts
|
|
687
|
+
import fs4 from "fs";
|
|
508
688
|
import { Readable as Readable2 } from "stream";
|
|
509
689
|
import { setTimeout as sleep3 } from "timers/promises";
|
|
510
690
|
|
|
@@ -532,54 +712,12 @@ var createThrottleTransform = (bytesPerSec) => {
|
|
|
532
712
|
};
|
|
533
713
|
|
|
534
714
|
// src/model/openlist/webdav.ts
|
|
715
|
+
import { logger as logger3 } from "node-karin";
|
|
535
716
|
var isRetryableWebDavError = (error) => {
|
|
536
717
|
const msg = formatErrorMessage(error);
|
|
537
718
|
return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|UND_ERR|socket hang up/i.test(msg);
|
|
538
719
|
};
|
|
539
720
|
var webdavMkcolOk = (status) => status === 201 || status === 405;
|
|
540
|
-
var webdavPropfindListNames = async (params) => {
|
|
541
|
-
const { davBaseUrl, auth, dirPath, timeoutMs } = params;
|
|
542
|
-
const controller = new AbortController();
|
|
543
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
544
|
-
try {
|
|
545
|
-
const url = `${davBaseUrl}${encodePathForUrl(dirPath)}`;
|
|
546
|
-
const body = `<?xml version="1.0" encoding="utf-8" ?>
|
|
547
|
-
<d:propfind xmlns:d="DAV:">
|
|
548
|
-
<d:prop>
|
|
549
|
-
<d:displayname />
|
|
550
|
-
</d:prop>
|
|
551
|
-
</d:propfind>`;
|
|
552
|
-
const res = await fetch(url, {
|
|
553
|
-
method: "PROPFIND",
|
|
554
|
-
headers: {
|
|
555
|
-
Authorization: auth,
|
|
556
|
-
Depth: "1",
|
|
557
|
-
"Content-Type": "application/xml; charset=utf-8"
|
|
558
|
-
},
|
|
559
|
-
body,
|
|
560
|
-
redirect: "follow",
|
|
561
|
-
signal: controller.signal
|
|
562
|
-
});
|
|
563
|
-
if (!res.ok) return /* @__PURE__ */ new Set();
|
|
564
|
-
const text = await res.text();
|
|
565
|
-
if (!text) return /* @__PURE__ */ new Set();
|
|
566
|
-
const names = /* @__PURE__ */ new Set();
|
|
567
|
-
const hrefRegex = /<d:href>([^<]+)<\/d:href>/gi;
|
|
568
|
-
let match;
|
|
569
|
-
while (match = hrefRegex.exec(text)) {
|
|
570
|
-
const href = match[1] ?? "";
|
|
571
|
-
const decoded = decodeURIComponent(href);
|
|
572
|
-
const cleaned = decoded.replace(/\/+$/, "");
|
|
573
|
-
const base = cleaned.split("/").filter(Boolean).pop();
|
|
574
|
-
if (base) names.add(base);
|
|
575
|
-
}
|
|
576
|
-
return names;
|
|
577
|
-
} catch {
|
|
578
|
-
return /* @__PURE__ */ new Set();
|
|
579
|
-
} finally {
|
|
580
|
-
clearTimeout(timer);
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
721
|
var webdavPropfindListEntries = async (params) => {
|
|
584
722
|
const { davBaseUrl, auth, dirPath, timeoutMs } = params;
|
|
585
723
|
const controller = new AbortController();
|
|
@@ -607,7 +745,13 @@ var webdavPropfindListEntries = async (params) => {
|
|
|
607
745
|
redirect: "follow",
|
|
608
746
|
signal: controller.signal
|
|
609
747
|
});
|
|
610
|
-
if (!res.ok)
|
|
748
|
+
if (!res.ok) {
|
|
749
|
+
try {
|
|
750
|
+
await res.body?.cancel();
|
|
751
|
+
} catch {
|
|
752
|
+
}
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
611
755
|
const text = await res.text();
|
|
612
756
|
if (!text) return [];
|
|
613
757
|
const out = [];
|
|
@@ -663,6 +807,8 @@ var copyWebDavToWebDav = async (params) => {
|
|
|
663
807
|
await withGlobalTransferLimit(`copyWebDavToWebDav:${sourcePath}=>${targetPath}`, async () => {
|
|
664
808
|
const controller = new AbortController();
|
|
665
809
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
810
|
+
let tmpFilePath;
|
|
811
|
+
let uploadFileStream;
|
|
666
812
|
try {
|
|
667
813
|
const sourceUrl = `${sourceDavBaseUrl}${encodePathForUrl(sourcePath)}`;
|
|
668
814
|
const targetUrl = `${targetDavBaseUrl}${encodePathForUrl(targetPath)}`;
|
|
@@ -688,16 +834,28 @@ var copyWebDavToWebDav = async (params) => {
|
|
|
688
834
|
if (!downloadRes.body) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
689
835
|
const headers = { Authorization: targetAuth };
|
|
690
836
|
const contentType = downloadRes.headers.get("content-type");
|
|
691
|
-
const contentLength = downloadRes.headers.get("content-length");
|
|
837
|
+
const contentLength = parseContentLengthHeader(downloadRes.headers.get("content-length"));
|
|
692
838
|
if (contentType) headers["Content-Type"] = contentType;
|
|
693
|
-
if (contentLength) headers["Content-Length"] = contentLength;
|
|
694
|
-
const
|
|
839
|
+
if (contentLength) headers["Content-Length"] = String(contentLength);
|
|
840
|
+
const spoolToDisk = shouldSpoolToDisk(contentLength);
|
|
695
841
|
let putRes;
|
|
696
842
|
try {
|
|
843
|
+
let body;
|
|
844
|
+
if (spoolToDisk) {
|
|
845
|
+
tmpFilePath = createTransferTempFilePath("webdav-copy");
|
|
846
|
+
logger3.info(`[\u4F20\u8F93][\u843D\u76D8] WebDAV\u590D\u5236\u68C0\u6D4B\u5230\u5927\u6587\u4EF6\uFF0C\u5C06\u5148\u4E0B\u8F7D\u843D\u76D8\u518D\u4E0A\u4F20: ${sourcePath} -> ${targetPath}`);
|
|
847
|
+
await streamToFile(Readable2.fromWeb(downloadRes.body), tmpFilePath, { signal: controller.signal });
|
|
848
|
+
const stat = await fs4.promises.stat(tmpFilePath);
|
|
849
|
+
headers["Content-Length"] = String(stat.size);
|
|
850
|
+
uploadFileStream = fs4.createReadStream(tmpFilePath);
|
|
851
|
+
body = uploadFileStream;
|
|
852
|
+
} else {
|
|
853
|
+
body = Readable2.fromWeb(downloadRes.body);
|
|
854
|
+
}
|
|
697
855
|
putRes = await fetch(targetUrl, {
|
|
698
856
|
method: "PUT",
|
|
699
857
|
headers,
|
|
700
|
-
body
|
|
858
|
+
body,
|
|
701
859
|
// @ts-expect-error Node fetch streaming body requires duplex
|
|
702
860
|
duplex: "half",
|
|
703
861
|
redirect: "follow",
|
|
@@ -711,8 +869,25 @@ var copyWebDavToWebDav = async (params) => {
|
|
|
711
869
|
const body = await fetchTextSafely(putRes);
|
|
712
870
|
throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${putRes.status} ${putRes.statusText}${body ? ` - ${body}` : ""}`);
|
|
713
871
|
}
|
|
872
|
+
await drainResponseBody(putRes);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
try {
|
|
875
|
+
controller.abort();
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
throw error;
|
|
714
879
|
} finally {
|
|
715
880
|
clearTimeout(timer);
|
|
881
|
+
if (uploadFileStream) {
|
|
882
|
+
await new Promise((resolve) => {
|
|
883
|
+
try {
|
|
884
|
+
uploadFileStream.close(() => resolve());
|
|
885
|
+
} catch {
|
|
886
|
+
resolve();
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
if (tmpFilePath) await safeUnlink(tmpFilePath);
|
|
716
891
|
}
|
|
717
892
|
});
|
|
718
893
|
};
|
|
@@ -752,6 +927,7 @@ var createWebDavDirEnsurer = (davBaseUrl, auth, timeoutMs) => {
|
|
|
752
927
|
const body = await fetchTextSafely(res);
|
|
753
928
|
throw new Error(`MKCOL \u5931\u8D25: ${current} -> ${res.status} ${res.statusText}${body ? ` - ${body}` : ""}`);
|
|
754
929
|
}
|
|
930
|
+
await drainResponseBody(res);
|
|
755
931
|
} finally {
|
|
756
932
|
clearTimeout(timer);
|
|
757
933
|
}
|
|
@@ -768,10 +944,12 @@ var createWebDavDirEnsurer = (davBaseUrl, auth, timeoutMs) => {
|
|
|
768
944
|
return { ensureDir: ensureDir2 };
|
|
769
945
|
};
|
|
770
946
|
var downloadAndUploadByWebDav = async (params) => {
|
|
771
|
-
const { sourceUrl, sourceHeaders, targetUrl, auth, timeoutMs, rateLimitBytesPerSec } = params;
|
|
947
|
+
const { sourceUrl, sourceHeaders, targetUrl, auth, timeoutMs, rateLimitBytesPerSec, expectedSize } = params;
|
|
772
948
|
await withGlobalTransferLimit(`downloadAndUploadByWebDav:${targetUrl}`, async () => {
|
|
773
949
|
const controller = new AbortController();
|
|
774
950
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
951
|
+
let tmpFilePath;
|
|
952
|
+
let uploadFileStream;
|
|
775
953
|
try {
|
|
776
954
|
let downloadRes;
|
|
777
955
|
try {
|
|
@@ -793,14 +971,27 @@ var downloadAndUploadByWebDav = async (params) => {
|
|
|
793
971
|
Authorization: auth
|
|
794
972
|
};
|
|
795
973
|
const contentType = downloadRes.headers.get("content-type");
|
|
796
|
-
const contentLength = downloadRes.headers.get("content-length");
|
|
974
|
+
const contentLength = parseContentLengthHeader(downloadRes.headers.get("content-length"));
|
|
797
975
|
if (contentType) headers["Content-Type"] = contentType;
|
|
798
|
-
if (contentLength) headers["Content-Length"] = contentLength;
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
if (throttle) bodyStream = bodyStream.pipe(throttle);
|
|
976
|
+
if (contentLength) headers["Content-Length"] = String(contentLength);
|
|
977
|
+
const sizeForDecision = typeof expectedSize === "number" && Number.isFinite(expectedSize) && expectedSize > 0 ? Math.floor(expectedSize) : contentLength;
|
|
978
|
+
const spoolToDisk = shouldSpoolToDisk(sizeForDecision);
|
|
802
979
|
let putRes;
|
|
803
980
|
try {
|
|
981
|
+
let bodyStream;
|
|
982
|
+
if (spoolToDisk) {
|
|
983
|
+
tmpFilePath = createTransferTempFilePath("webdav-download-upload");
|
|
984
|
+
logger3.info(`[\u4F20\u8F93][\u843D\u76D8] \u68C0\u6D4B\u5230\u5927\u6587\u4EF6\uFF0C\u5C06\u5148\u4E0B\u8F7D\u843D\u76D8\u518D\u4E0A\u4F20: ${targetUrl}`);
|
|
985
|
+
await streamToFile(Readable2.fromWeb(downloadRes.body), tmpFilePath, { signal: controller.signal });
|
|
986
|
+
const stat = await fs4.promises.stat(tmpFilePath);
|
|
987
|
+
headers["Content-Length"] = String(stat.size);
|
|
988
|
+
uploadFileStream = fs4.createReadStream(tmpFilePath);
|
|
989
|
+
bodyStream = uploadFileStream;
|
|
990
|
+
} else {
|
|
991
|
+
bodyStream = Readable2.fromWeb(downloadRes.body);
|
|
992
|
+
}
|
|
993
|
+
const throttle = createThrottleTransform(rateLimitBytesPerSec || 0);
|
|
994
|
+
if (throttle) bodyStream = bodyStream.pipe(throttle);
|
|
804
995
|
putRes = await fetch(targetUrl, {
|
|
805
996
|
method: "PUT",
|
|
806
997
|
headers,
|
|
@@ -819,8 +1010,25 @@ var downloadAndUploadByWebDav = async (params) => {
|
|
|
819
1010
|
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
1011
|
throw new Error(`\u4E0A\u4F20\u5931\u8D25: ${putRes.status} ${putRes.statusText}${hint}${body ? ` - ${body}` : ""}`);
|
|
821
1012
|
}
|
|
1013
|
+
await drainResponseBody(putRes);
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
try {
|
|
1016
|
+
controller.abort();
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
throw error;
|
|
822
1020
|
} finally {
|
|
823
1021
|
clearTimeout(timer);
|
|
1022
|
+
if (uploadFileStream) {
|
|
1023
|
+
await new Promise((resolve) => {
|
|
1024
|
+
try {
|
|
1025
|
+
uploadFileStream.close(() => resolve());
|
|
1026
|
+
} catch {
|
|
1027
|
+
resolve();
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
if (tmpFilePath) await safeUnlink(tmpFilePath);
|
|
824
1032
|
}
|
|
825
1033
|
});
|
|
826
1034
|
};
|
|
@@ -849,66 +1057,7 @@ var normalizeOpenListBackupTransport = (value, fallback) => {
|
|
|
849
1057
|
};
|
|
850
1058
|
var buildTargetPath = (targetRoot, sourcePath) => {
|
|
851
1059
|
const rel = String(sourcePath ?? "").replace(/^\/+/, "");
|
|
852
|
-
return normalizePosixPath(
|
|
853
|
-
};
|
|
854
|
-
var scanOpenListFiles = async (params) => {
|
|
855
|
-
const {
|
|
856
|
-
sourceTransport,
|
|
857
|
-
sourceBaseUrl,
|
|
858
|
-
sourceToken,
|
|
859
|
-
sourceAuth,
|
|
860
|
-
sourceDavBaseUrl,
|
|
861
|
-
srcDir,
|
|
862
|
-
targetRoot,
|
|
863
|
-
timeoutMs,
|
|
864
|
-
perPage,
|
|
865
|
-
scanConcurrency,
|
|
866
|
-
onProgress
|
|
867
|
-
} = params;
|
|
868
|
-
const normalizedSrcDir = normalizePosixPath(String(srcDir ?? "/"));
|
|
869
|
-
const normalizedTargetRoot = normalizePosixPath(String(targetRoot ?? "/"));
|
|
870
|
-
const scanConcurrencyMax = Math.max(1, Math.min(200, Math.floor(scanConcurrency) || 20));
|
|
871
|
-
const files = [];
|
|
872
|
-
let scannedDirs = 0;
|
|
873
|
-
const listDir = async (dirPath) => {
|
|
874
|
-
if (sourceTransport === "webdav") {
|
|
875
|
-
if (!sourceDavBaseUrl) throw new Error("\u6E90\u7AEF WebDAV \u5730\u5740\u4E0D\u6B63\u786E");
|
|
876
|
-
return await webdavPropfindListEntries({
|
|
877
|
-
davBaseUrl: sourceDavBaseUrl,
|
|
878
|
-
auth: sourceAuth,
|
|
879
|
-
dirPath,
|
|
880
|
-
timeoutMs
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
return await openlistApiListEntries({
|
|
884
|
-
baseUrl: sourceBaseUrl,
|
|
885
|
-
token: sourceToken,
|
|
886
|
-
dirPath,
|
|
887
|
-
timeoutMs,
|
|
888
|
-
perPage
|
|
889
|
-
});
|
|
890
|
-
};
|
|
891
|
-
const reportProgress = () => {
|
|
892
|
-
if (!onProgress) return;
|
|
893
|
-
onProgress({ scannedDirs, files: files.length });
|
|
894
|
-
};
|
|
895
|
-
let pendingDirs = [normalizedSrcDir];
|
|
896
|
-
while (pendingDirs.length) {
|
|
897
|
-
const batch = pendingDirs.splice(0, scanConcurrencyMax);
|
|
898
|
-
await runWithConcurrency(batch, scanConcurrencyMax, async (dirPath) => {
|
|
899
|
-
scannedDirs++;
|
|
900
|
-
const entries = await listDir(dirPath);
|
|
901
|
-
for (const it of entries) {
|
|
902
|
-
const sourcePath = normalizePosixPath(path.posix.join(dirPath, it.name));
|
|
903
|
-
const targetPath = buildTargetPath(normalizedTargetRoot, sourcePath);
|
|
904
|
-
if (it.isDir) pendingDirs.push(sourcePath);
|
|
905
|
-
else files.push({ sourcePath, targetPath });
|
|
906
|
-
}
|
|
907
|
-
reportProgress();
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
reportProgress();
|
|
911
|
-
return { files, scannedDirs };
|
|
1060
|
+
return normalizePosixPath(path3.posix.join(targetRoot, rel));
|
|
912
1061
|
};
|
|
913
1062
|
var existsOnTarget = async (params) => {
|
|
914
1063
|
const { targetTransport, targetDavBaseUrl, targetAuth, targetBaseUrl, getTargetToken, targetPath, timeoutMs } = params;
|
|
@@ -1047,7 +1196,7 @@ var backupOpenListToOpenListCore = async (params) => {
|
|
|
1047
1196
|
if (!targetDavBaseUrl) throw new Error("\u76EE\u6807\u7AEF OpenList \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 openlistBaseUrl");
|
|
1048
1197
|
const normalizedSrcDir = normalizePosixPath(String(params.srcDir ?? "/"));
|
|
1049
1198
|
const normalizedTargetBaseDir = normalizePosixPath(String(params.toDir ?? cfg.openlistTargetDir ?? "/"));
|
|
1050
|
-
const targetRoot = params.appendHostDir === false ? normalizedTargetBaseDir : normalizePosixPath(
|
|
1199
|
+
const targetRoot = params.appendHostDir === false ? normalizedTargetBaseDir : normalizePosixPath(path3.posix.join(normalizedTargetBaseDir, safeHostDirName(sourceBaseUrl)));
|
|
1051
1200
|
const mode = params.mode ?? "incremental";
|
|
1052
1201
|
const transport = params.transport ?? normalizeOpenListBackupTransport(cfg?.openListBackupTransport, "auto");
|
|
1053
1202
|
const lockKey = `${sourceBaseUrl} -> ${targetBaseUrl}`;
|
|
@@ -1128,46 +1277,12 @@ var backupOpenListToOpenListCore = async (params) => {
|
|
|
1128
1277
|
const maxConcurrency = Math.max(1, Math.min(50, Math.floor(params.concurrency || 0) || 3));
|
|
1129
1278
|
let scannedDirs = 0;
|
|
1130
1279
|
let scannedFiles = 0;
|
|
1280
|
+
let ok = 0;
|
|
1281
|
+
let skipped = 0;
|
|
1282
|
+
let fail = 0;
|
|
1131
1283
|
const startAt = Date.now();
|
|
1132
|
-
|
|
1133
|
-
const elapsed = Math.floor((Date.now() - startAt) / 1e3);
|
|
1134
|
-
enqueueReport(`\u5907\u4EFD\u8FDB\u884C\u4E2D\uFF08${elapsed}s\uFF09
|
|
1135
|
-
\u9636\u6BB5\uFF1A\u626B\u63CF
|
|
1136
|
-
\u5DF2\u626B\u63CF\u76EE\u5F55\uFF1A${scannedDirs}
|
|
1137
|
-
\u5DF2\u53D1\u73B0\u6587\u4EF6\uFF1A${scannedFiles}`);
|
|
1138
|
-
}, 1e4);
|
|
1284
|
+
const maxFilesLimit = typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : void 0;
|
|
1139
1285
|
const sourceAuth = hasSourceAuth ? buildOpenListAuthHeader(sourceUsername, sourcePassword) : "";
|
|
1140
|
-
const scanResult = await scanOpenListFiles({
|
|
1141
|
-
sourceTransport,
|
|
1142
|
-
sourceBaseUrl,
|
|
1143
|
-
sourceToken: await getSourceToken(),
|
|
1144
|
-
sourceAuth: sourceAuth || void 0,
|
|
1145
|
-
sourceDavBaseUrl,
|
|
1146
|
-
srcDir: normalizedSrcDir,
|
|
1147
|
-
targetRoot,
|
|
1148
|
-
timeoutMs: listTimeoutMs,
|
|
1149
|
-
perPage: listPerPage,
|
|
1150
|
-
scanConcurrency: scanConcurrencyMax,
|
|
1151
|
-
onProgress: (p) => {
|
|
1152
|
-
scannedDirs = p.scannedDirs;
|
|
1153
|
-
scannedFiles = p.files;
|
|
1154
|
-
}
|
|
1155
|
-
});
|
|
1156
|
-
const files = scanResult.files;
|
|
1157
|
-
if (typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0) {
|
|
1158
|
-
files.splice(Math.floor(params.maxFiles));
|
|
1159
|
-
}
|
|
1160
|
-
if (!files.length) {
|
|
1161
|
-
enqueueReport("\u672A\u53D1\u73B0\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6\u3002");
|
|
1162
|
-
return { ok: 0, skipped: 0, fail: 0 };
|
|
1163
|
-
}
|
|
1164
|
-
if (ticker) clearInterval(ticker);
|
|
1165
|
-
ticker = setInterval(() => {
|
|
1166
|
-
const elapsed = Math.floor((Date.now() - startAt) / 1e3);
|
|
1167
|
-
enqueueReport(`\u5907\u4EFD\u8FDB\u884C\u4E2D\uFF08${elapsed}s\uFF09
|
|
1168
|
-
\u9636\u6BB5\uFF1A\u590D\u5236
|
|
1169
|
-
\u5F85\u5904\u7406\uFF1A${files.length}`);
|
|
1170
|
-
}, 1e4);
|
|
1171
1286
|
enqueueReport([
|
|
1172
1287
|
"\u5F00\u59CB\u5907\u4EFD OpenList...",
|
|
1173
1288
|
`\u6E90\uFF1A${sourceBaseUrl}`,
|
|
@@ -1176,14 +1291,13 @@ var backupOpenListToOpenListCore = async (params) => {
|
|
|
1176
1291
|
`\u76EE\u6807\u76EE\u5F55\uFF1A${targetRoot}`,
|
|
1177
1292
|
`\u6A21\u5F0F\uFF1A${mode}`,
|
|
1178
1293
|
`\u4F20\u8F93\uFF1A${transport}`,
|
|
1179
|
-
`\
|
|
1294
|
+
`\u626B\u63CF\u5E76\u53D1\uFF1A${scanConcurrencyMax}`,
|
|
1295
|
+
`\u590D\u5236\u5E76\u53D1\uFF1A${maxConcurrency}`,
|
|
1296
|
+
`maxFiles\uFF1A${typeof maxFilesLimit === "number" ? maxFilesLimit : "-"}`
|
|
1180
1297
|
].join("\n"));
|
|
1181
|
-
let skipped = 0;
|
|
1182
|
-
let ok = 0;
|
|
1183
|
-
let fail = 0;
|
|
1184
1298
|
const targetDavBase = targetDavBaseUrl;
|
|
1185
1299
|
const ensureDirForPath = async (filePath) => {
|
|
1186
|
-
const dirPath = normalizePosixPath(
|
|
1300
|
+
const dirPath = normalizePosixPath(path3.posix.dirname(filePath));
|
|
1187
1301
|
targetTransport = await ensureTargetDir({
|
|
1188
1302
|
dirPath,
|
|
1189
1303
|
targetTransport,
|
|
@@ -1193,50 +1307,109 @@ var backupOpenListToOpenListCore = async (params) => {
|
|
|
1193
1307
|
getApiEnsurer: getTargetDirEnsurerApi
|
|
1194
1308
|
});
|
|
1195
1309
|
};
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1310
|
+
const listDirEntries = async (dirPath) => {
|
|
1311
|
+
if (sourceTransport === "webdav") {
|
|
1312
|
+
if (!sourceDavBaseUrl) throw new Error("\u6E90\u7AEF WebDAV \u5730\u5740\u4E0D\u6B63\u786E");
|
|
1313
|
+
return await webdavPropfindListEntries({
|
|
1314
|
+
davBaseUrl: sourceDavBaseUrl,
|
|
1315
|
+
auth: sourceAuth,
|
|
1316
|
+
dirPath,
|
|
1317
|
+
timeoutMs: listTimeoutMs
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
return await openlistApiListEntries({
|
|
1321
|
+
baseUrl: sourceBaseUrl,
|
|
1322
|
+
token: await getSourceToken(),
|
|
1323
|
+
dirPath,
|
|
1324
|
+
timeoutMs: listTimeoutMs,
|
|
1325
|
+
perPage: listPerPage
|
|
1326
|
+
});
|
|
1327
|
+
};
|
|
1328
|
+
const executing = /* @__PURE__ */ new Set();
|
|
1329
|
+
const launchCopy = (file) => {
|
|
1330
|
+
const task = (async () => {
|
|
1331
|
+
const { sourcePath, targetPath } = file;
|
|
1332
|
+
try {
|
|
1333
|
+
if (mode === "incremental") {
|
|
1334
|
+
const exists = await existsOnTarget({
|
|
1335
|
+
targetTransport,
|
|
1336
|
+
targetDavBaseUrl: targetDavBase,
|
|
1337
|
+
targetAuth,
|
|
1338
|
+
targetBaseUrl,
|
|
1339
|
+
getTargetToken,
|
|
1340
|
+
targetPath,
|
|
1341
|
+
timeoutMs: listTimeoutMs
|
|
1342
|
+
});
|
|
1343
|
+
if (exists) {
|
|
1344
|
+
skipped++;
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
await ensureDirForPath(targetPath);
|
|
1349
|
+
await copyOpenListFile({
|
|
1350
|
+
sourceTransport,
|
|
1200
1351
|
targetTransport,
|
|
1352
|
+
sourceBaseUrl,
|
|
1353
|
+
sourceDavBaseUrl,
|
|
1354
|
+
sourceAuth,
|
|
1355
|
+
targetBaseUrl,
|
|
1201
1356
|
targetDavBaseUrl: targetDavBase,
|
|
1202
1357
|
targetAuth,
|
|
1203
|
-
|
|
1204
|
-
getTargetToken,
|
|
1358
|
+
sourcePath,
|
|
1205
1359
|
targetPath,
|
|
1206
|
-
|
|
1360
|
+
getSourceToken,
|
|
1361
|
+
getTargetToken,
|
|
1362
|
+
timeoutMs
|
|
1207
1363
|
});
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1364
|
+
ok++;
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
fail++;
|
|
1367
|
+
logger4.error(error);
|
|
1368
|
+
if (allowAutoFallback) {
|
|
1369
|
+
const msg = formatErrorMessage(error);
|
|
1370
|
+
if (targetTransport === "webdav" && /401|403|MKCOL|PUT/i.test(msg)) targetTransport = "api";
|
|
1371
|
+
else if (targetTransport === "api" && /fs\/put|fs\/mkdir|code=/i.test(msg)) targetTransport = "webdav";
|
|
1211
1372
|
}
|
|
1212
1373
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1374
|
+
})();
|
|
1375
|
+
executing.add(task);
|
|
1376
|
+
task.finally(() => executing.delete(task));
|
|
1377
|
+
return task;
|
|
1378
|
+
};
|
|
1379
|
+
ticker = setInterval(() => {
|
|
1380
|
+
const elapsed = Math.floor((Date.now() - startAt) / 1e3);
|
|
1381
|
+
enqueueReport([
|
|
1382
|
+
`\u5907\u4EFD\u8FDB\u884C\u4E2D\uFF08${elapsed}s\uFF09`,
|
|
1383
|
+
`\u5DF2\u626B\u63CF\u76EE\u5F55\uFF1A${scannedDirs}`,
|
|
1384
|
+
`\u5DF2\u53D1\u73B0\u6587\u4EF6\uFF1A${scannedFiles}${typeof maxFilesLimit === "number" ? `/${maxFilesLimit}` : ""}`,
|
|
1385
|
+
`\u8FDB\u5EA6\uFF1Aok=${ok} skip=${skipped} fail=${fail}`,
|
|
1386
|
+
`\u5E76\u53D1\uFF1AinFlight=${executing.size}/${maxConcurrency}`
|
|
1387
|
+
].join("\n"));
|
|
1388
|
+
}, 1e4);
|
|
1389
|
+
let pendingDirs = [normalizedSrcDir];
|
|
1390
|
+
while (pendingDirs.length) {
|
|
1391
|
+
const dirPath = pendingDirs.pop();
|
|
1392
|
+
scannedDirs++;
|
|
1393
|
+
const entries = await listDirEntries(dirPath);
|
|
1394
|
+
for (const it of entries) {
|
|
1395
|
+
const sourcePath = normalizePosixPath(path3.posix.join(dirPath, it.name));
|
|
1396
|
+
const targetPath = buildTargetPath(targetRoot, sourcePath);
|
|
1397
|
+
if (it.isDir) {
|
|
1398
|
+
pendingDirs.push(sourcePath);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
scannedFiles++;
|
|
1402
|
+
if (typeof maxFilesLimit === "number" && scannedFiles > maxFilesLimit) {
|
|
1403
|
+
pendingDirs = [];
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
launchCopy({ sourcePath, targetPath });
|
|
1407
|
+
if (executing.size >= maxConcurrency) {
|
|
1408
|
+
await Promise.race(executing);
|
|
1237
1409
|
}
|
|
1238
1410
|
}
|
|
1239
|
-
}
|
|
1411
|
+
}
|
|
1412
|
+
await Promise.all(executing);
|
|
1240
1413
|
enqueueReport(`\u5907\u4EFD\u5B8C\u6210\uFF1A\u6210\u529F ${ok}\uFF0C\u8DF3\u8FC7 ${skipped}\uFF0C\u5931\u8D25 ${fail}`);
|
|
1241
1414
|
return { ok, skipped, fail };
|
|
1242
1415
|
} finally {
|
|
@@ -1245,24 +1418,6 @@ var backupOpenListToOpenListCore = async (params) => {
|
|
|
1245
1418
|
}
|
|
1246
1419
|
};
|
|
1247
1420
|
|
|
1248
|
-
// src/model/shared/fsJson.ts
|
|
1249
|
-
import fs from "fs";
|
|
1250
|
-
import path2 from "path";
|
|
1251
|
-
var ensureDir = (dirPath) => fs.mkdirSync(dirPath, { recursive: true });
|
|
1252
|
-
var readJsonSafe = (filePath) => {
|
|
1253
|
-
try {
|
|
1254
|
-
if (!fs.existsSync(filePath)) return {};
|
|
1255
|
-
const raw = fs.readFileSync(filePath, "utf8");
|
|
1256
|
-
return raw ? JSON.parse(raw) : {};
|
|
1257
|
-
} catch {
|
|
1258
|
-
return {};
|
|
1259
|
-
}
|
|
1260
|
-
};
|
|
1261
|
-
var writeJsonSafe = (filePath, data) => {
|
|
1262
|
-
ensureDir(path2.dirname(filePath));
|
|
1263
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1264
|
-
};
|
|
1265
|
-
|
|
1266
1421
|
export {
|
|
1267
1422
|
buildOpenListDavBaseUrl,
|
|
1268
1423
|
buildOpenListAuthHeader,
|
|
@@ -1270,13 +1425,13 @@ export {
|
|
|
1270
1425
|
safePathSegment,
|
|
1271
1426
|
encodePathForUrl,
|
|
1272
1427
|
formatErrorMessage,
|
|
1273
|
-
|
|
1428
|
+
ensureDir,
|
|
1429
|
+
readJsonSafe,
|
|
1430
|
+
writeJsonSafe,
|
|
1431
|
+
webdavHeadExists,
|
|
1274
1432
|
createWebDavDirEnsurer,
|
|
1275
1433
|
downloadAndUploadByWebDav,
|
|
1276
1434
|
runWithConcurrency,
|
|
1277
1435
|
runWithAdaptiveConcurrency,
|
|
1278
|
-
backupOpenListToOpenListCore
|
|
1279
|
-
ensureDir,
|
|
1280
|
-
readJsonSafe,
|
|
1281
|
-
writeJsonSafe
|
|
1436
|
+
backupOpenListToOpenListCore
|
|
1282
1437
|
};
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
readGroupSyncState,
|
|
3
3
|
withGroupSyncLock,
|
|
4
4
|
writeGroupSyncState
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-AAKFRFSO.js";
|
|
6
6
|
import {
|
|
7
7
|
buildOpenListAuthHeader,
|
|
8
8
|
buildOpenListDavBaseUrl,
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
runWithAdaptiveConcurrency,
|
|
16
16
|
runWithConcurrency,
|
|
17
17
|
safePathSegment,
|
|
18
|
-
|
|
19
|
-
} from "./chunk-
|
|
18
|
+
webdavHeadExists
|
|
19
|
+
} from "./chunk-HSFFULJR.js";
|
|
20
20
|
import {
|
|
21
21
|
config,
|
|
22
22
|
time
|
|
@@ -461,24 +461,18 @@ var syncGroupFilesToOpenListCore = async (params) => {
|
|
|
461
461
|
return !ok;
|
|
462
462
|
}) : candidates;
|
|
463
463
|
if (mode === "incremental" && needSync.length) {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
await runWithConcurrency(dirs, 3, async (dirPath) => {
|
|
469
|
-
const names = await webdavPropfindListNames({
|
|
464
|
+
const existsResults = new Array(needSync.length).fill(false);
|
|
465
|
+
const existsConcurrency = 10;
|
|
466
|
+
await runWithConcurrency(needSync, existsConcurrency, async ({ remotePath }, index) => {
|
|
467
|
+
existsResults[index] = await webdavHeadExists({
|
|
470
468
|
davBaseUrl,
|
|
471
469
|
auth,
|
|
472
|
-
|
|
470
|
+
filePath: remotePath,
|
|
473
471
|
timeoutMs: webdavTimeoutMs
|
|
474
472
|
});
|
|
475
|
-
namesByDir.set(dirPath, names);
|
|
476
473
|
});
|
|
477
|
-
needSync = needSync.filter((
|
|
478
|
-
|
|
479
|
-
const base = path2.posix.basename(remotePath);
|
|
480
|
-
const names = namesByDir.get(dirPath);
|
|
481
|
-
if (names && names.has(base)) {
|
|
474
|
+
needSync = needSync.filter((_it, index) => {
|
|
475
|
+
if (existsResults[index]) {
|
|
482
476
|
skipped++;
|
|
483
477
|
return false;
|
|
484
478
|
}
|
|
@@ -508,13 +502,14 @@ var syncGroupFilesToOpenListCore = async (params) => {
|
|
|
508
502
|
const shouldRefreshUrl = (message) => {
|
|
509
503
|
return /403|URL已过期|url已过期|url可能已失效|需要重新获取|下载超时/.test(message);
|
|
510
504
|
};
|
|
511
|
-
const transferOne = async (sourceUrl, targetUrl) => {
|
|
505
|
+
const transferOne = async (sourceUrl, targetUrl, expectedSize) => {
|
|
512
506
|
await downloadAndUploadByWebDav({
|
|
513
507
|
sourceUrl,
|
|
514
508
|
targetUrl,
|
|
515
509
|
auth,
|
|
516
510
|
timeoutMs: transferTimeoutMs,
|
|
517
|
-
rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0
|
|
511
|
+
rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0,
|
|
512
|
+
expectedSize
|
|
518
513
|
});
|
|
519
514
|
};
|
|
520
515
|
report && await report("\u5F00\u59CB\u4E0B\u8F7D\u5E76\u4E0A\u4F20\u5230 OpenList\uFF0C\u8BF7\u7A0D\u5019..");
|
|
@@ -532,7 +527,7 @@ var syncGroupFilesToOpenListCore = async (params) => {
|
|
|
532
527
|
await dirEnsurer.ensureDir(remoteDir);
|
|
533
528
|
const currentUrl = item.url;
|
|
534
529
|
if (!currentUrl) throw new Error("\u7F3A\u5C11\u4E0B\u8F7D URL");
|
|
535
|
-
await transferOne(currentUrl, targetUrl);
|
|
530
|
+
await transferOne(currentUrl, targetUrl, item.size);
|
|
536
531
|
okCount++;
|
|
537
532
|
succeeded = true;
|
|
538
533
|
lastError = void 0;
|
|
@@ -686,13 +681,15 @@ var handleGroupFileUploadedAutoBackup = (e) => {
|
|
|
686
681
|
const normalized = direct ? normalizeGroupFileRelativePath(direct) : "";
|
|
687
682
|
if (normalized) item.path = normalized;
|
|
688
683
|
const bot = e?.bot;
|
|
689
|
-
|
|
684
|
+
const resolvePathEnabled = typeof targetCfg?.uploadBackupResolvePath === "boolean" ? Boolean(targetCfg.uploadBackupResolvePath) : true;
|
|
685
|
+
const resolvePathMaxFolders = typeof targetCfg?.uploadBackupResolvePathMaxFolders === "number" ? Math.max(1, Math.floor(Number(targetCfg.uploadBackupResolvePathMaxFolders)) || 1) : 4e3;
|
|
686
|
+
if (resolvePathEnabled && bot && !item.path.includes("/")) {
|
|
690
687
|
try {
|
|
691
688
|
const found = await locateGroupFileByIdWithRetry(bot, groupId, fid, {
|
|
692
689
|
retries: 3,
|
|
693
690
|
delayMs: 1200,
|
|
694
691
|
timeoutMs: 15e3,
|
|
695
|
-
maxFolders:
|
|
692
|
+
maxFolders: resolvePathMaxFolders,
|
|
696
693
|
expectedName: name,
|
|
697
694
|
expectedSize: size
|
|
698
695
|
});
|
|
@@ -704,6 +701,8 @@ var handleGroupFileUploadedAutoBackup = (e) => {
|
|
|
704
701
|
} catch (error) {
|
|
705
702
|
logger3.debug(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u83B7\u53D6\u7FA4\u5185\u6587\u4EF6\u8DEF\u5F84\u5931\u8D25\uFF0C\u5C06\u9000\u5316\u4E3A\u6839\u76EE\u5F55: ${formatErrorMessage(error)}`);
|
|
706
703
|
}
|
|
704
|
+
} else if (!resolvePathEnabled && bot && !item.path.includes("/")) {
|
|
705
|
+
logger3.debug(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] uploadBackupResolvePath=false\uFF0C\u5DF2\u8DF3\u8FC7\u7FA4\u6587\u4EF6\u5939\u8DEF\u5F84\u89E3\u6790\uFF1A${name}`);
|
|
707
706
|
}
|
|
708
707
|
}
|
|
709
708
|
const resolveUrl = async () => {
|
|
@@ -817,7 +816,8 @@ var handleGroupFileUploadedAutoBackup = (e) => {
|
|
|
817
816
|
targetUrl,
|
|
818
817
|
auth,
|
|
819
818
|
timeoutMs: transferTimeoutMs,
|
|
820
|
-
rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0
|
|
819
|
+
rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0,
|
|
820
|
+
expectedSize: size
|
|
821
821
|
});
|
|
822
822
|
break;
|
|
823
823
|
} catch (error) {
|