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 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
 
@@ -5,7 +5,8 @@
5
5
  "openlistPassword": "",
6
6
  "openlistTargetDir": "/",
7
7
  "resourceLimits": {
8
- "transferConcurrency": 1
8
+ "transferConcurrency": 1,
9
+ "largeFileSpoolThresholdMB": 200
9
10
  },
10
11
  "groupSyncDefaults": {
11
12
  "mode": "incremental",
@@ -6,12 +6,12 @@ import "../chunk-PBBZ5KAD.js";
6
6
  import "../chunk-QB3GSENE.js";
7
7
  import {
8
8
  handleGroupFileUploadedAutoBackup
9
- } from "../chunk-BWVJMEKK.js";
10
- import "../chunk-5QW7XYQX.js";
9
+ } from "../chunk-ZXZMBJN3.js";
10
+ import "../chunk-AAKFRFSO.js";
11
11
  import {
12
12
  backupOpenListToOpenListCore,
13
13
  formatErrorMessage
14
- } from "../chunk-YVFRUAZO.js";
14
+ } from "../chunk-HSFFULJR.js";
15
15
  import {
16
16
  config
17
17
  } from "../chunk-DA4U55JC.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  exportGroupFilesToDisk,
3
3
  syncGroupFilesToOpenListCore
4
- } from "../chunk-BWVJMEKK.js";
5
- import "../chunk-5QW7XYQX.js";
4
+ } from "../chunk-ZXZMBJN3.js";
5
+ import "../chunk-AAKFRFSO.js";
6
6
  import {
7
7
  formatErrorMessage,
8
8
  normalizePosixPath
9
- } from "../chunk-YVFRUAZO.js";
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-I2I6ZPJU.js";
4
- import "../chunk-BWVJMEKK.js";
5
- import "../chunk-5QW7XYQX.js";
6
- import "../chunk-YVFRUAZO.js";
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
 
@@ -5,7 +5,7 @@ import {
5
5
  resolveOpltMapping,
6
6
  withOpltUser,
7
7
  writeOpltData
8
- } from "../chunk-WQFR5LF3.js";
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-YVFRUAZO.js";
23
+ } from "../chunk-HSFFULJR.js";
24
24
  import {
25
25
  config
26
26
  } from "../chunk-DA4U55JC.js";
@@ -4,14 +4,14 @@ import {
4
4
  import "../chunk-QB3GSENE.js";
5
5
  import {
6
6
  readGroupSyncState
7
- } from "../chunk-5QW7XYQX.js";
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-YVFRUAZO.js";
14
+ } from "../chunk-HSFFULJR.js";
15
15
  import "../chunk-DA4U55JC.js";
16
16
  import {
17
17
  dir
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  runNightlyOpltBackup
3
- } from "../chunk-WQFR5LF3.js";
3
+ } from "../chunk-GAVQ23XT.js";
4
4
  import {
5
5
  runNightlyGroupBackup
6
- } from "../chunk-I2I6ZPJU.js";
7
- import "../chunk-BWVJMEKK.js";
8
- import "../chunk-5QW7XYQX.js";
9
- import "../chunk-YVFRUAZO.js";
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,7 +1,7 @@
1
1
  import {
2
2
  readJsonSafe,
3
3
  writeJsonSafe
4
- } from "./chunk-YVFRUAZO.js";
4
+ } from "./chunk-HSFFULJR.js";
5
5
  import {
6
6
  dir
7
7
  } from "./chunk-IZS467MR.js";
@@ -4,7 +4,7 @@ import {
4
4
  normalizePosixPath,
5
5
  readJsonSafe,
6
6
  writeJsonSafe
7
- } from "./chunk-YVFRUAZO.js";
7
+ } from "./chunk-HSFFULJR.js";
8
8
  import {
9
9
  config
10
10
  } from "./chunk-DA4U55JC.js";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  syncGroupFilesToOpenListCore
3
- } from "./chunk-BWVJMEKK.js";
3
+ } from "./chunk-ZXZMBJN3.js";
4
4
  import {
5
5
  normalizePosixPath,
6
6
  readJsonSafe,
7
7
  writeJsonSafe
8
- } from "./chunk-YVFRUAZO.js";
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 text = await res.text();
46
- return text.slice(0, maxLen);
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/openlist/backup.ts
103
+ // src/model/shared/fsJson.ts
104
+ import fs from "fs";
53
105
  import path from "path";
54
- import { logger as logger2 } from "node-karin";
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 sourceStream = Readable.fromWeb(downloadRes.body);
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: sourceStream,
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) return [];
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 sourceStream = Readable2.fromWeb(downloadRes.body);
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: sourceStream,
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
- let bodyStream = Readable2.fromWeb(downloadRes.body);
800
- const throttle = createThrottleTransform(rateLimitBytesPerSec || 0);
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(path.posix.join(targetRoot, rel));
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(path.posix.join(normalizedTargetBaseDir, safeHostDirName(sourceBaseUrl)));
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
- ticker = setInterval(() => {
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
- `\u6587\u4EF6\u6570\uFF1A${files.length}`
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(path.posix.dirname(filePath));
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
- await runWithConcurrency(files, maxConcurrency, async ({ sourcePath, targetPath }) => {
1197
- try {
1198
- if (mode === "incremental") {
1199
- const exists = await existsOnTarget({
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
- targetBaseUrl,
1204
- getTargetToken,
1358
+ sourcePath,
1205
1359
  targetPath,
1206
- timeoutMs: listTimeoutMs
1360
+ getSourceToken,
1361
+ getTargetToken,
1362
+ timeoutMs
1207
1363
  });
1208
- if (exists) {
1209
- skipped++;
1210
- return;
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
- await ensureDirForPath(targetPath);
1214
- await copyOpenListFile({
1215
- sourceTransport,
1216
- targetTransport,
1217
- sourceBaseUrl,
1218
- sourceDavBaseUrl,
1219
- sourceAuth,
1220
- targetBaseUrl,
1221
- targetDavBaseUrl: targetDavBase,
1222
- targetAuth,
1223
- sourcePath,
1224
- targetPath,
1225
- getSourceToken,
1226
- getTargetToken,
1227
- timeoutMs
1228
- });
1229
- ok++;
1230
- } catch (error) {
1231
- fail++;
1232
- logger2.error(error);
1233
- if (allowAutoFallback) {
1234
- const msg = formatErrorMessage(error);
1235
- if (targetTransport === "webdav" && /401|403|MKCOL|PUT/i.test(msg)) targetTransport = "api";
1236
- else if (targetTransport === "api" && /fs\/put|fs\/mkdir|code=/i.test(msg)) targetTransport = "webdav";
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
- webdavPropfindListNames,
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-5QW7XYQX.js";
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
- webdavPropfindListNames
19
- } from "./chunk-YVFRUAZO.js";
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 dirs = Array.from(new Set(
465
- needSync.map(({ remotePath }) => normalizePosixPath(path2.posix.dirname(remotePath)))
466
- ));
467
- const namesByDir = /* @__PURE__ */ new Map();
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
- dirPath,
470
+ filePath: remotePath,
473
471
  timeoutMs: webdavTimeoutMs
474
472
  });
475
- namesByDir.set(dirPath, names);
476
473
  });
477
- needSync = needSync.filter(({ remotePath }) => {
478
- const dirPath = normalizePosixPath(path2.posix.dirname(remotePath));
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
- if (bot && !item.path.includes("/")) {
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: 4e3,
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karin-plugin-qgroup-file2openlist",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "author": "429",
5
5
  "type": "module",
6
6
  "description": "karin plugin for QGroupFile backup",