@koolbase/core 12.17.0 → 12.19.0
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/dist/cjs/files-controller.d.ts +62 -0
- package/dist/cjs/files-controller.js +160 -0
- package/dist/cjs/files-refresh.d.ts +9 -0
- package/dist/cjs/files-refresh.js +39 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/react/index.d.ts +1 -0
- package/dist/cjs/react/index.js +3 -1
- package/dist/cjs/react/use-files.d.ts +8 -0
- package/dist/cjs/react/use-files.js +37 -0
- package/dist/cjs/storage-errors.d.ts +14 -0
- package/dist/cjs/storage-errors.js +29 -1
- package/dist/cjs/storage-multipart.d.ts +29 -0
- package/dist/cjs/storage-multipart.js +168 -0
- package/dist/cjs/storage-transfer.d.ts +25 -6
- package/dist/cjs/storage-transfer.js +78 -37
- package/dist/cjs/storage.d.ts +40 -0
- package/dist/cjs/storage.js +205 -15
- package/dist/esm/files-controller.d.ts +62 -0
- package/dist/esm/files-controller.js +155 -0
- package/dist/esm/files-refresh.d.ts +9 -0
- package/dist/esm/files-refresh.js +34 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/react/index.d.ts +1 -0
- package/dist/esm/react/index.js +1 -0
- package/dist/esm/react/use-files.d.ts +8 -0
- package/dist/esm/react/use-files.js +34 -0
- package/dist/esm/storage-errors.d.ts +14 -0
- package/dist/esm/storage-errors.js +26 -0
- package/dist/esm/storage-multipart.d.ts +29 -0
- package/dist/esm/storage-multipart.js +164 -0
- package/dist/esm/storage-transfer.d.ts +25 -6
- package/dist/esm/storage-transfer.js +77 -37
- package/dist/esm/storage.d.ts +40 -0
- package/dist/esm/storage.js +205 -15
- package/package.json +1 -1
|
@@ -33,6 +33,7 @@ export declare class KoolbaseUploadTask {
|
|
|
33
33
|
private cancelRequested;
|
|
34
34
|
private abortTransfer;
|
|
35
35
|
private total;
|
|
36
|
+
private _uploadId;
|
|
36
37
|
/**
|
|
37
38
|
* The uploaded file. Rejects with `KoolbaseUploadCancelledError` if it was
|
|
38
39
|
* cancelled, or the error that made it fail.
|
|
@@ -44,6 +45,15 @@ export declare class KoolbaseUploadTask {
|
|
|
44
45
|
constructor();
|
|
45
46
|
/** Where the upload is now. */
|
|
46
47
|
get current(): KoolbaseUploadProgress;
|
|
48
|
+
/**
|
|
49
|
+
* A large file's upload goes in parts, and can be resumed for a day: keep
|
|
50
|
+
* this id, and pass it to `KoolbaseStorage.resumeUpload` with the same
|
|
51
|
+
* file after the app restarts or the upload fails. Null for a file sent
|
|
52
|
+
* in one piece, and until the upload has begun.
|
|
53
|
+
*/
|
|
54
|
+
get uploadId(): string | null;
|
|
55
|
+
/** @internal */
|
|
56
|
+
setUploadId(id: string): void;
|
|
47
57
|
/**
|
|
48
58
|
* Calls `listener` on every change of state and every chunk sent, starting
|
|
49
59
|
* with where the upload is now. Returns a function that stops it.
|
|
@@ -74,15 +84,24 @@ export declare class KoolbaseUploadTask {
|
|
|
74
84
|
}
|
|
75
85
|
/** Runs `body` as the task's upload, starting on the next turn so a caller can listen to every event. */
|
|
76
86
|
export declare function runUploadTask(body: (task: KoolbaseUploadTask) => Promise<UploadResult>): KoolbaseUploadTask;
|
|
77
|
-
interface PutResult {
|
|
87
|
+
export interface PutResult {
|
|
78
88
|
status: number;
|
|
79
89
|
etag: string;
|
|
80
90
|
}
|
|
91
|
+
/** A PUT under way: its outcome, and how to stop it. */
|
|
92
|
+
export interface Transfer {
|
|
93
|
+
done: Promise<PutResult>;
|
|
94
|
+
abort(): void;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* PUTs `body` to `url`, reporting bytes as they are sent. XMLHttpRequest
|
|
98
|
+
* where the host has it (browsers and React Native both report upload
|
|
99
|
+
* progress); fetch elsewhere, where progress jumps from 0 to the total.
|
|
100
|
+
* An aborted transfer rejects with `KoolbaseUploadCancelledError`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function transferBytes(url: string, body: Blob, contentType: string | null, onSent: (sent: number) => void): Transfer;
|
|
81
103
|
/**
|
|
82
|
-
* Sends `body` to a presigned `url` with PUT
|
|
83
|
-
*
|
|
84
|
-
* both report upload progress); fetch elsewhere, where progress jumps from
|
|
85
|
-
* 0 to the total. A cancel aborts it.
|
|
104
|
+
* Sends `body` to a presigned `url` with PUT as the task's one transfer,
|
|
105
|
+
* reporting bytes as they are sent; a cancel aborts it.
|
|
86
106
|
*/
|
|
87
107
|
export declare function putWithProgress(task: KoolbaseUploadTask, url: string, body: Blob, contentType: string): Promise<PutResult>;
|
|
88
|
-
export {};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.KoolbaseUploadTask = void 0;
|
|
11
11
|
exports.runUploadTask = runUploadTask;
|
|
12
|
+
exports.transferBytes = transferBytes;
|
|
12
13
|
exports.putWithProgress = putWithProgress;
|
|
13
14
|
const network_js_1 = require("./network.js");
|
|
14
15
|
const storage_errors_js_1 = require("./storage-errors.js");
|
|
@@ -37,6 +38,7 @@ class KoolbaseUploadTask {
|
|
|
37
38
|
this.cancelRequested = false;
|
|
38
39
|
this.abortTransfer = null;
|
|
39
40
|
this.total = 0;
|
|
41
|
+
this._uploadId = null;
|
|
40
42
|
this._current = progressOf('pending', 0, 0);
|
|
41
43
|
this.result = new Promise((res, rej) => {
|
|
42
44
|
this.resolve = res;
|
|
@@ -50,6 +52,19 @@ class KoolbaseUploadTask {
|
|
|
50
52
|
get current() {
|
|
51
53
|
return this._current;
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* A large file's upload goes in parts, and can be resumed for a day: keep
|
|
57
|
+
* this id, and pass it to `KoolbaseStorage.resumeUpload` with the same
|
|
58
|
+
* file after the app restarts or the upload fails. Null for a file sent
|
|
59
|
+
* in one piece, and until the upload has begun.
|
|
60
|
+
*/
|
|
61
|
+
get uploadId() {
|
|
62
|
+
return this._uploadId;
|
|
63
|
+
}
|
|
64
|
+
/** @internal */
|
|
65
|
+
setUploadId(id) {
|
|
66
|
+
this._uploadId = id;
|
|
67
|
+
}
|
|
53
68
|
/**
|
|
54
69
|
* Calls `listener` on every change of state and every chunk sent, starting
|
|
55
70
|
* with where the upload is now. Returns a function that stops it.
|
|
@@ -145,70 +160,96 @@ function runUploadTask(body) {
|
|
|
145
160
|
return task;
|
|
146
161
|
}
|
|
147
162
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
163
|
+
* PUTs `body` to `url`, reporting bytes as they are sent. XMLHttpRequest
|
|
164
|
+
* where the host has it (browsers and React Native both report upload
|
|
165
|
+
* progress); fetch elsewhere, where progress jumps from 0 to the total.
|
|
166
|
+
* An aborted transfer rejects with `KoolbaseUploadCancelledError`.
|
|
152
167
|
*/
|
|
153
|
-
function
|
|
154
|
-
task.checkCancelled();
|
|
155
|
-
const total = body.size;
|
|
156
|
-
const report = (sent) => {
|
|
157
|
-
if (!task.cancelled)
|
|
158
|
-
task.emit('uploading', Math.min(sent, total));
|
|
159
|
-
};
|
|
160
|
-
report(0);
|
|
168
|
+
function transferBytes(url, body, contentType, onSent) {
|
|
161
169
|
const XHR = globalThis.XMLHttpRequest;
|
|
162
170
|
if (XHR) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
const xhr = new XHR();
|
|
172
|
+
let settled = false;
|
|
173
|
+
let fail = () => undefined;
|
|
174
|
+
const done = new Promise((resolve, reject) => {
|
|
175
|
+
const finish = (fn) => {
|
|
167
176
|
if (settled)
|
|
168
177
|
return;
|
|
169
178
|
settled = true;
|
|
170
|
-
task.setAbort(null);
|
|
171
179
|
fn();
|
|
172
180
|
};
|
|
181
|
+
fail = (e) => finish(() => reject(e));
|
|
173
182
|
xhr.open('PUT', url);
|
|
174
|
-
|
|
183
|
+
if (contentType)
|
|
184
|
+
xhr.setRequestHeader('Content-Type', contentType);
|
|
175
185
|
if (xhr.upload) {
|
|
176
|
-
xhr.upload.onprogress = (ev) =>
|
|
186
|
+
xhr.upload.onprogress = (ev) => onSent(Math.min(ev.loaded, body.size));
|
|
177
187
|
}
|
|
178
|
-
xhr.onload = () =>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
xhr.onerror = () => done(() => reject(task.cancelled ? new storage_errors_js_1.KoolbaseUploadCancelledError() : new network_js_1.KoolbaseNetworkError(url, new Error('the upload did not complete'))));
|
|
184
|
-
xhr.onabort = () => done(() => reject(new storage_errors_js_1.KoolbaseUploadCancelledError()));
|
|
185
|
-
task.setAbort(() => {
|
|
186
|
-
xhr.abort();
|
|
187
|
-
// Some hosts do not fire onabort for an aborted send.
|
|
188
|
-
done(() => reject(new storage_errors_js_1.KoolbaseUploadCancelledError()));
|
|
189
|
-
});
|
|
188
|
+
xhr.onload = () => finish(() => resolve({ status: xhr.status, etag: xhr.getResponseHeader('etag') ?? '' }));
|
|
189
|
+
xhr.onerror = () => fail(new network_js_1.KoolbaseNetworkError(url, new Error('the upload did not complete')));
|
|
190
|
+
xhr.onabort = () => fail(new storage_errors_js_1.KoolbaseUploadCancelledError());
|
|
191
|
+
onSent(0);
|
|
190
192
|
xhr.send(body);
|
|
191
193
|
});
|
|
194
|
+
return {
|
|
195
|
+
done,
|
|
196
|
+
abort: () => {
|
|
197
|
+
xhr.abort();
|
|
198
|
+
// Some hosts do not fire onabort for an aborted send.
|
|
199
|
+
fail(new storage_errors_js_1.KoolbaseUploadCancelledError());
|
|
200
|
+
},
|
|
201
|
+
};
|
|
192
202
|
}
|
|
193
203
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
let aborted = false;
|
|
205
|
+
onSent(0);
|
|
206
|
+
const done = fetch(url, {
|
|
196
207
|
method: 'PUT',
|
|
197
|
-
headers: { 'Content-Type': contentType },
|
|
208
|
+
headers: contentType ? { 'Content-Type': contentType } : {},
|
|
198
209
|
body,
|
|
199
210
|
signal: controller?.signal,
|
|
200
211
|
})
|
|
201
212
|
.then((res) => {
|
|
202
|
-
|
|
213
|
+
if (aborted)
|
|
214
|
+
throw new storage_errors_js_1.KoolbaseUploadCancelledError();
|
|
215
|
+
onSent(body.size);
|
|
203
216
|
return { status: res.status, etag: res.headers.get('etag') ?? '' };
|
|
204
217
|
})
|
|
205
218
|
.catch((e) => {
|
|
206
|
-
if (
|
|
219
|
+
if (aborted || e?.name === 'AbortError')
|
|
207
220
|
throw new storage_errors_js_1.KoolbaseUploadCancelledError();
|
|
208
|
-
}
|
|
209
221
|
if (e instanceof storage_errors_js_1.KoolbaseStorageError)
|
|
210
222
|
throw e;
|
|
211
223
|
throw new network_js_1.KoolbaseNetworkError(url, e);
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
done,
|
|
227
|
+
abort: () => {
|
|
228
|
+
aborted = true;
|
|
229
|
+
controller?.abort();
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Sends `body` to a presigned `url` with PUT as the task's one transfer,
|
|
235
|
+
* reporting bytes as they are sent; a cancel aborts it.
|
|
236
|
+
*/
|
|
237
|
+
function putWithProgress(task, url, body, contentType) {
|
|
238
|
+
task.checkCancelled();
|
|
239
|
+
const total = body.size;
|
|
240
|
+
const report = (sent) => {
|
|
241
|
+
if (!task.cancelled)
|
|
242
|
+
task.emit('uploading', Math.min(sent, total));
|
|
243
|
+
};
|
|
244
|
+
const t = transferBytes(url, body, contentType, report);
|
|
245
|
+
task.setAbort(() => t.abort());
|
|
246
|
+
return t.done
|
|
247
|
+
.then((r) => {
|
|
248
|
+
task.checkCancelled();
|
|
249
|
+
return r;
|
|
250
|
+
})
|
|
251
|
+
.catch((e) => {
|
|
252
|
+
throw task.cancelled ? new storage_errors_js_1.KoolbaseUploadCancelledError() : e;
|
|
212
253
|
})
|
|
213
254
|
.finally(() => task.setAbort(null));
|
|
214
255
|
}
|
package/dist/cjs/storage.d.ts
CHANGED
|
@@ -72,6 +72,46 @@ export declare class KoolbaseStorage {
|
|
|
72
72
|
*/
|
|
73
73
|
startUpload(options: UploadOptions): KoolbaseUploadTask;
|
|
74
74
|
private runUpload;
|
|
75
|
+
/**
|
|
76
|
+
* Files larger than this go up in parts (S3): each part retried on its
|
|
77
|
+
* own, the upload resumable for a day with `resumeUpload`. 16 MiB.
|
|
78
|
+
*/
|
|
79
|
+
static multipartThreshold: number;
|
|
80
|
+
/**
|
|
81
|
+
* Resumes a large file's upload that failed or was interrupted -- the
|
|
82
|
+
* network dropped, the app was closed -- sending only the parts that did
|
|
83
|
+
* not arrive. `uploadId` is the failed task's `uploadId`; `file` must be
|
|
84
|
+
* the same file. Within a day of the upload beginning.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const task = Koolbase.storage.startUpload({ bucket: 'video', path: 'talk.mp4', file });
|
|
88
|
+
* saveForLater(task.uploadId); // once the upload has begun
|
|
89
|
+
* // ... after a restart:
|
|
90
|
+
* await Koolbase.storage.resumeUpload({ bucket: 'video', uploadId, file }).result;
|
|
91
|
+
*/
|
|
92
|
+
resumeUpload(options: {
|
|
93
|
+
bucket: string;
|
|
94
|
+
uploadId: string;
|
|
95
|
+
file: UploadOptions['file'];
|
|
96
|
+
metadata?: Record<string, string>;
|
|
97
|
+
overwrite?: boolean;
|
|
98
|
+
}): KoolbaseUploadTask;
|
|
99
|
+
private runInParts;
|
|
100
|
+
/**
|
|
101
|
+
* Downloads a file's bytes, reporting them as they arrive (S3):
|
|
102
|
+
* `onProgress(received, total)`, total null when the size is not known.
|
|
103
|
+
* Aborting `signal` stops it with `KoolbaseDownloadCancelledError`.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* const blob = await Koolbase.storage.download({ bucket: 'video', path: 'talk.mp4', onProgress: (r, t) => setPct(t ? r / t : 0) });
|
|
107
|
+
*/
|
|
108
|
+
download(options: {
|
|
109
|
+
bucket: string;
|
|
110
|
+
path: string;
|
|
111
|
+
versionId?: string;
|
|
112
|
+
onProgress?: (received: number, total: number | null) => void;
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}): Promise<Blob>;
|
|
75
115
|
/**
|
|
76
116
|
* One page of a bucket's files. The bucket's view rule is applied to every
|
|
77
117
|
* file: the page holds only files the signed-in person may view. Pass the
|
package/dist/cjs/storage.js
CHANGED
|
@@ -6,7 +6,16 @@ const network_js_1 = require("./network.js");
|
|
|
6
6
|
const errors_js_1 = require("./errors.js");
|
|
7
7
|
const storage_errors_js_2 = require("./storage-errors.js");
|
|
8
8
|
const storage_transfer_js_1 = require("./storage-transfer.js");
|
|
9
|
+
const storage_multipart_js_1 = require("./storage-multipart.js");
|
|
10
|
+
const storage_errors_js_3 = require("./storage-errors.js");
|
|
11
|
+
const files_refresh_js_1 = require("./files-refresh.js");
|
|
12
|
+
const network_js_2 = require("./network.js");
|
|
9
13
|
const storage_files_js_1 = require("./storage-files.js");
|
|
14
|
+
/** An upload task whose success reloads the bucket's file lists (storage S4). */
|
|
15
|
+
function listsReload(task, bucket) {
|
|
16
|
+
task.result.then(() => { void (0, files_refresh_js_1.refreshBucketFiles)(bucket); }, () => { });
|
|
17
|
+
return task;
|
|
18
|
+
}
|
|
10
19
|
// --- Cloudflare image-transform URL helpers -------------------------------
|
|
11
20
|
// Module-private — callers use KoolbaseStorage.publicUrl / publicUrlForObject.
|
|
12
21
|
function clampInt(v, min, max) {
|
|
@@ -129,7 +138,7 @@ class KoolbaseStorage {
|
|
|
129
138
|
* const { object } = await task.result;
|
|
130
139
|
*/
|
|
131
140
|
startUpload(options) {
|
|
132
|
-
return (0, storage_transfer_js_1.runUploadTask)((task) => this.runUpload(options, task));
|
|
141
|
+
return listsReload((0, storage_transfer_js_1.runUploadTask)((task) => this.runUpload(options, task)), options.bucket);
|
|
133
142
|
}
|
|
134
143
|
async runUpload(options, task) {
|
|
135
144
|
task.checkCancelled();
|
|
@@ -144,6 +153,19 @@ class KoolbaseStorage {
|
|
|
144
153
|
: options.file.type;
|
|
145
154
|
if (isBlob)
|
|
146
155
|
task.setTotal(options.file.size);
|
|
156
|
+
// A browser hands us the bytes already: a File from an input is a Blob,
|
|
157
|
+
// and fetching its .uri would be fetching undefined. React Native hands
|
|
158
|
+
// us a local URI, and its fetch resolves that into a Blob -- read first,
|
|
159
|
+
// since its size decides how it is sent (S3).
|
|
160
|
+
const fileBlob = isBlob
|
|
161
|
+
? options.file
|
|
162
|
+
: await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
|
|
163
|
+
task.setTotal(fileBlob.size);
|
|
164
|
+
task.checkCancelled();
|
|
165
|
+
// Large files go in parts: resumable, each part retried (S3).
|
|
166
|
+
if (fileBlob.size > KoolbaseStorage.multipartThreshold) {
|
|
167
|
+
return this.runInParts(options.bucket, options, task, fileBlob, contentType, null);
|
|
168
|
+
}
|
|
147
169
|
// ─── Step 1: Get presigned upload URL ───
|
|
148
170
|
const urlRes = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
|
|
149
171
|
method: 'POST',
|
|
@@ -165,16 +187,7 @@ class KoolbaseStorage {
|
|
|
165
187
|
task.checkCancelled();
|
|
166
188
|
// ─── Step 2: Upload directly to R2 ───
|
|
167
189
|
// R2 presigned URLs expect raw binary, NOT multipart/form-data.
|
|
168
|
-
//
|
|
169
|
-
// A browser hands us the bytes already: a File from an input is a Blob,
|
|
170
|
-
// and fetching its .uri would be fetching undefined. React Native hands
|
|
171
|
-
// us a local URI, and its fetch resolves that into a Blob — which is why
|
|
172
|
-
// the two hosts differ here and nowhere else in this method.
|
|
173
|
-
const fileBlob = isBlob
|
|
174
|
-
? options.file
|
|
175
|
-
: await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
|
|
176
190
|
const fileSize = fileBlob.size;
|
|
177
|
-
task.setTotal(fileSize);
|
|
178
191
|
const uploadRes = await (0, storage_transfer_js_1.putWithProgress)(task, upload_url, fileBlob, contentType);
|
|
179
192
|
if (uploadRes.status < 200 || uploadRes.status >= 300) {
|
|
180
193
|
// R2 PUT errors don't follow the Koolbase error shape — surface as a
|
|
@@ -222,6 +235,173 @@ class KoolbaseStorage {
|
|
|
222
235
|
const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
|
|
223
236
|
return { object, downloadUrl };
|
|
224
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Resumes a large file's upload that failed or was interrupted -- the
|
|
240
|
+
* network dropped, the app was closed -- sending only the parts that did
|
|
241
|
+
* not arrive. `uploadId` is the failed task's `uploadId`; `file` must be
|
|
242
|
+
* the same file. Within a day of the upload beginning.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* const task = Koolbase.storage.startUpload({ bucket: 'video', path: 'talk.mp4', file });
|
|
246
|
+
* saveForLater(task.uploadId); // once the upload has begun
|
|
247
|
+
* // ... after a restart:
|
|
248
|
+
* await Koolbase.storage.resumeUpload({ bucket: 'video', uploadId, file }).result;
|
|
249
|
+
*/
|
|
250
|
+
resumeUpload(options) {
|
|
251
|
+
return listsReload((0, storage_transfer_js_1.runUploadTask)(async (task) => {
|
|
252
|
+
const isBlob = typeof Blob !== 'undefined' && options.file instanceof Blob;
|
|
253
|
+
const blob = isBlob
|
|
254
|
+
? options.file
|
|
255
|
+
: await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
|
|
256
|
+
task.setTotal(blob.size);
|
|
257
|
+
return this.runInParts(options.bucket, options, task, blob, '', options.uploadId);
|
|
258
|
+
}), options.bucket);
|
|
259
|
+
}
|
|
260
|
+
async runInParts(bucket, options, task, file, contentType, resumeId) {
|
|
261
|
+
const base = `${this.config.baseUrl}/v1/sdk/storage/uploads`;
|
|
262
|
+
const json = async () => ({ ...(await this.buildHeaders()), 'Content-Type': 'application/json' });
|
|
263
|
+
const overwrite = options.overwrite ?? false;
|
|
264
|
+
let plan;
|
|
265
|
+
if (resumeId) {
|
|
266
|
+
const res = await (0, network_js_1.sessionFetch)(`${base}?bucket=${encodeURIComponent(bucket)}&upload_id=${encodeURIComponent(resumeId)}`, {
|
|
267
|
+
headers: await this.buildHeaders(),
|
|
268
|
+
});
|
|
269
|
+
if (!res.ok)
|
|
270
|
+
throw await this.error(res, 'Failed to resume the upload');
|
|
271
|
+
plan = (await res.json());
|
|
272
|
+
if (plan.size !== file.size) {
|
|
273
|
+
throw new storage_errors_js_2.KoolbaseStorageError(`This is not the file the upload began with: it has ${file.size} bytes, the upload ${plan.size}`, 'validation_error');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const res = await (0, network_js_1.sessionFetch)(base, {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: await json(),
|
|
280
|
+
body: JSON.stringify({ bucket, path: options.path, content_type: contentType, size: file.size, overwrite }),
|
|
281
|
+
});
|
|
282
|
+
if (!res.ok)
|
|
283
|
+
throw await this.error(res, 'Failed to begin the upload');
|
|
284
|
+
plan = (await res.json());
|
|
285
|
+
}
|
|
286
|
+
task.setUploadId(plan.upload_id);
|
|
287
|
+
task.checkCancelled();
|
|
288
|
+
await (0, storage_multipart_js_1.sendParts)(task, plan, file, {
|
|
289
|
+
links: async (parts) => {
|
|
290
|
+
const res = await (0, network_js_1.sessionFetch)(`${base}/parts`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: await json(),
|
|
293
|
+
body: JSON.stringify({ bucket, upload_id: plan.upload_id, parts }),
|
|
294
|
+
});
|
|
295
|
+
if (!res.ok)
|
|
296
|
+
throw await this.error(res, 'Failed to get upload links');
|
|
297
|
+
return (await res.json()).links;
|
|
298
|
+
},
|
|
299
|
+
abort: async () => {
|
|
300
|
+
await (0, network_js_1.sessionFetch)(base, {
|
|
301
|
+
method: 'DELETE',
|
|
302
|
+
headers: await json(),
|
|
303
|
+
body: JSON.stringify({ bucket, upload_id: plan.upload_id }),
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
// The parts have arrived: the server joins and checks them. No longer
|
|
308
|
+
// cancellable.
|
|
309
|
+
task.verifying();
|
|
310
|
+
const body = { bucket, upload_id: plan.upload_id, overwrite };
|
|
311
|
+
if (options.metadata !== undefined)
|
|
312
|
+
body.metadata = options.metadata;
|
|
313
|
+
const done = await (0, network_js_1.sessionFetch)(`${base}/complete`, { method: 'POST', headers: await json(), body: JSON.stringify(body) });
|
|
314
|
+
if (!done.ok)
|
|
315
|
+
throw await this.error(done, 'Failed to complete the upload');
|
|
316
|
+
const object = mapObjectFromServer(await done.json());
|
|
317
|
+
const downloadUrl = await this.getDownloadUrl(bucket, object.path);
|
|
318
|
+
return { object, downloadUrl };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Downloads a file's bytes, reporting them as they arrive (S3):
|
|
322
|
+
* `onProgress(received, total)`, total null when the size is not known.
|
|
323
|
+
* Aborting `signal` stops it with `KoolbaseDownloadCancelledError`.
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* const blob = await Koolbase.storage.download({ bucket: 'video', path: 'talk.mp4', onProgress: (r, t) => setPct(t ? r / t : 0) });
|
|
327
|
+
*/
|
|
328
|
+
async download(options) {
|
|
329
|
+
if (options.signal?.aborted)
|
|
330
|
+
throw new storage_errors_js_3.KoolbaseDownloadCancelledError();
|
|
331
|
+
const url = await this.getDownloadUrl(options.bucket, options.path, options.versionId);
|
|
332
|
+
const report = options.onProgress ?? (() => undefined);
|
|
333
|
+
const XHR = globalThis.XMLHttpRequest;
|
|
334
|
+
if (XHR) {
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
const xhr = new XHR();
|
|
337
|
+
let settled = false;
|
|
338
|
+
const finish = (fn) => {
|
|
339
|
+
if (settled)
|
|
340
|
+
return;
|
|
341
|
+
settled = true;
|
|
342
|
+
options.signal?.removeEventListener('abort', stop);
|
|
343
|
+
fn();
|
|
344
|
+
};
|
|
345
|
+
const stop = () => {
|
|
346
|
+
xhr.abort();
|
|
347
|
+
finish(() => reject(new storage_errors_js_3.KoolbaseDownloadCancelledError()));
|
|
348
|
+
};
|
|
349
|
+
options.signal?.addEventListener('abort', stop);
|
|
350
|
+
xhr.open('GET', url);
|
|
351
|
+
xhr.responseType = 'blob';
|
|
352
|
+
xhr.onprogress = (ev) => report(ev.loaded, ev.lengthComputable ? ev.total : null);
|
|
353
|
+
xhr.onload = () => finish(() => {
|
|
354
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
355
|
+
reject(new storage_errors_js_2.KoolbaseStorageError(`Download failed: ${xhr.status}`));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const blob = xhr.response;
|
|
359
|
+
report(blob.size, blob.size);
|
|
360
|
+
resolve(blob);
|
|
361
|
+
});
|
|
362
|
+
xhr.onerror = () => finish(() => reject(new network_js_2.KoolbaseNetworkError(url, new Error('the download did not complete'))));
|
|
363
|
+
xhr.onabort = () => finish(() => reject(new storage_errors_js_3.KoolbaseDownloadCancelledError()));
|
|
364
|
+
xhr.send();
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
let res;
|
|
368
|
+
try {
|
|
369
|
+
res = await fetch(url, { signal: options.signal });
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
if (options.signal?.aborted)
|
|
373
|
+
throw new storage_errors_js_3.KoolbaseDownloadCancelledError();
|
|
374
|
+
throw new network_js_2.KoolbaseNetworkError(url, e);
|
|
375
|
+
}
|
|
376
|
+
if (!res.ok)
|
|
377
|
+
throw new storage_errors_js_2.KoolbaseStorageError(`Download failed: ${res.status}`);
|
|
378
|
+
const total = Number(res.headers.get('content-length')) || null;
|
|
379
|
+
const reader = res.body?.getReader?.();
|
|
380
|
+
if (!reader) {
|
|
381
|
+
const blob = await res.blob();
|
|
382
|
+
report(blob.size, blob.size);
|
|
383
|
+
return blob;
|
|
384
|
+
}
|
|
385
|
+
const chunks = [];
|
|
386
|
+
let received = 0;
|
|
387
|
+
report(0, total);
|
|
388
|
+
try {
|
|
389
|
+
for (;;) {
|
|
390
|
+
const { done, value } = await reader.read();
|
|
391
|
+
if (done)
|
|
392
|
+
break;
|
|
393
|
+
chunks.push(value);
|
|
394
|
+
received += value.length;
|
|
395
|
+
report(received, total);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
if (options.signal?.aborted)
|
|
400
|
+
throw new storage_errors_js_3.KoolbaseDownloadCancelledError();
|
|
401
|
+
throw new network_js_2.KoolbaseNetworkError(url, e);
|
|
402
|
+
}
|
|
403
|
+
return new Blob(chunks, { type: res.headers.get('content-type') ?? '' });
|
|
404
|
+
}
|
|
225
405
|
/**
|
|
226
406
|
* One page of a bucket's files. The bucket's view rule is applied to every
|
|
227
407
|
* file: the page holds only files the signed-in person may view. Pass the
|
|
@@ -292,7 +472,9 @@ class KoolbaseStorage {
|
|
|
292
472
|
if (!res.ok) {
|
|
293
473
|
throw await this.error(res, op === 'move' ? 'Failed to move the file' : 'Failed to copy the file');
|
|
294
474
|
}
|
|
295
|
-
|
|
475
|
+
const moved = mapObjectFromServer(await res.json());
|
|
476
|
+
void (0, files_refresh_js_1.refreshBucketFiles)(o.bucket, ...(o.toBucket && o.toBucket !== o.bucket ? [o.toBucket] : []));
|
|
477
|
+
return moved;
|
|
296
478
|
}
|
|
297
479
|
/**
|
|
298
480
|
* Apply a partial metadata update to an existing object. Returns the
|
|
@@ -340,6 +522,7 @@ class KoolbaseStorage {
|
|
|
340
522
|
throw await this.error(res, 'Failed to update metadata');
|
|
341
523
|
}
|
|
342
524
|
const raw = await res.json();
|
|
525
|
+
void (0, files_refresh_js_1.refreshBucketFiles)(bucket);
|
|
343
526
|
return mapObjectFromServer(raw);
|
|
344
527
|
}
|
|
345
528
|
/**
|
|
@@ -471,11 +654,11 @@ class KoolbaseStorage {
|
|
|
471
654
|
},
|
|
472
655
|
body: JSON.stringify({ bucket, path }),
|
|
473
656
|
});
|
|
474
|
-
if (res.
|
|
657
|
+
if (res.ok) {
|
|
658
|
+
void (0, files_refresh_js_1.refreshBucketFiles)(bucket);
|
|
475
659
|
return;
|
|
476
|
-
if (!res.ok) {
|
|
477
|
-
throw await this.error(res, 'Failed to delete file');
|
|
478
660
|
}
|
|
661
|
+
throw await this.error(res, 'Failed to delete file');
|
|
479
662
|
}
|
|
480
663
|
/**
|
|
481
664
|
* List all versions of a file path, newest-first. Returns a flat list
|
|
@@ -532,7 +715,9 @@ class KoolbaseStorage {
|
|
|
532
715
|
if (!res.ok) {
|
|
533
716
|
throw await this.error(res, 'Failed to restore version');
|
|
534
717
|
}
|
|
535
|
-
|
|
718
|
+
const restored = mapObjectFromServer(await res.json());
|
|
719
|
+
void (0, files_refresh_js_1.refreshBucketFiles)(bucket);
|
|
720
|
+
return restored;
|
|
536
721
|
}
|
|
537
722
|
/**
|
|
538
723
|
* Hard-remove a single history version — both the metadata row and
|
|
@@ -555,6 +740,11 @@ class KoolbaseStorage {
|
|
|
555
740
|
}
|
|
556
741
|
}
|
|
557
742
|
exports.KoolbaseStorage = KoolbaseStorage;
|
|
743
|
+
/**
|
|
744
|
+
* Files larger than this go up in parts (S3): each part retried on its
|
|
745
|
+
* own, the upload resumable for a day with `resumeUpload`. 16 MiB.
|
|
746
|
+
*/
|
|
747
|
+
KoolbaseStorage.multipartThreshold = 16 * 1024 * 1024;
|
|
558
748
|
/**
|
|
559
749
|
* Maps the snake_case server JSON to the camelCase {@link KoolbaseObject}.
|
|
560
750
|
* Defensive: missing or null `metadata` (older / non-Koolbase responses)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { KoolbaseFileInfo, KoolbaseFilePage, KoolbaseFileSort, ListFilesOptions } from './storage-files.js';
|
|
2
|
+
export type FilesStatus = 'loading' | 'loaded' | 'error';
|
|
3
|
+
/** Which files, in what order. The bucket's view rule decides the rest. */
|
|
4
|
+
export interface FilesQuery {
|
|
5
|
+
/** Only files whose path starts with this, taken literally. */
|
|
6
|
+
prefix?: string;
|
|
7
|
+
/** By path unless set. */
|
|
8
|
+
sort?: KoolbaseFileSort;
|
|
9
|
+
/** Z to A, newest first, largest first. */
|
|
10
|
+
descending?: boolean;
|
|
11
|
+
/** Files per page: 50 unless set, at most 100. */
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface FilesState {
|
|
15
|
+
readonly status: FilesStatus;
|
|
16
|
+
readonly files: readonly KoolbaseFileInfo[];
|
|
17
|
+
/** Why the first load failed, while status is 'error'. */
|
|
18
|
+
readonly error: unknown;
|
|
19
|
+
/** True while refresh() runs. */
|
|
20
|
+
readonly refreshing: boolean;
|
|
21
|
+
/** True while loadMore() runs. */
|
|
22
|
+
readonly loadingMore: boolean;
|
|
23
|
+
/** Whether there is a page past the last one loaded. True before the first load. */
|
|
24
|
+
readonly hasMore: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare const initialFilesState: FilesState;
|
|
27
|
+
/** One string per distinct listing. */
|
|
28
|
+
export declare function filesQueryKey(bucket: string, query?: FilesQuery): string;
|
|
29
|
+
type FilesReader = {
|
|
30
|
+
listFiles(options: ListFilesOptions): Promise<KoolbaseFilePage>;
|
|
31
|
+
};
|
|
32
|
+
export declare class KoolbaseFilesController {
|
|
33
|
+
readonly bucket: string;
|
|
34
|
+
private readonly storage;
|
|
35
|
+
private readonly spec;
|
|
36
|
+
private state;
|
|
37
|
+
private readonly listeners;
|
|
38
|
+
private generation;
|
|
39
|
+
private disposed;
|
|
40
|
+
private pageOne;
|
|
41
|
+
private laterPages;
|
|
42
|
+
private cursor;
|
|
43
|
+
private unregister;
|
|
44
|
+
constructor(storage: FilesReader, bucket: string, query?: FilesQuery);
|
|
45
|
+
getState(): FilesState;
|
|
46
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
47
|
+
subscribe(listener: () => void): () => void;
|
|
48
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
49
|
+
load(): Promise<void>;
|
|
50
|
+
/** Page one again. What is shown stays while it runs, and stays if it fails. */
|
|
51
|
+
refresh(): Promise<void>;
|
|
52
|
+
/** The next page, appended. Does nothing unless loaded with a page to come. */
|
|
53
|
+
loadMore(): Promise<void>;
|
|
54
|
+
/** Stops all updates. Results still in flight are dropped when they land. */
|
|
55
|
+
dispose(): void;
|
|
56
|
+
private firstPage;
|
|
57
|
+
private publish;
|
|
58
|
+
private options;
|
|
59
|
+
private isStale;
|
|
60
|
+
private set;
|
|
61
|
+
}
|
|
62
|
+
export {};
|