sliftutils 1.7.10 → 1.7.12
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/.claude/settings.local.json +2 -1
- package/examplestorage/exampleserver.ts +0 -2
- package/index.d.ts +301 -54
- package/misc/dist/environment.ts.cache +3 -3
- package/misc/dist/getSecret.ts.cache +139 -0
- package/misc/dist/strings.ts.cache +13 -0
- package/misc/dist/zip.ts.cache +9 -0
- package/misc/getSecret.ts +38 -16
- package/misc/https/cloudflareHelpers.d.ts +19 -0
- package/misc/https/cloudflareHelpers.ts +69 -0
- package/misc/https/dist/certs.ts.cache +115 -179
- package/misc/https/dist/cloudflareHelpers.ts.cache +68 -0
- package/misc/https/dist/dns.ts.cache +112 -100
- package/misc/https/dist/hostServer.ts.cache +130 -0
- package/misc/https/dist/httpsCerts.ts.cache +24 -5
- package/misc/https/dist/persistentLocalStorage.ts.cache +3 -2
- package/misc/https/dns.d.ts +41 -12
- package/misc/https/dns.ts +104 -101
- package/misc/https/hostServer.d.ts +0 -6
- package/misc/https/hostServer.ts +9 -5
- package/package.json +2 -2
- package/render-utils/dist/observer.tsx.cache +3 -2
- package/spec.txt +0 -90
- package/storage/ArchivesDisk.d.ts +2 -0
- package/storage/ArchivesDisk.ts +7 -3
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +1242 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +513 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +352 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +306 -0
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +435 -0
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +58 -0
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +179 -0
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +149 -0
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +101 -0
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +227 -0
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +104 -0
- package/storage/IArchives.d.ts +47 -15
- package/storage/IArchives.ts +86 -31
- package/storage/backblaze.d.ts +15 -0
- package/storage/backblaze.ts +199 -79
- package/storage/dist/ArchivesDisk.ts.cache +373 -0
- package/storage/dist/FileFolderAPI.tsx.cache +508 -65
- package/storage/dist/IArchives.ts.cache +44 -0
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +8 -2
- package/storage/dist/JSONStorage.ts.cache +6 -2
- package/storage/dist/PendingManager.tsx.cache +1 -1
- package/storage/dist/TransactionStorage.ts.cache +231 -92
- package/storage/dist/backblaze.ts.cache +797 -0
- package/storage/dist/fileSystemPointer.ts.cache +36 -3
- package/storage/dist/remoteFileStorage.ts.cache +495 -0
- package/storage/remoteStorage/ArchivesRemote.d.ts +5 -1
- package/storage/remoteStorage/ArchivesRemote.ts +28 -8
- package/storage/remoteStorage/ArchivesUrl.d.ts +2 -0
- package/storage/remoteStorage/ArchivesUrl.ts +16 -4
- package/storage/remoteStorage/blobStore.d.ts +63 -3
- package/storage/remoteStorage/blobStore.ts +419 -101
- package/storage/remoteStorage/createArchives.d.ts +31 -10
- package/storage/remoteStorage/createArchives.ts +470 -253
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +197 -0
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +85 -0
- package/storage/remoteStorage/dist/accessPage.tsx.cache +222 -0
- package/storage/remoteStorage/dist/blobStore.ts.cache +837 -0
- package/storage/remoteStorage/dist/cliArgs.ts.cache +16 -0
- package/storage/remoteStorage/dist/createArchives.ts.cache +610 -0
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +206 -0
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +223 -0
- package/storage/remoteStorage/dist/storageController.ts.cache +500 -0
- package/storage/remoteStorage/dist/storageServer.ts.cache +106 -0
- package/storage/remoteStorage/dist/storageServerCli.ts.cache +39 -0
- package/storage/remoteStorage/dist/storageServerState.ts.cache +518 -0
- package/storage/remoteStorage/remoteConfig.d.ts +8 -0
- package/storage/remoteStorage/remoteConfig.ts +105 -5
- package/storage/remoteStorage/sourceWrapper.d.ts +41 -0
- package/storage/remoteStorage/sourceWrapper.ts +223 -0
- package/storage/remoteStorage/spec.md +31 -0
- package/storage/remoteStorage/storageController.d.ts +11 -0
- package/storage/remoteStorage/storageController.ts +109 -40
- package/storage/remoteStorage/storageServer.d.ts +0 -5
- package/storage/remoteStorage/storageServer.ts +17 -10
- package/storage/remoteStorage/storageServerCli.ts +0 -3
- package/storage/remoteStorage/storageServerState.d.ts +7 -1
- package/storage/remoteStorage/storageServerState.ts +230 -48
- package/testsite/server.ts +0 -2
- package/yarn.lock +11 -6
package/storage/backblaze.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
"
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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(
|
|
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.
|
|
543
|
-
this.log(err.message
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
581
|
+
let resolveAddresses = await new Promise(resolve => {
|
|
582
|
+
dns.resolve4(hostname, (err, addresses) => {
|
|
583
|
+
resolve(addresses);
|
|
584
|
+
});
|
|
559
585
|
});
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
});
|