@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
|
@@ -32,6 +32,7 @@ export class KoolbaseUploadTask {
|
|
|
32
32
|
this.cancelRequested = false;
|
|
33
33
|
this.abortTransfer = null;
|
|
34
34
|
this.total = 0;
|
|
35
|
+
this._uploadId = null;
|
|
35
36
|
this._current = progressOf('pending', 0, 0);
|
|
36
37
|
this.result = new Promise((res, rej) => {
|
|
37
38
|
this.resolve = res;
|
|
@@ -45,6 +46,19 @@ export class KoolbaseUploadTask {
|
|
|
45
46
|
get current() {
|
|
46
47
|
return this._current;
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* A large file's upload goes in parts, and can be resumed for a day: keep
|
|
51
|
+
* this id, and pass it to `KoolbaseStorage.resumeUpload` with the same
|
|
52
|
+
* file after the app restarts or the upload fails. Null for a file sent
|
|
53
|
+
* in one piece, and until the upload has begun.
|
|
54
|
+
*/
|
|
55
|
+
get uploadId() {
|
|
56
|
+
return this._uploadId;
|
|
57
|
+
}
|
|
58
|
+
/** @internal */
|
|
59
|
+
setUploadId(id) {
|
|
60
|
+
this._uploadId = id;
|
|
61
|
+
}
|
|
48
62
|
/**
|
|
49
63
|
* Calls `listener` on every change of state and every chunk sent, starting
|
|
50
64
|
* with where the upload is now. Returns a function that stops it.
|
|
@@ -139,70 +153,96 @@ export function runUploadTask(body) {
|
|
|
139
153
|
return task;
|
|
140
154
|
}
|
|
141
155
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
156
|
+
* PUTs `body` to `url`, reporting bytes as they are sent. XMLHttpRequest
|
|
157
|
+
* where the host has it (browsers and React Native both report upload
|
|
158
|
+
* progress); fetch elsewhere, where progress jumps from 0 to the total.
|
|
159
|
+
* An aborted transfer rejects with `KoolbaseUploadCancelledError`.
|
|
146
160
|
*/
|
|
147
|
-
export function
|
|
148
|
-
task.checkCancelled();
|
|
149
|
-
const total = body.size;
|
|
150
|
-
const report = (sent) => {
|
|
151
|
-
if (!task.cancelled)
|
|
152
|
-
task.emit('uploading', Math.min(sent, total));
|
|
153
|
-
};
|
|
154
|
-
report(0);
|
|
161
|
+
export function transferBytes(url, body, contentType, onSent) {
|
|
155
162
|
const XHR = globalThis.XMLHttpRequest;
|
|
156
163
|
if (XHR) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
const xhr = new XHR();
|
|
165
|
+
let settled = false;
|
|
166
|
+
let fail = () => undefined;
|
|
167
|
+
const done = new Promise((resolve, reject) => {
|
|
168
|
+
const finish = (fn) => {
|
|
161
169
|
if (settled)
|
|
162
170
|
return;
|
|
163
171
|
settled = true;
|
|
164
|
-
task.setAbort(null);
|
|
165
172
|
fn();
|
|
166
173
|
};
|
|
174
|
+
fail = (e) => finish(() => reject(e));
|
|
167
175
|
xhr.open('PUT', url);
|
|
168
|
-
|
|
176
|
+
if (contentType)
|
|
177
|
+
xhr.setRequestHeader('Content-Type', contentType);
|
|
169
178
|
if (xhr.upload) {
|
|
170
|
-
xhr.upload.onprogress = (ev) =>
|
|
179
|
+
xhr.upload.onprogress = (ev) => onSent(Math.min(ev.loaded, body.size));
|
|
171
180
|
}
|
|
172
|
-
xhr.onload = () =>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
177
|
-
xhr.onerror = () => done(() => reject(task.cancelled ? new KoolbaseUploadCancelledError() : new KoolbaseNetworkError(url, new Error('the upload did not complete'))));
|
|
178
|
-
xhr.onabort = () => done(() => reject(new KoolbaseUploadCancelledError()));
|
|
179
|
-
task.setAbort(() => {
|
|
180
|
-
xhr.abort();
|
|
181
|
-
// Some hosts do not fire onabort for an aborted send.
|
|
182
|
-
done(() => reject(new KoolbaseUploadCancelledError()));
|
|
183
|
-
});
|
|
181
|
+
xhr.onload = () => finish(() => resolve({ status: xhr.status, etag: xhr.getResponseHeader('etag') ?? '' }));
|
|
182
|
+
xhr.onerror = () => fail(new KoolbaseNetworkError(url, new Error('the upload did not complete')));
|
|
183
|
+
xhr.onabort = () => fail(new KoolbaseUploadCancelledError());
|
|
184
|
+
onSent(0);
|
|
184
185
|
xhr.send(body);
|
|
185
186
|
});
|
|
187
|
+
return {
|
|
188
|
+
done,
|
|
189
|
+
abort: () => {
|
|
190
|
+
xhr.abort();
|
|
191
|
+
// Some hosts do not fire onabort for an aborted send.
|
|
192
|
+
fail(new KoolbaseUploadCancelledError());
|
|
193
|
+
},
|
|
194
|
+
};
|
|
186
195
|
}
|
|
187
196
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
let aborted = false;
|
|
198
|
+
onSent(0);
|
|
199
|
+
const done = fetch(url, {
|
|
190
200
|
method: 'PUT',
|
|
191
|
-
headers: { 'Content-Type': contentType },
|
|
201
|
+
headers: contentType ? { 'Content-Type': contentType } : {},
|
|
192
202
|
body,
|
|
193
203
|
signal: controller?.signal,
|
|
194
204
|
})
|
|
195
205
|
.then((res) => {
|
|
196
|
-
|
|
206
|
+
if (aborted)
|
|
207
|
+
throw new KoolbaseUploadCancelledError();
|
|
208
|
+
onSent(body.size);
|
|
197
209
|
return { status: res.status, etag: res.headers.get('etag') ?? '' };
|
|
198
210
|
})
|
|
199
211
|
.catch((e) => {
|
|
200
|
-
if (
|
|
212
|
+
if (aborted || e?.name === 'AbortError')
|
|
201
213
|
throw new KoolbaseUploadCancelledError();
|
|
202
|
-
}
|
|
203
214
|
if (e instanceof KoolbaseStorageError)
|
|
204
215
|
throw e;
|
|
205
216
|
throw new KoolbaseNetworkError(url, e);
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
done,
|
|
220
|
+
abort: () => {
|
|
221
|
+
aborted = true;
|
|
222
|
+
controller?.abort();
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Sends `body` to a presigned `url` with PUT as the task's one transfer,
|
|
228
|
+
* reporting bytes as they are sent; a cancel aborts it.
|
|
229
|
+
*/
|
|
230
|
+
export function putWithProgress(task, url, body, contentType) {
|
|
231
|
+
task.checkCancelled();
|
|
232
|
+
const total = body.size;
|
|
233
|
+
const report = (sent) => {
|
|
234
|
+
if (!task.cancelled)
|
|
235
|
+
task.emit('uploading', Math.min(sent, total));
|
|
236
|
+
};
|
|
237
|
+
const t = transferBytes(url, body, contentType, report);
|
|
238
|
+
task.setAbort(() => t.abort());
|
|
239
|
+
return t.done
|
|
240
|
+
.then((r) => {
|
|
241
|
+
task.checkCancelled();
|
|
242
|
+
return r;
|
|
243
|
+
})
|
|
244
|
+
.catch((e) => {
|
|
245
|
+
throw task.cancelled ? new KoolbaseUploadCancelledError() : e;
|
|
206
246
|
})
|
|
207
247
|
.finally(() => task.setAbort(null));
|
|
208
248
|
}
|
package/dist/esm/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/esm/storage.js
CHANGED
|
@@ -3,7 +3,16 @@ import { sessionFetch } from './network.js';
|
|
|
3
3
|
import { KoolbaseUnauthenticatedError } from './errors.js';
|
|
4
4
|
import { KoolbaseStorageError, koolbaseStorageErrorFromResponse, } from './storage-errors.js';
|
|
5
5
|
import { putWithProgress, runUploadTask } from './storage-transfer.js';
|
|
6
|
+
import { sendParts } from './storage-multipart.js';
|
|
7
|
+
import { KoolbaseDownloadCancelledError } from './storage-errors.js';
|
|
8
|
+
import { refreshBucketFiles } from './files-refresh.js';
|
|
9
|
+
import { KoolbaseNetworkError } from './network.js';
|
|
6
10
|
import { fileInfoFromJson, filePageFromJson, listFilesQuery, } from './storage-files.js';
|
|
11
|
+
/** An upload task whose success reloads the bucket's file lists (storage S4). */
|
|
12
|
+
function listsReload(task, bucket) {
|
|
13
|
+
task.result.then(() => { void refreshBucketFiles(bucket); }, () => { });
|
|
14
|
+
return task;
|
|
15
|
+
}
|
|
7
16
|
// --- Cloudflare image-transform URL helpers -------------------------------
|
|
8
17
|
// Module-private — callers use KoolbaseStorage.publicUrl / publicUrlForObject.
|
|
9
18
|
function clampInt(v, min, max) {
|
|
@@ -126,7 +135,7 @@ export class KoolbaseStorage {
|
|
|
126
135
|
* const { object } = await task.result;
|
|
127
136
|
*/
|
|
128
137
|
startUpload(options) {
|
|
129
|
-
return runUploadTask((task) => this.runUpload(options, task));
|
|
138
|
+
return listsReload(runUploadTask((task) => this.runUpload(options, task)), options.bucket);
|
|
130
139
|
}
|
|
131
140
|
async runUpload(options, task) {
|
|
132
141
|
task.checkCancelled();
|
|
@@ -141,6 +150,19 @@ export class KoolbaseStorage {
|
|
|
141
150
|
: options.file.type;
|
|
142
151
|
if (isBlob)
|
|
143
152
|
task.setTotal(options.file.size);
|
|
153
|
+
// A browser hands us the bytes already: a File from an input is a Blob,
|
|
154
|
+
// and fetching its .uri would be fetching undefined. React Native hands
|
|
155
|
+
// us a local URI, and its fetch resolves that into a Blob -- read first,
|
|
156
|
+
// since its size decides how it is sent (S3).
|
|
157
|
+
const fileBlob = isBlob
|
|
158
|
+
? options.file
|
|
159
|
+
: await (await sessionFetch(options.file.uri)).blob();
|
|
160
|
+
task.setTotal(fileBlob.size);
|
|
161
|
+
task.checkCancelled();
|
|
162
|
+
// Large files go in parts: resumable, each part retried (S3).
|
|
163
|
+
if (fileBlob.size > KoolbaseStorage.multipartThreshold) {
|
|
164
|
+
return this.runInParts(options.bucket, options, task, fileBlob, contentType, null);
|
|
165
|
+
}
|
|
144
166
|
// ─── Step 1: Get presigned upload URL ───
|
|
145
167
|
const urlRes = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
|
|
146
168
|
method: 'POST',
|
|
@@ -162,16 +184,7 @@ export class KoolbaseStorage {
|
|
|
162
184
|
task.checkCancelled();
|
|
163
185
|
// ─── Step 2: Upload directly to R2 ───
|
|
164
186
|
// R2 presigned URLs expect raw binary, NOT multipart/form-data.
|
|
165
|
-
//
|
|
166
|
-
// A browser hands us the bytes already: a File from an input is a Blob,
|
|
167
|
-
// and fetching its .uri would be fetching undefined. React Native hands
|
|
168
|
-
// us a local URI, and its fetch resolves that into a Blob — which is why
|
|
169
|
-
// the two hosts differ here and nowhere else in this method.
|
|
170
|
-
const fileBlob = isBlob
|
|
171
|
-
? options.file
|
|
172
|
-
: await (await sessionFetch(options.file.uri)).blob();
|
|
173
187
|
const fileSize = fileBlob.size;
|
|
174
|
-
task.setTotal(fileSize);
|
|
175
188
|
const uploadRes = await putWithProgress(task, upload_url, fileBlob, contentType);
|
|
176
189
|
if (uploadRes.status < 200 || uploadRes.status >= 300) {
|
|
177
190
|
// R2 PUT errors don't follow the Koolbase error shape — surface as a
|
|
@@ -219,6 +232,173 @@ export class KoolbaseStorage {
|
|
|
219
232
|
const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
|
|
220
233
|
return { object, downloadUrl };
|
|
221
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Resumes a large file's upload that failed or was interrupted -- the
|
|
237
|
+
* network dropped, the app was closed -- sending only the parts that did
|
|
238
|
+
* not arrive. `uploadId` is the failed task's `uploadId`; `file` must be
|
|
239
|
+
* the same file. Within a day of the upload beginning.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* const task = Koolbase.storage.startUpload({ bucket: 'video', path: 'talk.mp4', file });
|
|
243
|
+
* saveForLater(task.uploadId); // once the upload has begun
|
|
244
|
+
* // ... after a restart:
|
|
245
|
+
* await Koolbase.storage.resumeUpload({ bucket: 'video', uploadId, file }).result;
|
|
246
|
+
*/
|
|
247
|
+
resumeUpload(options) {
|
|
248
|
+
return listsReload(runUploadTask(async (task) => {
|
|
249
|
+
const isBlob = typeof Blob !== 'undefined' && options.file instanceof Blob;
|
|
250
|
+
const blob = isBlob
|
|
251
|
+
? options.file
|
|
252
|
+
: await (await sessionFetch(options.file.uri)).blob();
|
|
253
|
+
task.setTotal(blob.size);
|
|
254
|
+
return this.runInParts(options.bucket, options, task, blob, '', options.uploadId);
|
|
255
|
+
}), options.bucket);
|
|
256
|
+
}
|
|
257
|
+
async runInParts(bucket, options, task, file, contentType, resumeId) {
|
|
258
|
+
const base = `${this.config.baseUrl}/v1/sdk/storage/uploads`;
|
|
259
|
+
const json = async () => ({ ...(await this.buildHeaders()), 'Content-Type': 'application/json' });
|
|
260
|
+
const overwrite = options.overwrite ?? false;
|
|
261
|
+
let plan;
|
|
262
|
+
if (resumeId) {
|
|
263
|
+
const res = await sessionFetch(`${base}?bucket=${encodeURIComponent(bucket)}&upload_id=${encodeURIComponent(resumeId)}`, {
|
|
264
|
+
headers: await this.buildHeaders(),
|
|
265
|
+
});
|
|
266
|
+
if (!res.ok)
|
|
267
|
+
throw await this.error(res, 'Failed to resume the upload');
|
|
268
|
+
plan = (await res.json());
|
|
269
|
+
if (plan.size !== file.size) {
|
|
270
|
+
throw new KoolbaseStorageError(`This is not the file the upload began with: it has ${file.size} bytes, the upload ${plan.size}`, 'validation_error');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const res = await sessionFetch(base, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: await json(),
|
|
277
|
+
body: JSON.stringify({ bucket, path: options.path, content_type: contentType, size: file.size, overwrite }),
|
|
278
|
+
});
|
|
279
|
+
if (!res.ok)
|
|
280
|
+
throw await this.error(res, 'Failed to begin the upload');
|
|
281
|
+
plan = (await res.json());
|
|
282
|
+
}
|
|
283
|
+
task.setUploadId(plan.upload_id);
|
|
284
|
+
task.checkCancelled();
|
|
285
|
+
await sendParts(task, plan, file, {
|
|
286
|
+
links: async (parts) => {
|
|
287
|
+
const res = await sessionFetch(`${base}/parts`, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: await json(),
|
|
290
|
+
body: JSON.stringify({ bucket, upload_id: plan.upload_id, parts }),
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok)
|
|
293
|
+
throw await this.error(res, 'Failed to get upload links');
|
|
294
|
+
return (await res.json()).links;
|
|
295
|
+
},
|
|
296
|
+
abort: async () => {
|
|
297
|
+
await sessionFetch(base, {
|
|
298
|
+
method: 'DELETE',
|
|
299
|
+
headers: await json(),
|
|
300
|
+
body: JSON.stringify({ bucket, upload_id: plan.upload_id }),
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
// The parts have arrived: the server joins and checks them. No longer
|
|
305
|
+
// cancellable.
|
|
306
|
+
task.verifying();
|
|
307
|
+
const body = { bucket, upload_id: plan.upload_id, overwrite };
|
|
308
|
+
if (options.metadata !== undefined)
|
|
309
|
+
body.metadata = options.metadata;
|
|
310
|
+
const done = await sessionFetch(`${base}/complete`, { method: 'POST', headers: await json(), body: JSON.stringify(body) });
|
|
311
|
+
if (!done.ok)
|
|
312
|
+
throw await this.error(done, 'Failed to complete the upload');
|
|
313
|
+
const object = mapObjectFromServer(await done.json());
|
|
314
|
+
const downloadUrl = await this.getDownloadUrl(bucket, object.path);
|
|
315
|
+
return { object, downloadUrl };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Downloads a file's bytes, reporting them as they arrive (S3):
|
|
319
|
+
* `onProgress(received, total)`, total null when the size is not known.
|
|
320
|
+
* Aborting `signal` stops it with `KoolbaseDownloadCancelledError`.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* const blob = await Koolbase.storage.download({ bucket: 'video', path: 'talk.mp4', onProgress: (r, t) => setPct(t ? r / t : 0) });
|
|
324
|
+
*/
|
|
325
|
+
async download(options) {
|
|
326
|
+
if (options.signal?.aborted)
|
|
327
|
+
throw new KoolbaseDownloadCancelledError();
|
|
328
|
+
const url = await this.getDownloadUrl(options.bucket, options.path, options.versionId);
|
|
329
|
+
const report = options.onProgress ?? (() => undefined);
|
|
330
|
+
const XHR = globalThis.XMLHttpRequest;
|
|
331
|
+
if (XHR) {
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
const xhr = new XHR();
|
|
334
|
+
let settled = false;
|
|
335
|
+
const finish = (fn) => {
|
|
336
|
+
if (settled)
|
|
337
|
+
return;
|
|
338
|
+
settled = true;
|
|
339
|
+
options.signal?.removeEventListener('abort', stop);
|
|
340
|
+
fn();
|
|
341
|
+
};
|
|
342
|
+
const stop = () => {
|
|
343
|
+
xhr.abort();
|
|
344
|
+
finish(() => reject(new KoolbaseDownloadCancelledError()));
|
|
345
|
+
};
|
|
346
|
+
options.signal?.addEventListener('abort', stop);
|
|
347
|
+
xhr.open('GET', url);
|
|
348
|
+
xhr.responseType = 'blob';
|
|
349
|
+
xhr.onprogress = (ev) => report(ev.loaded, ev.lengthComputable ? ev.total : null);
|
|
350
|
+
xhr.onload = () => finish(() => {
|
|
351
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
352
|
+
reject(new KoolbaseStorageError(`Download failed: ${xhr.status}`));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const blob = xhr.response;
|
|
356
|
+
report(blob.size, blob.size);
|
|
357
|
+
resolve(blob);
|
|
358
|
+
});
|
|
359
|
+
xhr.onerror = () => finish(() => reject(new KoolbaseNetworkError(url, new Error('the download did not complete'))));
|
|
360
|
+
xhr.onabort = () => finish(() => reject(new KoolbaseDownloadCancelledError()));
|
|
361
|
+
xhr.send();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
let res;
|
|
365
|
+
try {
|
|
366
|
+
res = await fetch(url, { signal: options.signal });
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
if (options.signal?.aborted)
|
|
370
|
+
throw new KoolbaseDownloadCancelledError();
|
|
371
|
+
throw new KoolbaseNetworkError(url, e);
|
|
372
|
+
}
|
|
373
|
+
if (!res.ok)
|
|
374
|
+
throw new KoolbaseStorageError(`Download failed: ${res.status}`);
|
|
375
|
+
const total = Number(res.headers.get('content-length')) || null;
|
|
376
|
+
const reader = res.body?.getReader?.();
|
|
377
|
+
if (!reader) {
|
|
378
|
+
const blob = await res.blob();
|
|
379
|
+
report(blob.size, blob.size);
|
|
380
|
+
return blob;
|
|
381
|
+
}
|
|
382
|
+
const chunks = [];
|
|
383
|
+
let received = 0;
|
|
384
|
+
report(0, total);
|
|
385
|
+
try {
|
|
386
|
+
for (;;) {
|
|
387
|
+
const { done, value } = await reader.read();
|
|
388
|
+
if (done)
|
|
389
|
+
break;
|
|
390
|
+
chunks.push(value);
|
|
391
|
+
received += value.length;
|
|
392
|
+
report(received, total);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
if (options.signal?.aborted)
|
|
397
|
+
throw new KoolbaseDownloadCancelledError();
|
|
398
|
+
throw new KoolbaseNetworkError(url, e);
|
|
399
|
+
}
|
|
400
|
+
return new Blob(chunks, { type: res.headers.get('content-type') ?? '' });
|
|
401
|
+
}
|
|
222
402
|
/**
|
|
223
403
|
* One page of a bucket's files. The bucket's view rule is applied to every
|
|
224
404
|
* file: the page holds only files the signed-in person may view. Pass the
|
|
@@ -289,7 +469,9 @@ export class KoolbaseStorage {
|
|
|
289
469
|
if (!res.ok) {
|
|
290
470
|
throw await this.error(res, op === 'move' ? 'Failed to move the file' : 'Failed to copy the file');
|
|
291
471
|
}
|
|
292
|
-
|
|
472
|
+
const moved = mapObjectFromServer(await res.json());
|
|
473
|
+
void refreshBucketFiles(o.bucket, ...(o.toBucket && o.toBucket !== o.bucket ? [o.toBucket] : []));
|
|
474
|
+
return moved;
|
|
293
475
|
}
|
|
294
476
|
/**
|
|
295
477
|
* Apply a partial metadata update to an existing object. Returns the
|
|
@@ -337,6 +519,7 @@ export class KoolbaseStorage {
|
|
|
337
519
|
throw await this.error(res, 'Failed to update metadata');
|
|
338
520
|
}
|
|
339
521
|
const raw = await res.json();
|
|
522
|
+
void refreshBucketFiles(bucket);
|
|
340
523
|
return mapObjectFromServer(raw);
|
|
341
524
|
}
|
|
342
525
|
/**
|
|
@@ -468,11 +651,11 @@ export class KoolbaseStorage {
|
|
|
468
651
|
},
|
|
469
652
|
body: JSON.stringify({ bucket, path }),
|
|
470
653
|
});
|
|
471
|
-
if (res.
|
|
654
|
+
if (res.ok) {
|
|
655
|
+
void refreshBucketFiles(bucket);
|
|
472
656
|
return;
|
|
473
|
-
if (!res.ok) {
|
|
474
|
-
throw await this.error(res, 'Failed to delete file');
|
|
475
657
|
}
|
|
658
|
+
throw await this.error(res, 'Failed to delete file');
|
|
476
659
|
}
|
|
477
660
|
/**
|
|
478
661
|
* List all versions of a file path, newest-first. Returns a flat list
|
|
@@ -529,7 +712,9 @@ export class KoolbaseStorage {
|
|
|
529
712
|
if (!res.ok) {
|
|
530
713
|
throw await this.error(res, 'Failed to restore version');
|
|
531
714
|
}
|
|
532
|
-
|
|
715
|
+
const restored = mapObjectFromServer(await res.json());
|
|
716
|
+
void refreshBucketFiles(bucket);
|
|
717
|
+
return restored;
|
|
533
718
|
}
|
|
534
719
|
/**
|
|
535
720
|
* Hard-remove a single history version — both the metadata row and
|
|
@@ -551,6 +736,11 @@ export class KoolbaseStorage {
|
|
|
551
736
|
}
|
|
552
737
|
}
|
|
553
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Files larger than this go up in parts (S3): each part retried on its
|
|
741
|
+
* own, the upload resumable for a day with `resumeUpload`. 16 MiB.
|
|
742
|
+
*/
|
|
743
|
+
KoolbaseStorage.multipartThreshold = 16 * 1024 * 1024;
|
|
554
744
|
/**
|
|
555
745
|
* Maps the snake_case server JSON to the camelCase {@link KoolbaseObject}.
|
|
556
746
|
* Defensive: missing or null `metadata` (older / non-Koolbase responses)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koolbase/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.19.0",
|
|
4
4
|
"description": "Koolbase SDK core \u2014 shared behaviour behind @koolbase/react-native and @koolbase/js. Install one of those, not this.",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"types": "./dist/esm/index.d.ts",
|