karin-plugin-qgroup-file2openlist 0.0.26 → 0.0.27

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-R4MRR5TI.js";
10
+ import "../chunk-DDV3OZXB.js";
11
11
  import {
12
12
  backupOpenListToOpenListCore,
13
13
  formatErrorMessage
14
- } from "../chunk-YVFRUAZO.js";
14
+ } from "../chunk-GDVU2T6R.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-R4MRR5TI.js";
5
+ import "../chunk-DDV3OZXB.js";
6
6
  import {
7
7
  formatErrorMessage,
8
8
  normalizePosixPath
9
- } from "../chunk-YVFRUAZO.js";
9
+ } from "../chunk-GDVU2T6R.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-P5FZDWLZ.js";
4
+ import "../chunk-R4MRR5TI.js";
5
+ import "../chunk-DDV3OZXB.js";
6
+ import "../chunk-GDVU2T6R.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-5HRWYTEM.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-GDVU2T6R.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-DDV3OZXB.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-GDVU2T6R.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-5HRWYTEM.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-P5FZDWLZ.js";
7
+ import "../chunk-R4MRR5TI.js";
8
+ import "../chunk-DDV3OZXB.js";
9
+ import "../chunk-GDVU2T6R.js";
10
10
  import {
11
11
  config
12
12
  } from "../chunk-DA4U55JC.js";
@@ -4,7 +4,7 @@ import {
4
4
  normalizePosixPath,
5
5
  readJsonSafe,
6
6
  writeJsonSafe
7
- } from "./chunk-YVFRUAZO.js";
7
+ } from "./chunk-GDVU2T6R.js";
8
8
  import {
9
9
  config
10
10
  } 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-GDVU2T6R.js";
5
5
  import {
6
6
  dir
7
7
  } from "./chunk-IZS467MR.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 } = {}) => {
@@ -49,9 +52,27 @@ var fetchTextSafely = async (res, maxLen = 500) => {
49
52
  }
50
53
  };
51
54
 
52
- // src/model/openlist/backup.ts
55
+ // src/model/shared/fsJson.ts
56
+ import fs from "fs";
53
57
  import path from "path";
54
- import { logger as logger2 } from "node-karin";
58
+ var ensureDir = (dirPath) => fs.mkdirSync(dirPath, { recursive: true });
59
+ var readJsonSafe = (filePath) => {
60
+ try {
61
+ if (!fs.existsSync(filePath)) return {};
62
+ const raw = fs.readFileSync(filePath, "utf8");
63
+ return raw ? JSON.parse(raw) : {};
64
+ } catch {
65
+ return {};
66
+ }
67
+ };
68
+ var writeJsonSafe = (filePath, data) => {
69
+ ensureDir(path.dirname(filePath));
70
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
71
+ };
72
+
73
+ // src/model/openlist/backup.ts
74
+ import path3 from "path";
75
+ import { logger as logger4 } from "node-karin";
55
76
 
56
77
  // src/model/shared/concurrency.ts
57
78
  var runWithConcurrency = async (items, concurrency, fn) => {
@@ -176,6 +197,7 @@ var buildOpenListAuthHeader = (username, password) => {
176
197
  };
177
198
 
178
199
  // src/model/openlist/api.ts
200
+ import fs3 from "fs";
179
201
  import { Readable } from "stream";
180
202
  import { setTimeout as sleep } from "timers/promises";
181
203
 
@@ -247,7 +269,85 @@ var withGlobalTransferLimit = async (label, fn) => {
247
269
  }
248
270
  };
249
271
 
272
+ // src/model/shared/transferSpool.ts
273
+ import fs2 from "fs";
274
+ import path2 from "path";
275
+ import { randomUUID } from "crypto";
276
+ import { pipeline } from "stream/promises";
277
+ var DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB = 200;
278
+ var MAX_THRESHOLD_MB = 5e4;
279
+ var cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
280
+ var cachedAt2 = 0;
281
+ var CACHE_MS2 = 1e3;
282
+ var clampInt2 = (value, min, max) => Math.min(max, Math.max(min, Math.floor(value)));
283
+ var parseContentLengthHeader = (value) => {
284
+ const raw = String(value ?? "").trim();
285
+ if (!raw) return void 0;
286
+ const n = Number(raw);
287
+ if (!Number.isFinite(n)) return void 0;
288
+ const v = Math.floor(n);
289
+ if (v <= 0) return void 0;
290
+ return v;
291
+ };
292
+ var getLargeFileSpoolThresholdBytes = () => {
293
+ const now = Date.now();
294
+ if (now - cachedAt2 < CACHE_MS2) return cachedThresholdBytes;
295
+ cachedAt2 = now;
296
+ try {
297
+ const cfg = config();
298
+ const raw = cfg?.resourceLimits?.largeFileSpoolThresholdMB;
299
+ if (raw === void 0 || raw === null || raw === "") {
300
+ cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
301
+ return cachedThresholdBytes;
302
+ }
303
+ const n = typeof raw === "number" ? raw : Number(raw);
304
+ if (!Number.isFinite(n)) {
305
+ cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
306
+ return cachedThresholdBytes;
307
+ }
308
+ if (n <= 0) {
309
+ cachedThresholdBytes = 0;
310
+ return cachedThresholdBytes;
311
+ }
312
+ const mb = clampInt2(n, 1, MAX_THRESHOLD_MB);
313
+ cachedThresholdBytes = mb * 1024 * 1024;
314
+ return cachedThresholdBytes;
315
+ } catch {
316
+ cachedThresholdBytes = DEFAULT_LARGE_FILE_SPOOL_THRESHOLD_MB * 1024 * 1024;
317
+ return cachedThresholdBytes;
318
+ }
319
+ };
320
+ var shouldSpoolToDisk = (sizeBytes) => {
321
+ const threshold = getLargeFileSpoolThresholdBytes();
322
+ if (!threshold) return false;
323
+ if (typeof sizeBytes !== "number" || !Number.isFinite(sizeBytes)) return false;
324
+ return Math.floor(sizeBytes) >= threshold;
325
+ };
326
+ var safeFileName = (value) => {
327
+ return String(value ?? "").replaceAll("\0", "").replace(/[^\w.-]+/g, "_").replace(/^_+/, "").slice(0, 50) || "transfer";
328
+ };
329
+ var getTransferTmpDir = () => {
330
+ const tmpDir = path2.join(dir.DataDir, "transfer-tmp");
331
+ ensureDir(tmpDir);
332
+ return tmpDir;
333
+ };
334
+ var createTransferTempFilePath = (prefix) => {
335
+ const tmpDir = getTransferTmpDir();
336
+ const name = `${safeFileName(prefix)}-${Date.now()}-${randomUUID()}.tmp`;
337
+ return path2.join(tmpDir, name);
338
+ };
339
+ var streamToFile = async (readable, filePath, options) => {
340
+ await pipeline(readable, fs2.createWriteStream(filePath), { signal: options?.signal });
341
+ };
342
+ var safeUnlink = async (filePath) => {
343
+ try {
344
+ await fs2.promises.unlink(filePath);
345
+ } catch {
346
+ }
347
+ };
348
+
250
349
  // src/model/openlist/api.ts
350
+ import { logger as logger2 } from "node-karin";
251
351
  var openlistApiReadJson = async (res) => {
252
352
  try {
253
353
  return await res.json();
@@ -441,12 +541,13 @@ var createOpenListApiDirEnsurer = (baseUrl, token, timeoutMs) => {
441
541
  return { ensureDir: ensureDir2 };
442
542
  };
443
543
  var downloadAndUploadByOpenListApiPut = async (params) => {
444
- const { sourceUrl, sourceHeaders, targetBaseUrl, targetToken, targetPath, timeoutMs } = params;
544
+ const { sourceUrl, sourceHeaders, targetBaseUrl, targetToken, targetPath, timeoutMs, expectedSize } = params;
445
545
  const apiBaseUrl = buildOpenListApiBaseUrl(targetBaseUrl);
446
546
  if (!apiBaseUrl) throw new Error("\u76EE\u6807 OpenList API \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5\u76EE\u6807 OpenList \u5730\u5740");
447
547
  await withGlobalTransferLimit(`downloadAndUploadByOpenListApiPut:${targetPath}`, async () => {
448
548
  const controller = new AbortController();
449
549
  const timer = setTimeout(() => controller.abort(), timeoutMs);
550
+ let tmpFilePath;
450
551
  try {
451
552
  let downloadRes;
452
553
  try {
@@ -470,16 +571,28 @@ var downloadAndUploadByOpenListApiPut = async (params) => {
470
571
  const authToken = String(targetToken ?? "").trim();
471
572
  if (authToken) headers.Authorization = authToken;
472
573
  const contentType = downloadRes.headers.get("content-type");
473
- const contentLength = downloadRes.headers.get("content-length");
574
+ const contentLength = parseContentLengthHeader(downloadRes.headers.get("content-length"));
474
575
  if (contentType) headers["Content-Type"] = contentType;
475
- if (contentLength) headers["Content-Length"] = contentLength;
476
- const sourceStream = Readable.fromWeb(downloadRes.body);
576
+ if (contentLength) headers["Content-Length"] = String(contentLength);
577
+ const sizeForDecision = typeof expectedSize === "number" && Number.isFinite(expectedSize) && expectedSize > 0 ? Math.floor(expectedSize) : contentLength;
578
+ const spoolToDisk = shouldSpoolToDisk(sizeForDecision);
477
579
  let putRes;
478
580
  try {
581
+ let body;
582
+ if (spoolToDisk) {
583
+ tmpFilePath = createTransferTempFilePath("openlist-api-put");
584
+ logger2.info(`[\u4F20\u8F93][\u843D\u76D8] \u68C0\u6D4B\u5230\u5927\u6587\u4EF6\uFF0C\u5C06\u5148\u4E0B\u8F7D\u843D\u76D8\u518D\u4E0A\u4F20(API): ${targetPath}`);
585
+ await streamToFile(Readable.fromWeb(downloadRes.body), tmpFilePath, { signal: controller.signal });
586
+ const stat = await fs3.promises.stat(tmpFilePath);
587
+ headers["Content-Length"] = String(stat.size);
588
+ body = fs3.createReadStream(tmpFilePath);
589
+ } else {
590
+ body = Readable.fromWeb(downloadRes.body);
591
+ }
479
592
  putRes = await fetch(`${apiBaseUrl}/fs/put`, {
480
593
  method: "PUT",
481
594
  headers,
482
- body: sourceStream,
595
+ body,
483
596
  // @ts-expect-error Node fetch streaming body requires duplex
484
597
  duplex: "half",
485
598
  redirect: "follow",
@@ -498,13 +611,21 @@ var downloadAndUploadByOpenListApiPut = async (params) => {
498
611
  const msg = json?.message ? ` - ${json.message}` : "";
499
612
  throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: code=${json.code}${msg}`);
500
613
  }
614
+ } catch (error) {
615
+ try {
616
+ controller.abort();
617
+ } catch {
618
+ }
619
+ throw error;
501
620
  } finally {
502
621
  clearTimeout(timer);
622
+ if (tmpFilePath) await safeUnlink(tmpFilePath);
503
623
  }
504
624
  });
505
625
  };
506
626
 
507
627
  // src/model/openlist/webdav.ts
628
+ import fs4 from "fs";
508
629
  import { Readable as Readable2 } from "stream";
509
630
  import { setTimeout as sleep3 } from "timers/promises";
510
631
 
@@ -532,6 +653,7 @@ var createThrottleTransform = (bytesPerSec) => {
532
653
  };
533
654
 
534
655
  // src/model/openlist/webdav.ts
656
+ import { logger as logger3 } from "node-karin";
535
657
  var isRetryableWebDavError = (error) => {
536
658
  const msg = formatErrorMessage(error);
537
659
  return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|UND_ERR|socket hang up/i.test(msg);
@@ -663,6 +785,7 @@ var copyWebDavToWebDav = async (params) => {
663
785
  await withGlobalTransferLimit(`copyWebDavToWebDav:${sourcePath}=>${targetPath}`, async () => {
664
786
  const controller = new AbortController();
665
787
  const timer = setTimeout(() => controller.abort(), timeoutMs);
788
+ let tmpFilePath;
666
789
  try {
667
790
  const sourceUrl = `${sourceDavBaseUrl}${encodePathForUrl(sourcePath)}`;
668
791
  const targetUrl = `${targetDavBaseUrl}${encodePathForUrl(targetPath)}`;
@@ -688,16 +811,27 @@ var copyWebDavToWebDav = async (params) => {
688
811
  if (!downloadRes.body) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
689
812
  const headers = { Authorization: targetAuth };
690
813
  const contentType = downloadRes.headers.get("content-type");
691
- const contentLength = downloadRes.headers.get("content-length");
814
+ const contentLength = parseContentLengthHeader(downloadRes.headers.get("content-length"));
692
815
  if (contentType) headers["Content-Type"] = contentType;
693
- if (contentLength) headers["Content-Length"] = contentLength;
694
- const sourceStream = Readable2.fromWeb(downloadRes.body);
816
+ if (contentLength) headers["Content-Length"] = String(contentLength);
817
+ const spoolToDisk = shouldSpoolToDisk(contentLength);
695
818
  let putRes;
696
819
  try {
820
+ let body;
821
+ if (spoolToDisk) {
822
+ tmpFilePath = createTransferTempFilePath("webdav-copy");
823
+ 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}`);
824
+ await streamToFile(Readable2.fromWeb(downloadRes.body), tmpFilePath, { signal: controller.signal });
825
+ const stat = await fs4.promises.stat(tmpFilePath);
826
+ headers["Content-Length"] = String(stat.size);
827
+ body = fs4.createReadStream(tmpFilePath);
828
+ } else {
829
+ body = Readable2.fromWeb(downloadRes.body);
830
+ }
697
831
  putRes = await fetch(targetUrl, {
698
832
  method: "PUT",
699
833
  headers,
700
- body: sourceStream,
834
+ body,
701
835
  // @ts-expect-error Node fetch streaming body requires duplex
702
836
  duplex: "half",
703
837
  redirect: "follow",
@@ -711,8 +845,15 @@ var copyWebDavToWebDav = async (params) => {
711
845
  const body = await fetchTextSafely(putRes);
712
846
  throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${putRes.status} ${putRes.statusText}${body ? ` - ${body}` : ""}`);
713
847
  }
848
+ } catch (error) {
849
+ try {
850
+ controller.abort();
851
+ } catch {
852
+ }
853
+ throw error;
714
854
  } finally {
715
855
  clearTimeout(timer);
856
+ if (tmpFilePath) await safeUnlink(tmpFilePath);
716
857
  }
717
858
  });
718
859
  };
@@ -768,10 +909,11 @@ var createWebDavDirEnsurer = (davBaseUrl, auth, timeoutMs) => {
768
909
  return { ensureDir: ensureDir2 };
769
910
  };
770
911
  var downloadAndUploadByWebDav = async (params) => {
771
- const { sourceUrl, sourceHeaders, targetUrl, auth, timeoutMs, rateLimitBytesPerSec } = params;
912
+ const { sourceUrl, sourceHeaders, targetUrl, auth, timeoutMs, rateLimitBytesPerSec, expectedSize } = params;
772
913
  await withGlobalTransferLimit(`downloadAndUploadByWebDav:${targetUrl}`, async () => {
773
914
  const controller = new AbortController();
774
915
  const timer = setTimeout(() => controller.abort(), timeoutMs);
916
+ let tmpFilePath;
775
917
  try {
776
918
  let downloadRes;
777
919
  try {
@@ -793,14 +935,26 @@ var downloadAndUploadByWebDav = async (params) => {
793
935
  Authorization: auth
794
936
  };
795
937
  const contentType = downloadRes.headers.get("content-type");
796
- const contentLength = downloadRes.headers.get("content-length");
938
+ const contentLength = parseContentLengthHeader(downloadRes.headers.get("content-length"));
797
939
  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);
940
+ if (contentLength) headers["Content-Length"] = String(contentLength);
941
+ const sizeForDecision = typeof expectedSize === "number" && Number.isFinite(expectedSize) && expectedSize > 0 ? Math.floor(expectedSize) : contentLength;
942
+ const spoolToDisk = shouldSpoolToDisk(sizeForDecision);
802
943
  let putRes;
803
944
  try {
945
+ let bodyStream;
946
+ if (spoolToDisk) {
947
+ tmpFilePath = createTransferTempFilePath("webdav-download-upload");
948
+ logger3.info(`[\u4F20\u8F93][\u843D\u76D8] \u68C0\u6D4B\u5230\u5927\u6587\u4EF6\uFF0C\u5C06\u5148\u4E0B\u8F7D\u843D\u76D8\u518D\u4E0A\u4F20: ${targetUrl}`);
949
+ await streamToFile(Readable2.fromWeb(downloadRes.body), tmpFilePath, { signal: controller.signal });
950
+ const stat = await fs4.promises.stat(tmpFilePath);
951
+ headers["Content-Length"] = String(stat.size);
952
+ bodyStream = fs4.createReadStream(tmpFilePath);
953
+ } else {
954
+ bodyStream = Readable2.fromWeb(downloadRes.body);
955
+ }
956
+ const throttle = createThrottleTransform(rateLimitBytesPerSec || 0);
957
+ if (throttle) bodyStream = bodyStream.pipe(throttle);
804
958
  putRes = await fetch(targetUrl, {
805
959
  method: "PUT",
806
960
  headers,
@@ -819,8 +973,15 @@ var downloadAndUploadByWebDav = async (params) => {
819
973
  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
974
  throw new Error(`\u4E0A\u4F20\u5931\u8D25: ${putRes.status} ${putRes.statusText}${hint}${body ? ` - ${body}` : ""}`);
821
975
  }
976
+ } catch (error) {
977
+ try {
978
+ controller.abort();
979
+ } catch {
980
+ }
981
+ throw error;
822
982
  } finally {
823
983
  clearTimeout(timer);
984
+ if (tmpFilePath) await safeUnlink(tmpFilePath);
824
985
  }
825
986
  });
826
987
  };
@@ -849,66 +1010,7 @@ var normalizeOpenListBackupTransport = (value, fallback) => {
849
1010
  };
850
1011
  var buildTargetPath = (targetRoot, sourcePath) => {
851
1012
  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 };
1013
+ return normalizePosixPath(path3.posix.join(targetRoot, rel));
912
1014
  };
913
1015
  var existsOnTarget = async (params) => {
914
1016
  const { targetTransport, targetDavBaseUrl, targetAuth, targetBaseUrl, getTargetToken, targetPath, timeoutMs } = params;
@@ -1047,7 +1149,7 @@ var backupOpenListToOpenListCore = async (params) => {
1047
1149
  if (!targetDavBaseUrl) throw new Error("\u76EE\u6807\u7AEF OpenList \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 openlistBaseUrl");
1048
1150
  const normalizedSrcDir = normalizePosixPath(String(params.srcDir ?? "/"));
1049
1151
  const normalizedTargetBaseDir = normalizePosixPath(String(params.toDir ?? cfg.openlistTargetDir ?? "/"));
1050
- const targetRoot = params.appendHostDir === false ? normalizedTargetBaseDir : normalizePosixPath(path.posix.join(normalizedTargetBaseDir, safeHostDirName(sourceBaseUrl)));
1152
+ const targetRoot = params.appendHostDir === false ? normalizedTargetBaseDir : normalizePosixPath(path3.posix.join(normalizedTargetBaseDir, safeHostDirName(sourceBaseUrl)));
1051
1153
  const mode = params.mode ?? "incremental";
1052
1154
  const transport = params.transport ?? normalizeOpenListBackupTransport(cfg?.openListBackupTransport, "auto");
1053
1155
  const lockKey = `${sourceBaseUrl} -> ${targetBaseUrl}`;
@@ -1128,46 +1230,12 @@ var backupOpenListToOpenListCore = async (params) => {
1128
1230
  const maxConcurrency = Math.max(1, Math.min(50, Math.floor(params.concurrency || 0) || 3));
1129
1231
  let scannedDirs = 0;
1130
1232
  let scannedFiles = 0;
1233
+ let ok = 0;
1234
+ let skipped = 0;
1235
+ let fail = 0;
1131
1236
  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);
1237
+ const maxFilesLimit = typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : void 0;
1139
1238
  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
1239
  enqueueReport([
1172
1240
  "\u5F00\u59CB\u5907\u4EFD OpenList...",
1173
1241
  `\u6E90\uFF1A${sourceBaseUrl}`,
@@ -1176,14 +1244,13 @@ var backupOpenListToOpenListCore = async (params) => {
1176
1244
  `\u76EE\u6807\u76EE\u5F55\uFF1A${targetRoot}`,
1177
1245
  `\u6A21\u5F0F\uFF1A${mode}`,
1178
1246
  `\u4F20\u8F93\uFF1A${transport}`,
1179
- `\u6587\u4EF6\u6570\uFF1A${files.length}`
1247
+ `\u626B\u63CF\u5E76\u53D1\uFF1A${scanConcurrencyMax}`,
1248
+ `\u590D\u5236\u5E76\u53D1\uFF1A${maxConcurrency}`,
1249
+ `maxFiles\uFF1A${typeof maxFilesLimit === "number" ? maxFilesLimit : "-"}`
1180
1250
  ].join("\n"));
1181
- let skipped = 0;
1182
- let ok = 0;
1183
- let fail = 0;
1184
1251
  const targetDavBase = targetDavBaseUrl;
1185
1252
  const ensureDirForPath = async (filePath) => {
1186
- const dirPath = normalizePosixPath(path.posix.dirname(filePath));
1253
+ const dirPath = normalizePosixPath(path3.posix.dirname(filePath));
1187
1254
  targetTransport = await ensureTargetDir({
1188
1255
  dirPath,
1189
1256
  targetTransport,
@@ -1193,50 +1260,109 @@ var backupOpenListToOpenListCore = async (params) => {
1193
1260
  getApiEnsurer: getTargetDirEnsurerApi
1194
1261
  });
1195
1262
  };
1196
- await runWithConcurrency(files, maxConcurrency, async ({ sourcePath, targetPath }) => {
1197
- try {
1198
- if (mode === "incremental") {
1199
- const exists = await existsOnTarget({
1263
+ const listDirEntries = async (dirPath) => {
1264
+ if (sourceTransport === "webdav") {
1265
+ if (!sourceDavBaseUrl) throw new Error("\u6E90\u7AEF WebDAV \u5730\u5740\u4E0D\u6B63\u786E");
1266
+ return await webdavPropfindListEntries({
1267
+ davBaseUrl: sourceDavBaseUrl,
1268
+ auth: sourceAuth,
1269
+ dirPath,
1270
+ timeoutMs: listTimeoutMs
1271
+ });
1272
+ }
1273
+ return await openlistApiListEntries({
1274
+ baseUrl: sourceBaseUrl,
1275
+ token: await getSourceToken(),
1276
+ dirPath,
1277
+ timeoutMs: listTimeoutMs,
1278
+ perPage: listPerPage
1279
+ });
1280
+ };
1281
+ const executing = /* @__PURE__ */ new Set();
1282
+ const launchCopy = (file) => {
1283
+ const task = (async () => {
1284
+ const { sourcePath, targetPath } = file;
1285
+ try {
1286
+ if (mode === "incremental") {
1287
+ const exists = await existsOnTarget({
1288
+ targetTransport,
1289
+ targetDavBaseUrl: targetDavBase,
1290
+ targetAuth,
1291
+ targetBaseUrl,
1292
+ getTargetToken,
1293
+ targetPath,
1294
+ timeoutMs: listTimeoutMs
1295
+ });
1296
+ if (exists) {
1297
+ skipped++;
1298
+ return;
1299
+ }
1300
+ }
1301
+ await ensureDirForPath(targetPath);
1302
+ await copyOpenListFile({
1303
+ sourceTransport,
1200
1304
  targetTransport,
1305
+ sourceBaseUrl,
1306
+ sourceDavBaseUrl,
1307
+ sourceAuth,
1308
+ targetBaseUrl,
1201
1309
  targetDavBaseUrl: targetDavBase,
1202
1310
  targetAuth,
1203
- targetBaseUrl,
1204
- getTargetToken,
1311
+ sourcePath,
1205
1312
  targetPath,
1206
- timeoutMs: listTimeoutMs
1313
+ getSourceToken,
1314
+ getTargetToken,
1315
+ timeoutMs
1207
1316
  });
1208
- if (exists) {
1209
- skipped++;
1210
- return;
1317
+ ok++;
1318
+ } catch (error) {
1319
+ fail++;
1320
+ logger4.error(error);
1321
+ if (allowAutoFallback) {
1322
+ const msg = formatErrorMessage(error);
1323
+ if (targetTransport === "webdav" && /401|403|MKCOL|PUT/i.test(msg)) targetTransport = "api";
1324
+ else if (targetTransport === "api" && /fs\/put|fs\/mkdir|code=/i.test(msg)) targetTransport = "webdav";
1211
1325
  }
1212
1326
  }
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";
1327
+ })();
1328
+ executing.add(task);
1329
+ task.finally(() => executing.delete(task));
1330
+ return task;
1331
+ };
1332
+ ticker = setInterval(() => {
1333
+ const elapsed = Math.floor((Date.now() - startAt) / 1e3);
1334
+ enqueueReport([
1335
+ `\u5907\u4EFD\u8FDB\u884C\u4E2D\uFF08${elapsed}s\uFF09`,
1336
+ `\u5DF2\u626B\u63CF\u76EE\u5F55\uFF1A${scannedDirs}`,
1337
+ `\u5DF2\u53D1\u73B0\u6587\u4EF6\uFF1A${scannedFiles}${typeof maxFilesLimit === "number" ? `/${maxFilesLimit}` : ""}`,
1338
+ `\u8FDB\u5EA6\uFF1Aok=${ok} skip=${skipped} fail=${fail}`,
1339
+ `\u5E76\u53D1\uFF1AinFlight=${executing.size}/${maxConcurrency}`
1340
+ ].join("\n"));
1341
+ }, 1e4);
1342
+ let pendingDirs = [normalizedSrcDir];
1343
+ while (pendingDirs.length) {
1344
+ const dirPath = pendingDirs.pop();
1345
+ scannedDirs++;
1346
+ const entries = await listDirEntries(dirPath);
1347
+ for (const it of entries) {
1348
+ const sourcePath = normalizePosixPath(path3.posix.join(dirPath, it.name));
1349
+ const targetPath = buildTargetPath(targetRoot, sourcePath);
1350
+ if (it.isDir) {
1351
+ pendingDirs.push(sourcePath);
1352
+ continue;
1353
+ }
1354
+ scannedFiles++;
1355
+ if (typeof maxFilesLimit === "number" && scannedFiles > maxFilesLimit) {
1356
+ pendingDirs = [];
1357
+ break;
1358
+ }
1359
+ launchCopy({ sourcePath, targetPath });
1360
+ if (executing.size >= maxConcurrency) {
1361
+ await Promise.race(executing);
1237
1362
  }
1238
1363
  }
1239
- });
1364
+ }
1365
+ await Promise.all(executing);
1240
1366
  enqueueReport(`\u5907\u4EFD\u5B8C\u6210\uFF1A\u6210\u529F ${ok}\uFF0C\u8DF3\u8FC7 ${skipped}\uFF0C\u5931\u8D25 ${fail}`);
1241
1367
  return { ok, skipped, fail };
1242
1368
  } finally {
@@ -1245,24 +1371,6 @@ var backupOpenListToOpenListCore = async (params) => {
1245
1371
  }
1246
1372
  };
1247
1373
 
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
1374
  export {
1267
1375
  buildOpenListDavBaseUrl,
1268
1376
  buildOpenListAuthHeader,
@@ -1270,13 +1378,13 @@ export {
1270
1378
  safePathSegment,
1271
1379
  encodePathForUrl,
1272
1380
  formatErrorMessage,
1381
+ ensureDir,
1382
+ readJsonSafe,
1383
+ writeJsonSafe,
1273
1384
  webdavPropfindListNames,
1274
1385
  createWebDavDirEnsurer,
1275
1386
  downloadAndUploadByWebDav,
1276
1387
  runWithConcurrency,
1277
1388
  runWithAdaptiveConcurrency,
1278
- backupOpenListToOpenListCore,
1279
- ensureDir,
1280
- readJsonSafe,
1281
- writeJsonSafe
1389
+ backupOpenListToOpenListCore
1282
1390
  };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  syncGroupFilesToOpenListCore
3
- } from "./chunk-BWVJMEKK.js";
3
+ } from "./chunk-R4MRR5TI.js";
4
4
  import {
5
5
  normalizePosixPath,
6
6
  readJsonSafe,
7
7
  writeJsonSafe
8
- } from "./chunk-YVFRUAZO.js";
8
+ } from "./chunk-GDVU2T6R.js";
9
9
  import {
10
10
  config
11
11
  } from "./chunk-DA4U55JC.js";
@@ -2,7 +2,7 @@ import {
2
2
  readGroupSyncState,
3
3
  withGroupSyncLock,
4
4
  writeGroupSyncState
5
- } from "./chunk-5QW7XYQX.js";
5
+ } from "./chunk-DDV3OZXB.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-YVFRUAZO.js";
19
+ } from "./chunk-GDVU2T6R.js";
20
20
  import {
21
21
  config,
22
22
  time
@@ -508,13 +508,14 @@ var syncGroupFilesToOpenListCore = async (params) => {
508
508
  const shouldRefreshUrl = (message) => {
509
509
  return /403|URL已过期|url已过期|url可能已失效|需要重新获取|下载超时/.test(message);
510
510
  };
511
- const transferOne = async (sourceUrl, targetUrl) => {
511
+ const transferOne = async (sourceUrl, targetUrl, expectedSize) => {
512
512
  await downloadAndUploadByWebDav({
513
513
  sourceUrl,
514
514
  targetUrl,
515
515
  auth,
516
516
  timeoutMs: transferTimeoutMs,
517
- rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0
517
+ rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0,
518
+ expectedSize
518
519
  });
519
520
  };
520
521
  report && await report("\u5F00\u59CB\u4E0B\u8F7D\u5E76\u4E0A\u4F20\u5230 OpenList\uFF0C\u8BF7\u7A0D\u5019..");
@@ -532,7 +533,7 @@ var syncGroupFilesToOpenListCore = async (params) => {
532
533
  await dirEnsurer.ensureDir(remoteDir);
533
534
  const currentUrl = item.url;
534
535
  if (!currentUrl) throw new Error("\u7F3A\u5C11\u4E0B\u8F7D URL");
535
- await transferOne(currentUrl, targetUrl);
536
+ await transferOne(currentUrl, targetUrl, item.size);
536
537
  okCount++;
537
538
  succeeded = true;
538
539
  lastError = void 0;
@@ -817,7 +818,8 @@ var handleGroupFileUploadedAutoBackup = (e) => {
817
818
  targetUrl,
818
819
  auth,
819
820
  timeoutMs: transferTimeoutMs,
820
- rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0
821
+ rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0,
822
+ expectedSize: size
821
823
  });
822
824
  break;
823
825
  } 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.27",
4
4
  "author": "429",
5
5
  "type": "module",
6
6
  "description": "karin plugin for QGroupFile backup",