sliftutils 1.7.10 → 1.7.11

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.
Files changed (83) hide show
  1. package/examplestorage/exampleserver.ts +0 -2
  2. package/index.d.ts +290 -54
  3. package/misc/dist/environment.ts.cache +3 -3
  4. package/misc/dist/getSecret.ts.cache +139 -0
  5. package/misc/dist/strings.ts.cache +13 -0
  6. package/misc/dist/zip.ts.cache +9 -0
  7. package/misc/getSecret.ts +38 -16
  8. package/misc/https/cloudflareHelpers.d.ts +19 -0
  9. package/misc/https/cloudflareHelpers.ts +69 -0
  10. package/misc/https/dist/certs.ts.cache +115 -179
  11. package/misc/https/dist/cloudflareHelpers.ts.cache +68 -0
  12. package/misc/https/dist/dns.ts.cache +112 -100
  13. package/misc/https/dist/hostServer.ts.cache +130 -0
  14. package/misc/https/dist/httpsCerts.ts.cache +24 -5
  15. package/misc/https/dist/persistentLocalStorage.ts.cache +3 -2
  16. package/misc/https/dns.d.ts +41 -12
  17. package/misc/https/dns.ts +104 -101
  18. package/misc/https/hostServer.d.ts +0 -6
  19. package/misc/https/hostServer.ts +9 -5
  20. package/package.json +2 -2
  21. package/render-utils/dist/observer.tsx.cache +3 -2
  22. package/spec.txt +0 -90
  23. package/storage/ArchivesDisk.d.ts +2 -0
  24. package/storage/ArchivesDisk.ts +7 -3
  25. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +1242 -0
  26. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +513 -0
  27. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +352 -0
  28. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +306 -0
  29. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +435 -0
  30. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +58 -0
  31. package/storage/BulkDatabase2/dist/blockCache.ts.cache +179 -0
  32. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +149 -0
  33. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +101 -0
  34. package/storage/BulkDatabase2/dist/streamLog.ts.cache +227 -0
  35. package/storage/BulkDatabase2/dist/syncClient.ts.cache +104 -0
  36. package/storage/IArchives.d.ts +47 -15
  37. package/storage/IArchives.ts +86 -31
  38. package/storage/backblaze.d.ts +15 -0
  39. package/storage/backblaze.ts +199 -79
  40. package/storage/dist/ArchivesDisk.ts.cache +373 -0
  41. package/storage/dist/FileFolderAPI.tsx.cache +508 -65
  42. package/storage/dist/IArchives.ts.cache +44 -0
  43. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +8 -2
  44. package/storage/dist/JSONStorage.ts.cache +6 -2
  45. package/storage/dist/PendingManager.tsx.cache +1 -1
  46. package/storage/dist/TransactionStorage.ts.cache +231 -92
  47. package/storage/dist/backblaze.ts.cache +797 -0
  48. package/storage/dist/fileSystemPointer.ts.cache +36 -3
  49. package/storage/dist/remoteFileStorage.ts.cache +495 -0
  50. package/storage/remoteStorage/ArchivesRemote.d.ts +5 -1
  51. package/storage/remoteStorage/ArchivesRemote.ts +28 -8
  52. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -0
  53. package/storage/remoteStorage/ArchivesUrl.ts +16 -4
  54. package/storage/remoteStorage/blobStore.d.ts +52 -3
  55. package/storage/remoteStorage/blobStore.ts +414 -101
  56. package/storage/remoteStorage/createArchives.d.ts +31 -10
  57. package/storage/remoteStorage/createArchives.ts +470 -253
  58. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +197 -0
  59. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +85 -0
  60. package/storage/remoteStorage/dist/accessPage.tsx.cache +222 -0
  61. package/storage/remoteStorage/dist/blobStore.ts.cache +837 -0
  62. package/storage/remoteStorage/dist/cliArgs.ts.cache +16 -0
  63. package/storage/remoteStorage/dist/createArchives.ts.cache +610 -0
  64. package/storage/remoteStorage/dist/remoteConfig.ts.cache +206 -0
  65. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +223 -0
  66. package/storage/remoteStorage/dist/storageController.ts.cache +500 -0
  67. package/storage/remoteStorage/dist/storageServer.ts.cache +106 -0
  68. package/storage/remoteStorage/dist/storageServerCli.ts.cache +39 -0
  69. package/storage/remoteStorage/dist/storageServerState.ts.cache +518 -0
  70. package/storage/remoteStorage/remoteConfig.d.ts +8 -0
  71. package/storage/remoteStorage/remoteConfig.ts +105 -5
  72. package/storage/remoteStorage/sourceWrapper.d.ts +41 -0
  73. package/storage/remoteStorage/sourceWrapper.ts +223 -0
  74. package/storage/remoteStorage/spec.md +31 -0
  75. package/storage/remoteStorage/storageController.d.ts +11 -0
  76. package/storage/remoteStorage/storageController.ts +109 -40
  77. package/storage/remoteStorage/storageServer.d.ts +0 -5
  78. package/storage/remoteStorage/storageServer.ts +17 -10
  79. package/storage/remoteStorage/storageServerCli.ts +0 -3
  80. package/storage/remoteStorage/storageServerState.d.ts +7 -1
  81. package/storage/remoteStorage/storageServerState.ts +230 -48
  82. package/testsite/server.ts +0 -2
  83. package/yarn.lock +11 -6
@@ -9,7 +9,7 @@ import debugbreak from "debugbreak";
9
9
  import dns from "dns";
10
10
  import { getSecret } from "../misc/getSecret";
11
11
  import { httpsRequest } from "socket-function/src/https";
12
- import { IArchives, ArchivesConfig, assertValidLastModified } from "./IArchives";
12
+ import { IArchives, ArchivesConfig, assertValidLastModified, IMMUTABLE_CACHE_TIME } from "./IArchives";
13
13
 
14
14
  type BackblazeCreds = {
15
15
  applicationKeyId: string;
@@ -55,7 +55,6 @@ const getAPI = lazy(async () => {
55
55
  }
56
56
  try {
57
57
  let url = auth.apiUrl + "/b2api/v2/" + name;
58
- let time = Date.now();
59
58
  let result = await httpsRequest(url, Buffer.from(JSON.stringify(arg)), type, undefined, {
60
59
  headers: {
61
60
  Authorization: auth.authorizationToken,
@@ -221,7 +220,7 @@ const getAPI = lazy(async () => {
221
220
  };
222
221
  action: string;
223
222
  uploadTimestamp: number;
224
- }>("b2_get_file_info", "POST");
223
+ }>("b2_get_file_info", "POST", "noAccountId");
225
224
 
226
225
  const listFileNames = createB2Function<{
227
226
  bucketId: string;
@@ -367,6 +366,7 @@ const getAPI = lazy(async () => {
367
366
  getDownloadAuthorization,
368
367
  getDownloadURL,
369
368
  apiUrl: auth.apiUrl,
369
+ allowed: auth.allowed,
370
370
  };
371
371
  });
372
372
 
@@ -379,7 +379,11 @@ export class ArchivesBackblaze implements IArchives {
379
379
  public?: boolean;
380
380
  immutable?: boolean;
381
381
  cacheTime?: number;
382
- }) { }
382
+ allowedOrigins?: string[];
383
+ }) {
384
+ // Get the api, to setup cors
385
+ void this.getBucketAPI();
386
+ }
383
387
 
384
388
  private bucketName = this.config.bucketName.replaceAll(/[^\w\d]/g, "-");
385
389
  private bucketId = "";
@@ -402,13 +406,13 @@ export class ArchivesBackblaze implements IArchives {
402
406
 
403
407
  let cacheTime = this.config.cacheTime ?? 0;
404
408
  if (this.config.immutable) {
405
- cacheTime = 86400 * 1000;
409
+ cacheTime = IMMUTABLE_CACHE_TIME;
406
410
  }
407
411
 
408
412
  // ALWAYS set access control, as we can make urls for private buckets with getDownloadAuthorization
409
413
  let desiredCorsRules = [{
410
414
  corsRuleName: "allowAll",
411
- allowedOrigins: ["https"],
415
+ allowedOrigins: this.config.allowedOrigins ?? ["https"],
412
416
  allowedOperations: ["b2_download_file_by_id", "b2_download_file_by_name"],
413
417
  allowedHeaders: ["range"],
414
418
  exposeHeaders: ["x-bz-content-sha1"],
@@ -421,24 +425,34 @@ export class ArchivesBackblaze implements IArchives {
421
425
 
422
426
 
423
427
  let exists = false;
424
- try {
425
- await api.createBucket({
426
- bucketName: this.bucketName,
427
- bucketType: this.config.public ? "allPublic" : "allPrivate",
428
- lifecycleRules: [{
429
- "daysFromUploadingToHiding": null,
430
- // Keep files for 7 days, which should be enough time to recover accidental hiding.
431
- "daysFromHidingToDeleting": 7,
432
- "fileNamePrefix": ""
433
- }],
434
- corsRules: desiredCorsRules,
435
- bucketInfo
436
- });
437
- } catch (e: any) {
438
- if (!e.stack.includes(`"duplicate_bucket_name"`)) {
439
- throw e;
428
+ let retries = 0;
429
+ while (true) {
430
+ try {
431
+ await api.createBucket({
432
+ bucketName: this.bucketName,
433
+ bucketType: this.config.public ? "allPublic" : "allPrivate",
434
+ lifecycleRules: [{
435
+ "daysFromUploadingToHiding": null,
436
+ // Keep files for 7 days, which should be enough time to recover accidental hiding.
437
+ "daysFromHidingToDeleting": 7,
438
+ "fileNamePrefix": ""
439
+ }],
440
+ corsRules: desiredCorsRules,
441
+ bucketInfo
442
+ });
443
+ } catch (e: any) {
444
+ if (!e.stack.includes(`"duplicate_bucket_name"`)) {
445
+ if (retries < 3) {
446
+ console.error(`Backblaze create bucket failed, retrying in 5s: ${e.stack}`);
447
+ await delay(5000);
448
+ retries++;
449
+ continue;
450
+ }
451
+ throw e;
452
+ }
453
+ exists = true;
440
454
  }
441
- exists = true;
455
+ break;
442
456
  }
443
457
 
444
458
  let bucketList = await api.listBuckets({
@@ -484,47 +498,57 @@ export class ArchivesBackblaze implements IArchives {
484
498
  return api;
485
499
  });
486
500
 
501
+ private currentReset: Promise<void> | undefined;
502
+
487
503
  // Keep track of when we last reset because of a 503
488
504
  private last503Reset = 0;
489
505
  // IMPORTANT! We must always CATCH AROUND the apiRetryLogic, NEVER inside of fnc. Otherwise we won't
490
506
  // be able to recreate the auth token.
507
+ // `context` is a short label (verb + file path) included in every retry/error log so a stuck
508
+ // silent-retry loop is identifiable from the logs.
491
509
  private async apiRetryLogic<T>(
510
+ context: string,
492
511
  fnc: (api: B2Api) => Promise<T>,
493
512
  retries = 3
494
513
  ): Promise<T> {
495
- let api = await this.getBucketAPI();
514
+ let api: B2Api | undefined;
496
515
  try {
516
+ api = await this.getBucketAPI();
497
517
  return await fnc(api);
498
518
  } catch (err: any) {
499
519
  if (retries <= 0) throw err;
500
520
 
501
- // If it's a 503 and it's been a minute since we last reset, then Wait and reset.
521
+ // If it's a 503 and it's been a minute since we last reset, then Wait and reset.
502
522
  if (
503
- (err.stack.includes(`503`)
523
+ (err.stack.includes(`"status": 503`)
504
524
  || err.stack.includes(`"service_unavailable"`)
505
525
  || err.stack.includes(`"internal_error"`)
506
526
  || err.stack.includes(`ENOBUFS`)
507
527
  ) && Date.now() - this.last503Reset > 60 * 1000) {
508
- console.error("503 error, waiting a minute and resetting: " + err.message);
509
- this.log("503 error, waiting a minute and resetting: " + err.message);
510
- await delay(10 * 1000);
511
- // We check again in case, and in the very likely case that this is being run in parallel, we only want to reset once.
512
- if (Date.now() - this.last503Reset > 60 * 1000) {
513
- this.log("Resetting getAPI and getBucketAPI: " + err.message);
514
- this.last503Reset = Date.now();
515
- getAPI.reset();
516
- this.getBucketAPI.reset();
517
- }
518
- return this.apiRetryLogic(fnc, retries - 1);
528
+ // Backblaze is so flaky that we're just going to warn here.
529
+ console.warn(`[${context}] Backblaze error, waiting and resetting: ${err.message}`);
530
+ this.log(`[${context}] Backblaze error, waiting and resetting: ${err.message}`);
531
+ this.currentReset = this.currentReset || (async () => {
532
+ await delay(10 * 1000);
533
+ // We check again in case, and in the very likely case that this is being run in parallel, we only want to reset once.
534
+ if (Date.now() - this.last503Reset > 60 * 1000) {
535
+ this.log(`[${context}] Resetting getAPI and getBucketAPI: ${err.message}`);
536
+ this.last503Reset = Date.now();
537
+ getAPI.reset();
538
+ this.getBucketAPI.reset();
539
+ }
540
+ })().finally(() => this.currentReset = undefined);
541
+ await this.currentReset;
542
+ return this.apiRetryLogic(context, fnc, retries - 1);
519
543
  }
520
544
 
521
545
  // If the error is that the authorization token is invalid, reset getBucketAPI and getAPI
522
546
  // If the error is that the bucket isn't found, reset getBucketAPI
523
547
  if (err.stack.includes(`"expired_auth_token"`)) {
524
- this.log("Authorization token expired");
548
+ this.log(`[${context}] Authorization token expired`);
525
549
  getAPI.reset();
526
550
  this.getBucketAPI.reset();
527
- return this.apiRetryLogic(fnc, retries - 1);
551
+ return this.apiRetryLogic(context, fnc, retries - 1);
528
552
  }
529
553
 
530
554
  if (
@@ -539,29 +563,30 @@ export class ArchivesBackblaze implements IArchives {
539
563
  || err.stack.includes(`ECONNREFUSED`)
540
564
  || err.stack.includes(`ENOBUFS`)
541
565
  ) {
542
- console.error("Retrying in 5s: " + err.message);
543
- this.log(err.message + " retrying in 5s");
566
+ console.warn(`[${context}] Retrying in 5s: ${err.message}`);
567
+ this.log(`[${context}] ${err.message} retrying in 5s`);
544
568
  await delay(5000);
545
- return this.apiRetryLogic(fnc, retries - 1);
569
+ return this.apiRetryLogic(context, fnc, retries - 1);
546
570
  }
547
571
 
548
572
  if (err.stack.includes(`getaddrinfo ENOTFOUND`)) {
549
- let urlObj = new URL(api.apiUrl);
550
- let hostname = urlObj.hostname;
551
- let lookupAddresses = await new Promise(resolve => {
552
- dns.lookup(hostname, (err, addresses) => {
553
- resolve(addresses);
573
+ if (api) {
574
+ let urlObj = new URL(api.apiUrl);
575
+ let hostname = urlObj.hostname;
576
+ let lookupAddresses = await new Promise(resolve => {
577
+ dns.lookup(hostname, (err, addresses) => {
578
+ resolve(addresses);
579
+ });
554
580
  });
555
- });
556
- let resolveAddresses = await new Promise(resolve => {
557
- dns.resolve4(hostname, (err, addresses) => {
558
- resolve(addresses);
581
+ let resolveAddresses = await new Promise(resolve => {
582
+ dns.resolve4(hostname, (err, addresses) => {
583
+ resolve(addresses);
584
+ });
559
585
  });
560
- });
561
- console.error(`getaddrinfo ENOTFOUND ${hostname}`, { lookupAddresses, resolveAddresses, apiUrl: api.apiUrl, fullError: err.stack });
586
+ console.error(`[${context}] getaddrinfo ENOTFOUND ${hostname}`, { lookupAddresses, resolveAddresses, apiUrl: api.apiUrl, fullError: err.stack });
587
+ }
562
588
  }
563
589
 
564
- // TODO: Handle if the bucket is deleted?
565
590
  throw err;
566
591
  }
567
592
  }
@@ -576,11 +601,28 @@ export class ArchivesBackblaze implements IArchives {
576
601
  setTimeout(downloadPoll, 5000);
577
602
  };
578
603
  setTimeout(downloadPoll, 5000);
579
- let result = await this.apiRetryLogic(async (api) => {
604
+ let result = await this.apiRetryLogic(`get ${fileName}`, async (api) => {
605
+ let range = config?.range;
606
+ if (range) {
607
+ let fileInfo = await this.getInfo(fileName);
608
+ if (!fileInfo) throw new Error(`File ${fileName} not found`);
609
+ let rangeStart = range.start;
610
+ let rangeEnd = Math.min(range.end, fileInfo.size);
611
+ // NOTE: I think if we request nothing, it confuses Backblaze and ends up giving us the entire file.
612
+ if (rangeEnd <= rangeStart) return Buffer.alloc(0);
613
+ let result = await api.downloadFileByName({
614
+ bucketName: this.bucketName,
615
+ fileName,
616
+ range: { start: rangeStart, end: rangeEnd },
617
+ });
618
+ if (result.length !== rangeEnd - rangeStart) {
619
+ console.error(`Backblaze range download returned the wrong number of bytes. Tried to get ${rangeStart}-${rangeEnd}, but received ${rangeStart}-${rangeStart + result.length}. For file: ${fileName}`);
620
+ }
621
+ return result;
622
+ }
580
623
  return await api.downloadFileByName({
581
624
  bucketName: this.bucketName,
582
625
  fileName,
583
- range: config?.range
584
626
  });
585
627
  });
586
628
  let timeStr = formatTime(Date.now() - time);
@@ -594,15 +636,31 @@ export class ArchivesBackblaze implements IArchives {
594
636
  downloading = false;
595
637
  }
596
638
  }
597
- public async get2(fileName: string, config?: { range?: { start: number; end: number; } }): Promise<{ data: Buffer; writeTime: number } | undefined> {
639
+ public async get2(fileName: string, config?: { range?: { start: number; end: number; } }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
598
640
  // B2 downloads don't return the upload time, so this takes a second API call
599
641
  let [data, info] = await Promise.all([this.get(fileName, config), this.getInfo(fileName)]);
600
642
  if (!data || !info) return undefined;
601
- return { data, writeTime: info.writeTime };
643
+ return { data, writeTime: info.writeTime, size: info.size };
602
644
  }
603
645
  public async getConfig(): Promise<ArchivesConfig> {
604
646
  return {};
605
647
  }
648
+ public async hasWriteAccess(): Promise<boolean> {
649
+ try {
650
+ let api = await getAPI();
651
+ // B2 v2 documents allowed as a single object; our type says a list - handle both
652
+ let allowedRaw = api.allowed as unknown;
653
+ let allowedList = (Array.isArray(allowedRaw) && allowedRaw || [allowedRaw]) as typeof api.allowed;
654
+ return allowedList.some(allowed =>
655
+ allowed.capabilities.includes("writeFiles")
656
+ && (!allowed.bucketName || allowed.bucketName === this.bucketName)
657
+ );
658
+ } catch (e) {
659
+ // No / invalid credentials
660
+ this.log(`backblaze hasWriteAccess check failed: ${(e as Error).stack ?? e}`);
661
+ return false;
662
+ }
663
+ }
606
664
  public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<void> {
607
665
  if (config?.lastModified) {
608
666
  assertValidLastModified(config.lastModified);
@@ -613,7 +671,7 @@ export class ArchivesBackblaze implements IArchives {
613
671
  }
614
672
  this.log(`backblaze upload (${formatNumber(data.length)}B) ${fileName}`);
615
673
  let f = fileName;
616
- await this.apiRetryLogic(async (api) => {
674
+ await this.apiRetryLogic(`uploadFile ${fileName}`, async (api) => {
617
675
  await api.uploadFile({ bucketId: this.bucketId, fileName, data: data, });
618
676
  });
619
677
  let existsChecks = 30;
@@ -632,7 +690,7 @@ export class ArchivesBackblaze implements IArchives {
632
690
  public async del(fileName: string): Promise<void> {
633
691
  this.log(`backblaze delete ${fileName}`);
634
692
  try {
635
- await this.apiRetryLogic(async (api) => {
693
+ await this.apiRetryLogic(`hideFile ${fileName}`, async (api) => {
636
694
  await api.hideFile({ bucketId: this.bucketId, fileName: fileName });
637
695
  });
638
696
  } catch (e: any) {
@@ -701,7 +759,7 @@ export class ArchivesBackblaze implements IArchives {
701
759
  dataQueue.unshift(data, secondData);
702
760
 
703
761
 
704
- let uploadInfo = await this.apiRetryLogic(async (api) => {
762
+ let uploadInfo = await this.apiRetryLogic(`startLargeFile ${fileName}`, async (api) => {
705
763
  return await api.startLargeFile({
706
764
  bucketId: this.bucketId,
707
765
  fileName: fileName,
@@ -710,7 +768,7 @@ export class ArchivesBackblaze implements IArchives {
710
768
  });
711
769
  });
712
770
  onError.push(async () => {
713
- await this.apiRetryLogic(async (api) => {
771
+ await this.apiRetryLogic(`cancelLargeFile ${fileName}`, async (api) => {
714
772
  await api.cancelLargeFile({ fileId: uploadInfo.fileId });
715
773
  });
716
774
  });
@@ -747,7 +805,7 @@ export class ArchivesBackblaze implements IArchives {
747
805
  sha1.update(data);
748
806
  let sha1Hex = sha1.digest("hex");
749
807
  partSha1Array.push(sha1Hex);
750
- await this.apiRetryLogic(async (api) => {
808
+ await this.apiRetryLogic(`uploadPart#${partNumber} ${fileName}`, async (api) => {
751
809
  if (!data) throw new Error("Impossible, data is undefined");
752
810
 
753
811
  let timeStr = formatTime(Date.now() - time);
@@ -773,7 +831,7 @@ export class ArchivesBackblaze implements IArchives {
773
831
  }
774
832
  this.log(`Finished uploading large file uploaded ${green(formatNumber(totalBytes))}B`);
775
833
 
776
- await this.apiRetryLogic(async (api) => {
834
+ await this.apiRetryLogic(`finishLargeFile ${fileName}`, async (api) => {
777
835
  await api.finishLargeFile({
778
836
  fileId: uploadInfo.fileId,
779
837
  partSha1Array: partSha1Array,
@@ -793,18 +851,27 @@ export class ArchivesBackblaze implements IArchives {
793
851
  }
794
852
 
795
853
  public async getInfo(fileName: string): Promise<{ writeTime: number; size: number; } | undefined> {
796
- return await this.apiRetryLogic(async (api) => {
797
- let info = await api.listFileNames({ bucketId: this.bucketId, prefix: fileName, });
798
- let file = info.files.find(x => x.fileName === fileName);
799
- if (!file) {
800
- this.log(`Backblaze file not exists ${fileName}`);
801
- return undefined;
854
+ return await this.apiRetryLogic(`getInfo ${fileName}`, async (api) => {
855
+ try {
856
+ // NOTE: Apparently, there's no other way to do this, as the file name does not equal the file ID, and get file info requires the file ID.
857
+ let info = await api.listFileNames({ bucketId: this.bucketId, prefix: fileName, maxFileCount: 10 });
858
+ let file = info.files.find(x => x.fileName === fileName && x.action === "upload");
859
+ if (!file) {
860
+ this.log(`Backblaze file not exists ${fileName}`);
861
+ return undefined;
862
+ }
863
+ this.log(`Backblaze file exists ${fileName}`);
864
+ return {
865
+ writeTime: file.uploadTimestamp,
866
+ size: file.contentLength,
867
+ };
868
+ } catch (e: any) {
869
+ if (e.stack.includes(`file_not_found`)) {
870
+ this.log(`Backblaze file not exists ${fileName}`);
871
+ return undefined;
872
+ }
873
+ throw e;
802
874
  }
803
- this.log(`Backblaze file exists ${fileName}`);
804
- return {
805
- writeTime: file.uploadTimestamp,
806
- size: file.contentLength,
807
- };
808
875
  });
809
876
  }
810
877
 
@@ -814,7 +881,7 @@ export class ArchivesBackblaze implements IArchives {
814
881
  return result.map(x => x.path);
815
882
  }
816
883
  public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
817
- return await this.apiRetryLogic(async (api) => {
884
+ return await this.apiRetryLogic(`findInfo ${prefix}`, async (api) => {
818
885
  if (!config?.shallow && config?.type === "folders") {
819
886
  let allFiles = await this.findInfo(prefix);
820
887
  let allFolders = new Map<string, { path: string; createTime: number; size: number }>();
@@ -861,8 +928,59 @@ export class ArchivesBackblaze implements IArchives {
861
928
  }
862
929
  }
863
930
 
931
+ public async move(config: {
932
+ path: string;
933
+ target: IArchives;
934
+ targetPath: string;
935
+ copyInstead?: boolean;
936
+ }) {
937
+ let { path, target, targetPath } = config;
938
+ // A self move should NOOP (and definitely not copy, and then delete itself!)
939
+ if (target === this && path === targetPath) {
940
+ this.log(`Backblaze move path to itself. Skipping move, as there is no work to do. ${path}`);
941
+ return;
942
+ }
943
+ if (target instanceof ArchivesBackblaze) {
944
+ let targetBucketId = target.bucketId;
945
+ if (targetBucketId === this.bucketId && path === targetPath) return;
946
+ await this.apiRetryLogic(`move ${path} -> ${targetPath}`, async (api) => {
947
+ // Ugh... listing the file name sucks, but... I guess it's still better than
948
+ // downloading and re-uploading the entire file.
949
+ let info = await api.listFileNames({ bucketId: this.bucketId, prefix: path, maxFileCount: 10 });
950
+ let file = info.files.find(x => x.fileName === path);
951
+ if (!file) throw new Error(`File not found to move: ${path}`);
952
+ await api.copyFile({
953
+ sourceFileId: file.fileId,
954
+ fileName: targetPath,
955
+ destinationBucketId: targetBucketId,
956
+ });
957
+ });
958
+ } else {
959
+ let data = await this.get(path);
960
+ if (!data) throw new Error(`File not found to move: ${path}`);
961
+ await target.set(targetPath, data);
962
+ }
963
+
964
+ if (!config.copyInstead) {
965
+ let exists = await this.getInfo(targetPath);
966
+ if (!exists) {
967
+ console.error(`File not found after move. Leaving BOTH files. ${targetPath} was not found. Being moved from ${path}`);
968
+ } else {
969
+ await this.del(path);
970
+ }
971
+ }
972
+ }
973
+
974
+ public async copy(config: {
975
+ path: string;
976
+ target: IArchives;
977
+ targetPath: string;
978
+ }): Promise<void> {
979
+ return this.move({ ...config, copyInstead: true });
980
+ }
981
+
864
982
  public async getURL(path: string) {
865
- return await this.apiRetryLogic(async (api) => {
983
+ return await this.apiRetryLogic(`getURL ${path}`, async (api) => {
866
984
  if (path.startsWith("/")) {
867
985
  path = path.slice(1);
868
986
  }
@@ -884,7 +1002,7 @@ export class ArchivesBackblaze implements IArchives {
884
1002
  fileNamePrefix: string;
885
1003
  authorizationToken: string;
886
1004
  }> {
887
- return await this.apiRetryLogic(async (api) => {
1005
+ return await this.apiRetryLogic(`getDownloadAuthorization ${config.fileNamePrefix ?? ""}`, async (api) => {
888
1006
  return await api.getDownloadAuthorization({
889
1007
  bucketId: this.bucketId,
890
1008
  fileNamePrefix: config.fileNamePrefix ?? "",
@@ -916,7 +1034,8 @@ export const getArchivesBackblazePublicImmutable = cache((domain: string) => {
916
1034
  return new ArchivesBackblaze({
917
1035
  bucketName: domain + "-public-immutable",
918
1036
  public: true,
919
- immutable: true
1037
+ immutable: true,
1038
+ allowedOrigins: [`https://${domain}`, `https://127-0-0-1.${domain}:7007`],
920
1039
  });
921
1040
  });
922
1041
 
@@ -928,5 +1047,6 @@ export const getArchivesBackblazePublic = cache((domain: string) => {
928
1047
  bucketName: domain + "-public",
929
1048
  public: true,
930
1049
  cacheTime: timeInMinute,
1050
+ allowedOrigins: [`https://${domain}`, `https://127-0-0-1.${domain}:7007`],
931
1051
  });
932
1052
  });