@koolbase/core 12.15.0 → 12.17.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/index.d.ts +3 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/storage-errors.d.ts +57 -0
- package/dist/cjs/storage-errors.js +109 -1
- package/dist/cjs/storage-files.d.ts +53 -0
- package/dist/cjs/storage-files.js +61 -0
- package/dist/cjs/storage-transfer.d.ts +88 -0
- package/dist/cjs/storage-transfer.js +214 -0
- package/dist/cjs/storage.d.ts +72 -0
- package/dist/cjs/storage.js +129 -8
- package/dist/cjs/types.d.ts +12 -1
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/storage-errors.d.ts +57 -0
- package/dist/esm/storage-errors.js +101 -0
- package/dist/esm/storage-files.d.ts +53 -0
- package/dist/esm/storage-files.js +56 -0
- package/dist/esm/storage-transfer.d.ts +88 -0
- package/dist/esm/storage-transfer.js +208 -0
- package/dist/esm/storage.d.ts +72 -0
- package/dist/esm/storage.js +129 -8
- package/dist/esm/types.d.ts +12 -1
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { UploadResult } from './types.js';
|
|
2
|
+
/** Where an upload is. */
|
|
3
|
+
export type KoolbaseUploadState =
|
|
4
|
+
/** Not started: asking the server for an upload link. */
|
|
5
|
+
'pending'
|
|
6
|
+
/** Sending the file's bytes; see `bytesSent`. */
|
|
7
|
+
| 'uploading'
|
|
8
|
+
/** The bytes have arrived; the server is checking them and putting the file in place. It can no longer be cancelled. */
|
|
9
|
+
| 'verifying'
|
|
10
|
+
/** Done: the file is in the bucket. */
|
|
11
|
+
| 'completed'
|
|
12
|
+
/** It failed; `error` says why. */
|
|
13
|
+
| 'failed'
|
|
14
|
+
/** Cancelled before the server began checking it. Nothing was stored. */
|
|
15
|
+
| 'cancelled';
|
|
16
|
+
/** One moment of an upload. */
|
|
17
|
+
export interface KoolbaseUploadProgress {
|
|
18
|
+
state: KoolbaseUploadState;
|
|
19
|
+
/** Bytes sent so far. */
|
|
20
|
+
bytesSent: number;
|
|
21
|
+
bytesTotal: number;
|
|
22
|
+
/** 0 to 1: the share of bytes sent; 1 once verifying. */
|
|
23
|
+
fraction: number;
|
|
24
|
+
/** Whether the upload has ended, one way or another. */
|
|
25
|
+
isDone: boolean;
|
|
26
|
+
/** Why it failed, when `state` is `'failed'`. */
|
|
27
|
+
error?: Error;
|
|
28
|
+
}
|
|
29
|
+
/** A running upload: watch it with `onProgress`, `cancel` it, await `result`. */
|
|
30
|
+
export declare class KoolbaseUploadTask {
|
|
31
|
+
private _current;
|
|
32
|
+
private listeners;
|
|
33
|
+
private cancelRequested;
|
|
34
|
+
private abortTransfer;
|
|
35
|
+
private total;
|
|
36
|
+
/**
|
|
37
|
+
* The uploaded file. Rejects with `KoolbaseUploadCancelledError` if it was
|
|
38
|
+
* cancelled, or the error that made it fail.
|
|
39
|
+
*/
|
|
40
|
+
readonly result: Promise<UploadResult>;
|
|
41
|
+
private resolve;
|
|
42
|
+
private reject;
|
|
43
|
+
/** @internal */
|
|
44
|
+
constructor();
|
|
45
|
+
/** Where the upload is now. */
|
|
46
|
+
get current(): KoolbaseUploadProgress;
|
|
47
|
+
/**
|
|
48
|
+
* Calls `listener` on every change of state and every chunk sent, starting
|
|
49
|
+
* with where the upload is now. Returns a function that stops it.
|
|
50
|
+
*/
|
|
51
|
+
onProgress(listener: (p: KoolbaseUploadProgress) => void): () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Stops the upload, if the server has not begun checking it yet. Returns
|
|
54
|
+
* whether it was stopped: false once verifying (the file will land, or
|
|
55
|
+
* fail on its own) or done.
|
|
56
|
+
*/
|
|
57
|
+
cancel(): boolean;
|
|
58
|
+
/** @internal */
|
|
59
|
+
get cancelled(): boolean;
|
|
60
|
+
/** @internal Throws if the upload was cancelled, between steps. */
|
|
61
|
+
checkCancelled(): void;
|
|
62
|
+
/** @internal */
|
|
63
|
+
setTotal(total: number): void;
|
|
64
|
+
/** @internal */
|
|
65
|
+
setAbort(abort: (() => void) | null): void;
|
|
66
|
+
/** @internal */
|
|
67
|
+
emit(state: KoolbaseUploadState, sent?: number, error?: Error): void;
|
|
68
|
+
/** @internal Marks the bytes sent: from here the upload cannot be cancelled. */
|
|
69
|
+
verifying(): void;
|
|
70
|
+
/** @internal */
|
|
71
|
+
finish(result: UploadResult): void;
|
|
72
|
+
/** @internal */
|
|
73
|
+
fail(error: unknown): void;
|
|
74
|
+
}
|
|
75
|
+
/** Runs `body` as the task's upload, starting on the next turn so a caller can listen to every event. */
|
|
76
|
+
export declare function runUploadTask(body: (task: KoolbaseUploadTask) => Promise<UploadResult>): KoolbaseUploadTask;
|
|
77
|
+
interface PutResult {
|
|
78
|
+
status: number;
|
|
79
|
+
etag: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Sends `body` to a presigned `url` with PUT, reporting bytes as they are
|
|
83
|
+
* sent. XMLHttpRequest where the host has it (browsers and React Native,
|
|
84
|
+
* both report upload progress); fetch elsewhere, where progress jumps from
|
|
85
|
+
* 0 to the total. A cancel aborts it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function putWithProgress(task: KoolbaseUploadTask, url: string, body: Blob, contentType: string): Promise<PutResult>;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// An upload's lifecycle, as an app watches it (S1).
|
|
2
|
+
//
|
|
3
|
+
// pending -> uploading (bytes sent of total) -> verifying -> completed, or
|
|
4
|
+
// failed / cancelled. Verifying is its own phase: the bytes have arrived and
|
|
5
|
+
// the server is checking them (size, type, the bucket's rules and cap) and
|
|
6
|
+
// putting them in place. Only completed means the file can be referenced
|
|
7
|
+
// from a record.
|
|
8
|
+
import { KoolbaseNetworkError } from './network.js';
|
|
9
|
+
import { KoolbaseStorageError, KoolbaseUploadCancelledError } from './storage-errors.js';
|
|
10
|
+
function progressOf(state, bytesSent, bytesTotal, error) {
|
|
11
|
+
let fraction;
|
|
12
|
+
if (state === 'verifying' || state === 'completed')
|
|
13
|
+
fraction = 1;
|
|
14
|
+
else if (bytesTotal <= 0)
|
|
15
|
+
fraction = 0;
|
|
16
|
+
else
|
|
17
|
+
fraction = Math.min(1, Math.max(0, bytesSent / bytesTotal));
|
|
18
|
+
return {
|
|
19
|
+
state,
|
|
20
|
+
bytesSent,
|
|
21
|
+
bytesTotal,
|
|
22
|
+
fraction,
|
|
23
|
+
isDone: state === 'completed' || state === 'failed' || state === 'cancelled',
|
|
24
|
+
...(error ? { error } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A running upload: watch it with `onProgress`, `cancel` it, await `result`. */
|
|
28
|
+
export class KoolbaseUploadTask {
|
|
29
|
+
/** @internal */
|
|
30
|
+
constructor() {
|
|
31
|
+
this.listeners = new Set();
|
|
32
|
+
this.cancelRequested = false;
|
|
33
|
+
this.abortTransfer = null;
|
|
34
|
+
this.total = 0;
|
|
35
|
+
this._current = progressOf('pending', 0, 0);
|
|
36
|
+
this.result = new Promise((res, rej) => {
|
|
37
|
+
this.resolve = res;
|
|
38
|
+
this.reject = rej;
|
|
39
|
+
});
|
|
40
|
+
// An app that only listens to progress is not left with an unhandled
|
|
41
|
+
// rejection; awaiting `result` still sees the error.
|
|
42
|
+
this.result.catch(() => undefined);
|
|
43
|
+
}
|
|
44
|
+
/** Where the upload is now. */
|
|
45
|
+
get current() {
|
|
46
|
+
return this._current;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Calls `listener` on every change of state and every chunk sent, starting
|
|
50
|
+
* with where the upload is now. Returns a function that stops it.
|
|
51
|
+
*/
|
|
52
|
+
onProgress(listener) {
|
|
53
|
+
this.listeners.add(listener);
|
|
54
|
+
listener(this._current);
|
|
55
|
+
return () => {
|
|
56
|
+
this.listeners.delete(listener);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Stops the upload, if the server has not begun checking it yet. Returns
|
|
61
|
+
* whether it was stopped: false once verifying (the file will land, or
|
|
62
|
+
* fail on its own) or done.
|
|
63
|
+
*/
|
|
64
|
+
cancel() {
|
|
65
|
+
if (this._current.isDone || this._current.state === 'verifying')
|
|
66
|
+
return false;
|
|
67
|
+
this.cancelRequested = true;
|
|
68
|
+
this.abortTransfer?.();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** @internal */
|
|
72
|
+
get cancelled() {
|
|
73
|
+
return this.cancelRequested;
|
|
74
|
+
}
|
|
75
|
+
/** @internal Throws if the upload was cancelled, between steps. */
|
|
76
|
+
checkCancelled() {
|
|
77
|
+
if (this.cancelRequested)
|
|
78
|
+
throw new KoolbaseUploadCancelledError();
|
|
79
|
+
}
|
|
80
|
+
/** @internal */
|
|
81
|
+
setTotal(total) {
|
|
82
|
+
this.total = total;
|
|
83
|
+
}
|
|
84
|
+
/** @internal */
|
|
85
|
+
setAbort(abort) {
|
|
86
|
+
this.abortTransfer = abort;
|
|
87
|
+
}
|
|
88
|
+
/** @internal */
|
|
89
|
+
emit(state, sent, error) {
|
|
90
|
+
if (this._current.isDone)
|
|
91
|
+
return;
|
|
92
|
+
this._current = progressOf(state, sent ?? this._current.bytesSent, this.total, error);
|
|
93
|
+
for (const l of [...this.listeners]) {
|
|
94
|
+
try {
|
|
95
|
+
l(this._current);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// A listener's own failure is not the upload's.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** @internal Marks the bytes sent: from here the upload cannot be cancelled. */
|
|
103
|
+
verifying() {
|
|
104
|
+
this.checkCancelled();
|
|
105
|
+
this.emit('verifying', this.total);
|
|
106
|
+
}
|
|
107
|
+
/** @internal */
|
|
108
|
+
finish(result) {
|
|
109
|
+
this.emit('completed', this.total);
|
|
110
|
+
this.listeners.clear();
|
|
111
|
+
this.resolve(result);
|
|
112
|
+
}
|
|
113
|
+
/** @internal */
|
|
114
|
+
fail(error) {
|
|
115
|
+
if (this.cancelRequested) {
|
|
116
|
+
const e = new KoolbaseUploadCancelledError();
|
|
117
|
+
this.emit('cancelled');
|
|
118
|
+
this.reject(e);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const e = error instanceof Error ? error : new KoolbaseStorageError(String(error));
|
|
122
|
+
this.emit('failed', undefined, e);
|
|
123
|
+
this.reject(e);
|
|
124
|
+
}
|
|
125
|
+
this.listeners.clear();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Runs `body` as the task's upload, starting on the next turn so a caller can listen to every event. */
|
|
129
|
+
export function runUploadTask(body) {
|
|
130
|
+
const task = new KoolbaseUploadTask();
|
|
131
|
+
Promise.resolve().then(async () => {
|
|
132
|
+
try {
|
|
133
|
+
task.finish(await body(task));
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
task.fail(e);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return task;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Sends `body` to a presigned `url` with PUT, reporting bytes as they are
|
|
143
|
+
* sent. XMLHttpRequest where the host has it (browsers and React Native,
|
|
144
|
+
* both report upload progress); fetch elsewhere, where progress jumps from
|
|
145
|
+
* 0 to the total. A cancel aborts it.
|
|
146
|
+
*/
|
|
147
|
+
export function putWithProgress(task, url, body, contentType) {
|
|
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);
|
|
155
|
+
const XHR = globalThis.XMLHttpRequest;
|
|
156
|
+
if (XHR) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const xhr = new XHR();
|
|
159
|
+
let settled = false;
|
|
160
|
+
const done = (fn) => {
|
|
161
|
+
if (settled)
|
|
162
|
+
return;
|
|
163
|
+
settled = true;
|
|
164
|
+
task.setAbort(null);
|
|
165
|
+
fn();
|
|
166
|
+
};
|
|
167
|
+
xhr.open('PUT', url);
|
|
168
|
+
xhr.setRequestHeader('Content-Type', contentType);
|
|
169
|
+
if (xhr.upload) {
|
|
170
|
+
xhr.upload.onprogress = (ev) => report(ev.loaded);
|
|
171
|
+
}
|
|
172
|
+
xhr.onload = () => done(() => {
|
|
173
|
+
if (task.cancelled)
|
|
174
|
+
return reject(new KoolbaseUploadCancelledError());
|
|
175
|
+
resolve({ status: xhr.status, etag: xhr.getResponseHeader('etag') ?? '' });
|
|
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
|
+
});
|
|
184
|
+
xhr.send(body);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
188
|
+
task.setAbort(() => controller?.abort());
|
|
189
|
+
return fetch(url, {
|
|
190
|
+
method: 'PUT',
|
|
191
|
+
headers: { 'Content-Type': contentType },
|
|
192
|
+
body,
|
|
193
|
+
signal: controller?.signal,
|
|
194
|
+
})
|
|
195
|
+
.then((res) => {
|
|
196
|
+
task.checkCancelled();
|
|
197
|
+
return { status: res.status, etag: res.headers.get('etag') ?? '' };
|
|
198
|
+
})
|
|
199
|
+
.catch((e) => {
|
|
200
|
+
if (task.cancelled || e?.name === 'AbortError') {
|
|
201
|
+
throw new KoolbaseUploadCancelledError();
|
|
202
|
+
}
|
|
203
|
+
if (e instanceof KoolbaseStorageError)
|
|
204
|
+
throw e;
|
|
205
|
+
throw new KoolbaseNetworkError(url, e);
|
|
206
|
+
})
|
|
207
|
+
.finally(() => task.setAbort(null));
|
|
208
|
+
}
|
package/dist/esm/storage.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { KoolbaseConfig, UploadOptions, UploadResult, KoolbaseObject, KoolbaseObjectVersion, KoolbaseImageTransform } from './types.js';
|
|
2
|
+
import { KoolbaseUploadTask } from './storage-transfer.js';
|
|
3
|
+
import { KoolbaseFileInfo, KoolbaseFilePage, ListFilesOptions } from './storage-files.js';
|
|
2
4
|
/**
|
|
3
5
|
* Koolbase storage client — uploads, downloads, and deletes via presigned
|
|
4
6
|
* Cloudflare R2 URLs.
|
|
@@ -53,6 +55,76 @@ export declare class KoolbaseStorage {
|
|
|
53
55
|
* overwriting silently, pass `overwrite: true` explicitly.
|
|
54
56
|
*/
|
|
55
57
|
upload(options: UploadOptions): Promise<UploadResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Starts an upload you can follow and cancel: the same upload as
|
|
60
|
+
* {@link upload}, returned at once as a {@link KoolbaseUploadTask}.
|
|
61
|
+
*
|
|
62
|
+
* The task goes pending -> uploading (bytes sent of total) -> verifying
|
|
63
|
+
* -> completed, or failed / cancelled. `cancel()` stops it until the
|
|
64
|
+
* server begins checking the bytes (verifying); nothing is stored. Only
|
|
65
|
+
* completed means the file can be referenced from a record.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const task = Koolbase.storage.startUpload({ bucket: 'posters', path: 'gig.png', file });
|
|
69
|
+
* const stop = task.onProgress((p) => setPercent(Math.round(p.fraction * 100)));
|
|
70
|
+
* cancelButton.onclick = () => task.cancel();
|
|
71
|
+
* const { object } = await task.result;
|
|
72
|
+
*/
|
|
73
|
+
startUpload(options: UploadOptions): KoolbaseUploadTask;
|
|
74
|
+
private runUpload;
|
|
75
|
+
/**
|
|
76
|
+
* One page of a bucket's files. The bucket's view rule is applied to every
|
|
77
|
+
* file: the page holds only files the signed-in person may view. Pass the
|
|
78
|
+
* page's `nextCursor` back as `cursor` for the next one; a page can hold
|
|
79
|
+
* fewer files than `limit` and still have a next.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* let cursor: string | undefined;
|
|
83
|
+
* do {
|
|
84
|
+
* const page = await Koolbase.storage.listFiles({ bucket: 'posters', prefix: 'events/', cursor });
|
|
85
|
+
* show(page.files);
|
|
86
|
+
* cursor = page.nextCursor ?? undefined;
|
|
87
|
+
* } while (cursor);
|
|
88
|
+
*/
|
|
89
|
+
listFiles(options: ListFilesOptions): Promise<KoolbaseFilePage>;
|
|
90
|
+
/**
|
|
91
|
+
* One file's details -- size, type, metadata, who uploaded it, and its
|
|
92
|
+
* permanent link while its bucket is public. Throws
|
|
93
|
+
* `KoolbaseStorageNotFoundError` for a file that is not there or that
|
|
94
|
+
* the bucket's view rule hides from the caller.
|
|
95
|
+
*/
|
|
96
|
+
getFileInfo(bucket: string, path: string): Promise<KoolbaseFileInfo>;
|
|
97
|
+
/**
|
|
98
|
+
* Move a file to `to`, in its bucket or into `toBucket` (S1b). Nothing is
|
|
99
|
+
* uploaded again. The bucket's rules decide: delete on the file, and
|
|
100
|
+
* upload at the destination, whose limits apply as to an upload. The
|
|
101
|
+
* destination must be free.
|
|
102
|
+
*
|
|
103
|
+
* Within one bucket the file keeps its kept versions. Refused with
|
|
104
|
+
* `KoolbaseStorageFileInUseError` while records point at the file: change
|
|
105
|
+
* them first, or {@link copy} it.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* await Koolbase.storage.move({ bucket: 'inbox', from: 'scan.png', toBucket: 'vault', to: '2026/scan.png' });
|
|
109
|
+
*/
|
|
110
|
+
move(options: {
|
|
111
|
+
bucket: string;
|
|
112
|
+
from: string;
|
|
113
|
+
to: string;
|
|
114
|
+
toBucket?: string;
|
|
115
|
+
}): Promise<KoolbaseObject>;
|
|
116
|
+
/**
|
|
117
|
+
* Copy a file to `to`, in its bucket or into `toBucket` (S1b): a new file,
|
|
118
|
+
* the caller's, with the same bytes. The bucket's rules decide: view on
|
|
119
|
+
* the file, and upload at the destination.
|
|
120
|
+
*/
|
|
121
|
+
copy(options: {
|
|
122
|
+
bucket: string;
|
|
123
|
+
from: string;
|
|
124
|
+
to: string;
|
|
125
|
+
toBucket?: string;
|
|
126
|
+
}): Promise<KoolbaseObject>;
|
|
127
|
+
private transfer;
|
|
56
128
|
/**
|
|
57
129
|
* Apply a partial metadata update to an existing object. Returns the
|
|
58
130
|
* post-update {@link KoolbaseObject} with the merged metadata.
|
package/dist/esm/storage.js
CHANGED
|
@@ -2,6 +2,8 @@ import { KoolbaseStorageProjectIdentityError } from './storage-errors.js';
|
|
|
2
2
|
import { sessionFetch } from './network.js';
|
|
3
3
|
import { KoolbaseUnauthenticatedError } from './errors.js';
|
|
4
4
|
import { KoolbaseStorageError, koolbaseStorageErrorFromResponse, } from './storage-errors.js';
|
|
5
|
+
import { putWithProgress, runUploadTask } from './storage-transfer.js';
|
|
6
|
+
import { fileInfoFromJson, filePageFromJson, listFilesQuery, } from './storage-files.js';
|
|
5
7
|
// --- Cloudflare image-transform URL helpers -------------------------------
|
|
6
8
|
// Module-private — callers use KoolbaseStorage.publicUrl / publicUrlForObject.
|
|
7
9
|
function clampInt(v, min, max) {
|
|
@@ -88,6 +90,46 @@ export class KoolbaseStorage {
|
|
|
88
90
|
* overwriting silently, pass `overwrite: true` explicitly.
|
|
89
91
|
*/
|
|
90
92
|
async upload(options) {
|
|
93
|
+
const task = this.startUpload(options);
|
|
94
|
+
const signal = options.signal;
|
|
95
|
+
const onAbort = () => task.cancel();
|
|
96
|
+
if (signal) {
|
|
97
|
+
if (signal.aborted)
|
|
98
|
+
task.cancel();
|
|
99
|
+
else
|
|
100
|
+
signal.addEventListener('abort', onAbort);
|
|
101
|
+
}
|
|
102
|
+
const stop = options.onProgress
|
|
103
|
+
? task.onProgress((p) => options.onProgress?.(Math.round(p.fraction * 100), p))
|
|
104
|
+
: null;
|
|
105
|
+
try {
|
|
106
|
+
return await task.result;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
stop?.();
|
|
110
|
+
signal?.removeEventListener('abort', onAbort);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Starts an upload you can follow and cancel: the same upload as
|
|
115
|
+
* {@link upload}, returned at once as a {@link KoolbaseUploadTask}.
|
|
116
|
+
*
|
|
117
|
+
* The task goes pending -> uploading (bytes sent of total) -> verifying
|
|
118
|
+
* -> completed, or failed / cancelled. `cancel()` stops it until the
|
|
119
|
+
* server begins checking the bytes (verifying); nothing is stored. Only
|
|
120
|
+
* completed means the file can be referenced from a record.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const task = Koolbase.storage.startUpload({ bucket: 'posters', path: 'gig.png', file });
|
|
124
|
+
* const stop = task.onProgress((p) => setPercent(Math.round(p.fraction * 100)));
|
|
125
|
+
* cancelButton.onclick = () => task.cancel();
|
|
126
|
+
* const { object } = await task.result;
|
|
127
|
+
*/
|
|
128
|
+
startUpload(options) {
|
|
129
|
+
return runUploadTask((task) => this.runUpload(options, task));
|
|
130
|
+
}
|
|
131
|
+
async runUpload(options, task) {
|
|
132
|
+
task.checkCancelled();
|
|
91
133
|
const overwrite = options.overwrite ?? false;
|
|
92
134
|
// A Blob (browser File included) knows its own type; the React Native
|
|
93
135
|
// URI form carries it alongside. Falling back to octet-stream keeps a
|
|
@@ -97,6 +139,8 @@ export class KoolbaseStorage {
|
|
|
97
139
|
const contentType = isBlob
|
|
98
140
|
? options.file.type || 'application/octet-stream'
|
|
99
141
|
: options.file.type;
|
|
142
|
+
if (isBlob)
|
|
143
|
+
task.setTotal(options.file.size);
|
|
100
144
|
// ─── Step 1: Get presigned upload URL ───
|
|
101
145
|
const urlRes = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
|
|
102
146
|
method: 'POST',
|
|
@@ -114,7 +158,8 @@ export class KoolbaseStorage {
|
|
|
114
158
|
if (!urlRes.ok) {
|
|
115
159
|
throw await this.error(urlRes, 'Failed to get upload URL');
|
|
116
160
|
}
|
|
117
|
-
const { upload_url } = (await urlRes.json());
|
|
161
|
+
const { upload_url, upload_id } = (await urlRes.json());
|
|
162
|
+
task.checkCancelled();
|
|
118
163
|
// ─── Step 2: Upload directly to R2 ───
|
|
119
164
|
// R2 presigned URLs expect raw binary, NOT multipart/form-data.
|
|
120
165
|
//
|
|
@@ -126,17 +171,17 @@ export class KoolbaseStorage {
|
|
|
126
171
|
? options.file
|
|
127
172
|
: await (await sessionFetch(options.file.uri)).blob();
|
|
128
173
|
const fileSize = fileBlob.size;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
body: fileBlob,
|
|
133
|
-
});
|
|
134
|
-
if (!uploadRes.ok) {
|
|
174
|
+
task.setTotal(fileSize);
|
|
175
|
+
const uploadRes = await putWithProgress(task, upload_url, fileBlob, contentType);
|
|
176
|
+
if (uploadRes.status < 200 || uploadRes.status >= 300) {
|
|
135
177
|
// R2 PUT errors don't follow the Koolbase error shape — surface as a
|
|
136
178
|
// generic storage error rather than trying to decode a Koolbase body.
|
|
137
179
|
throw new KoolbaseStorageError(`Upload to storage failed: ${uploadRes.status}`);
|
|
138
180
|
}
|
|
139
|
-
const etag = uploadRes.
|
|
181
|
+
const etag = uploadRes.etag;
|
|
182
|
+
// From here the server checks the bytes and puts them in place; the
|
|
183
|
+
// upload can no longer be cancelled.
|
|
184
|
+
task.verifying();
|
|
140
185
|
// ─── Step 3: Confirm upload ───
|
|
141
186
|
// Build the body conditionally so the `metadata` field is only sent
|
|
142
187
|
// when the caller passed it — keeps the wire shape clean for callers
|
|
@@ -150,6 +195,10 @@ export class KoolbaseStorage {
|
|
|
150
195
|
etag,
|
|
151
196
|
overwrite,
|
|
152
197
|
};
|
|
198
|
+
// The confirm names its upload, so it takes exactly these bytes.
|
|
199
|
+
if (upload_id) {
|
|
200
|
+
confirmBody.upload_id = upload_id;
|
|
201
|
+
}
|
|
153
202
|
if (options.metadata !== undefined) {
|
|
154
203
|
confirmBody.metadata = options.metadata;
|
|
155
204
|
}
|
|
@@ -170,6 +219,78 @@ export class KoolbaseStorage {
|
|
|
170
219
|
const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
|
|
171
220
|
return { object, downloadUrl };
|
|
172
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* One page of a bucket's files. The bucket's view rule is applied to every
|
|
224
|
+
* file: the page holds only files the signed-in person may view. Pass the
|
|
225
|
+
* page's `nextCursor` back as `cursor` for the next one; a page can hold
|
|
226
|
+
* fewer files than `limit` and still have a next.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* let cursor: string | undefined;
|
|
230
|
+
* do {
|
|
231
|
+
* const page = await Koolbase.storage.listFiles({ bucket: 'posters', prefix: 'events/', cursor });
|
|
232
|
+
* show(page.files);
|
|
233
|
+
* cursor = page.nextCursor ?? undefined;
|
|
234
|
+
* } while (cursor);
|
|
235
|
+
*/
|
|
236
|
+
async listFiles(options) {
|
|
237
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/files?${listFilesQuery(options)}`, { headers: await this.buildHeaders() });
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
throw await this.error(res, 'Failed to list files');
|
|
240
|
+
}
|
|
241
|
+
return filePageFromJson((await res.json()));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* One file's details -- size, type, metadata, who uploaded it, and its
|
|
245
|
+
* permanent link while its bucket is public. Throws
|
|
246
|
+
* `KoolbaseStorageNotFoundError` for a file that is not there or that
|
|
247
|
+
* the bucket's view rule hides from the caller.
|
|
248
|
+
*/
|
|
249
|
+
async getFileInfo(bucket, path) {
|
|
250
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/file?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`, { headers: await this.buildHeaders() });
|
|
251
|
+
if (!res.ok) {
|
|
252
|
+
throw await this.error(res, 'Failed to get the file');
|
|
253
|
+
}
|
|
254
|
+
return fileInfoFromJson((await res.json()));
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Move a file to `to`, in its bucket or into `toBucket` (S1b). Nothing is
|
|
258
|
+
* uploaded again. The bucket's rules decide: delete on the file, and
|
|
259
|
+
* upload at the destination, whose limits apply as to an upload. The
|
|
260
|
+
* destination must be free.
|
|
261
|
+
*
|
|
262
|
+
* Within one bucket the file keeps its kept versions. Refused with
|
|
263
|
+
* `KoolbaseStorageFileInUseError` while records point at the file: change
|
|
264
|
+
* them first, or {@link copy} it.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* await Koolbase.storage.move({ bucket: 'inbox', from: 'scan.png', toBucket: 'vault', to: '2026/scan.png' });
|
|
268
|
+
*/
|
|
269
|
+
async move(options) {
|
|
270
|
+
return this.transfer('move', options);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Copy a file to `to`, in its bucket or into `toBucket` (S1b): a new file,
|
|
274
|
+
* the caller's, with the same bytes. The bucket's rules decide: view on
|
|
275
|
+
* the file, and upload at the destination.
|
|
276
|
+
*/
|
|
277
|
+
async copy(options) {
|
|
278
|
+
return this.transfer('copy', options);
|
|
279
|
+
}
|
|
280
|
+
async transfer(op, o) {
|
|
281
|
+
const body = { bucket: o.bucket, from: o.from, to: o.to };
|
|
282
|
+
if (o.toBucket && o.toBucket !== o.bucket)
|
|
283
|
+
body.to_bucket = o.toBucket;
|
|
284
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/${op}`, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: { ...(await this.buildHeaders()), 'Content-Type': 'application/json' },
|
|
287
|
+
body: JSON.stringify(body),
|
|
288
|
+
});
|
|
289
|
+
if (!res.ok) {
|
|
290
|
+
throw await this.error(res, op === 'move' ? 'Failed to move the file' : 'Failed to copy the file');
|
|
291
|
+
}
|
|
292
|
+
return mapObjectFromServer(await res.json());
|
|
293
|
+
}
|
|
173
294
|
/**
|
|
174
295
|
* Apply a partial metadata update to an existing object. Returns the
|
|
175
296
|
* post-update {@link KoolbaseObject} with the merged metadata.
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -369,7 +369,18 @@ export interface UploadOptions {
|
|
|
369
369
|
* for post-upload merge changes.
|
|
370
370
|
*/
|
|
371
371
|
metadata?: Record<string, string>;
|
|
372
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Called as the upload goes: `percent` is 0 to 100 of the bytes sent;
|
|
374
|
+
* `progress` says where the upload is (uploading, verifying, completed,
|
|
375
|
+
* ...). For a cancellable upload use `KoolbaseStorage.startUpload`.
|
|
376
|
+
*/
|
|
377
|
+
onProgress?: (percent: number, progress: import('./storage-transfer.js').KoolbaseUploadProgress) => void;
|
|
378
|
+
/**
|
|
379
|
+
* Aborting this signal cancels the upload, if the server has not begun
|
|
380
|
+
* checking it yet; the upload then rejects with
|
|
381
|
+
* `KoolbaseUploadCancelledError`.
|
|
382
|
+
*/
|
|
383
|
+
signal?: AbortSignal;
|
|
373
384
|
}
|
|
374
385
|
/**
|
|
375
386
|
* A stored object's server-side metadata. Field names are camelCase here
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koolbase/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.17.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",
|