@telestack/storage 1.0.0 → 1.0.1
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/package.json +10 -2
- package/debug-fetch.js +0 -28
- package/src/BatchBuilder.ts +0 -48
- package/src/CryptoHelper.ts +0 -72
- package/src/HttpClient.ts +0 -152
- package/src/ImageHelper.ts +0 -80
- package/src/QueryBuilder.ts +0 -45
- package/src/StorageRef.ts +0 -153
- package/src/TelestackStorage.ts +0 -93
- package/src/UploadTask.ts +0 -332
- package/src/index.ts +0 -28
- package/src/types.ts +0 -91
- package/test-e2e.js +0 -142
- package/test-e2e.ts +0 -182
- package/test-output.txt +0 -0
- package/tsconfig.json +0 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telestack/storage",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Official Telestack Storage Web SDK — fluent, Firebase-killer, real-time cloud storage for web apps.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
15
20
|
"scripts": {
|
|
16
21
|
"build": "tsup src/index.ts --format cjs,esm,iife --global-name TelestackStorageSetup --minify --dts --clean"
|
|
17
22
|
},
|
|
@@ -23,6 +28,9 @@
|
|
|
23
28
|
"file-upload"
|
|
24
29
|
],
|
|
25
30
|
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
26
34
|
"devDependencies": {
|
|
27
35
|
"@types/jsdom": "^27.0.0",
|
|
28
36
|
"dotenv": "^17.3.1",
|
|
@@ -34,4 +42,4 @@
|
|
|
34
42
|
"typescript": "^5.0.0",
|
|
35
43
|
"xhr2": "^0.2.1"
|
|
36
44
|
}
|
|
37
|
-
}
|
|
45
|
+
}
|
package/debug-fetch.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import fetch from 'node-fetch';
|
|
2
|
-
|
|
3
|
-
async function testUploadUrl() {
|
|
4
|
-
try {
|
|
5
|
-
const res = await fetch('http://localhost:8787/files/upload-url', {
|
|
6
|
-
method: 'POST',
|
|
7
|
-
headers: {
|
|
8
|
-
'Content-Type': 'application/json',
|
|
9
|
-
'X-Tenant-ID': 'test-tenant',
|
|
10
|
-
'Authorization': 'Bearer DEV_TEST_TOKEN'
|
|
11
|
-
},
|
|
12
|
-
body: JSON.stringify({
|
|
13
|
-
path: 'test.txt',
|
|
14
|
-
name: 'test.txt',
|
|
15
|
-
size: 1024,
|
|
16
|
-
contentType: 'text/plain',
|
|
17
|
-
userId: 'user-sdk-test'
|
|
18
|
-
})
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const text = await res.text();
|
|
22
|
-
console.log(`Status: ${res.status}`);
|
|
23
|
-
console.log(`Body: ${text}`);
|
|
24
|
-
} catch (err) {
|
|
25
|
-
console.error('Fetch error:', err);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
testUploadUrl();
|
package/src/BatchBuilder.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { HttpClient } from './HttpClient';
|
|
2
|
-
import { BatchDeleteResult, BatchCopyResult } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Fluent builder for batch file operations.
|
|
6
|
-
* Operations execute identically as single network requests over the backend API.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* await storage.batch().delete(['a.pdf', 'b.pdf']).run();
|
|
10
|
-
* await storage.batch().copy(['a.pdf'], 'archive/').run();
|
|
11
|
-
* await storage.batch().move(['a.pdf'], 'archive/').run();
|
|
12
|
-
*/
|
|
13
|
-
export class BatchBuilder {
|
|
14
|
-
private _op: 'delete' | 'copy' | 'move' | null = null;
|
|
15
|
-
private _paths: string[] = [];
|
|
16
|
-
private _dest = '';
|
|
17
|
-
|
|
18
|
-
constructor(private readonly client: HttpClient) { }
|
|
19
|
-
|
|
20
|
-
delete(paths: string[]): this {
|
|
21
|
-
this._op = 'delete';
|
|
22
|
-
this._paths = paths;
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
copy(paths: string[], destinationPrefix: string): this {
|
|
27
|
-
this._op = 'copy';
|
|
28
|
-
this._paths = paths;
|
|
29
|
-
this._dest = destinationPrefix;
|
|
30
|
-
return this;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
move(paths: string[], destinationPrefix: string): this {
|
|
34
|
-
this._op = 'move';
|
|
35
|
-
this._paths = paths;
|
|
36
|
-
this._dest = destinationPrefix;
|
|
37
|
-
return this;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async run(): Promise<BatchDeleteResult | BatchCopyResult> {
|
|
41
|
-
if (!this._op) throw new Error('No batch operation specified');
|
|
42
|
-
if (this._op === 'delete') {
|
|
43
|
-
return this.client.post<BatchDeleteResult>('/files/batch/delete', { paths: this._paths });
|
|
44
|
-
}
|
|
45
|
-
const body = { sourcePaths: this._paths, destinationPrefix: this._dest };
|
|
46
|
-
return this.client.post<BatchCopyResult>(`/files/batch/${this._op}`, body);
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/CryptoHelper.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// TelestackStorage Web SDK — CryptoHelper (E2EE)
|
|
3
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
export class CryptoHelper {
|
|
6
|
-
/**
|
|
7
|
-
* Generates a new random AES-GCM 256-bit encryption key.
|
|
8
|
-
* Returns the key as a base64 encoded string.
|
|
9
|
-
*/
|
|
10
|
-
static async generateKey(): Promise<string> {
|
|
11
|
-
const key = await crypto.subtle.generateKey(
|
|
12
|
-
{ name: 'AES-GCM', length: 256 },
|
|
13
|
-
true, // extractable
|
|
14
|
-
['encrypt', 'decrypt']
|
|
15
|
-
);
|
|
16
|
-
const exported = await crypto.subtle.exportKey('raw', key);
|
|
17
|
-
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Encrypts a Blob using AES-GCM.
|
|
22
|
-
* Prepends a random 12-byte IV to the resulting ciphertext Blob.
|
|
23
|
-
*/
|
|
24
|
-
static async encrypt(blob: Blob, keyBase64: string): Promise<Blob> {
|
|
25
|
-
const key = await this._importKey(keyBase64);
|
|
26
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
27
|
-
|
|
28
|
-
const buffer = await blob.arrayBuffer();
|
|
29
|
-
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
30
|
-
{ name: 'AES-GCM', iv },
|
|
31
|
-
key,
|
|
32
|
-
buffer
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
// Prepend the 12-byte IV so we can decrypt it later
|
|
36
|
-
return new Blob([iv, encryptedBuffer], { type: 'application/octet-stream' });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Decrypts a Blob that was encrypted with `encrypt()`.
|
|
41
|
-
* Extracts the 12-byte IV from the front and decrypts the rest.
|
|
42
|
-
* Restores the original MIME type.
|
|
43
|
-
*/
|
|
44
|
-
static async decrypt(blob: Blob, keyBase64: string, originalMimeType: string = 'application/octet-stream'): Promise<Blob> {
|
|
45
|
-
const key = await this._importKey(keyBase64);
|
|
46
|
-
|
|
47
|
-
const buffer = await blob.arrayBuffer();
|
|
48
|
-
|
|
49
|
-
// Extract the 12-byte IV from the prefix
|
|
50
|
-
const iv = buffer.slice(0, 12);
|
|
51
|
-
const ciphertext = buffer.slice(12);
|
|
52
|
-
|
|
53
|
-
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
54
|
-
{ name: 'AES-GCM', iv: new Uint8Array(iv) },
|
|
55
|
-
key,
|
|
56
|
-
ciphertext
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
return new Blob([decryptedBuffer], { type: originalMimeType });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private static async _importKey(keyBase64: string): Promise<CryptoKey> {
|
|
63
|
-
const keyBuffer = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
|
|
64
|
-
return crypto.subtle.importKey(
|
|
65
|
-
'raw',
|
|
66
|
-
keyBuffer,
|
|
67
|
-
{ name: 'AES-GCM' },
|
|
68
|
-
false, // no need to extract it again
|
|
69
|
-
['encrypt', 'decrypt']
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
package/src/HttpClient.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// TelestackStorage Web SDK — HTTP Client
|
|
3
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
import type { TelestackConfig } from './types';
|
|
6
|
-
|
|
7
|
-
export class HttpClient {
|
|
8
|
-
private baseUrl: string;
|
|
9
|
-
private tenantId: string;
|
|
10
|
-
private headers: Record<string, string>;
|
|
11
|
-
|
|
12
|
-
constructor(config: TelestackConfig) {
|
|
13
|
-
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
14
|
-
this.tenantId = config.tenantId;
|
|
15
|
-
this.headers = {
|
|
16
|
-
'Content-Type': 'application/json',
|
|
17
|
-
'X-Tenant-ID': config.tenantId,
|
|
18
|
-
};
|
|
19
|
-
if (config.apiKey) {
|
|
20
|
-
this.headers['X-API-Key'] = config.apiKey;
|
|
21
|
-
} else if (config.token) {
|
|
22
|
-
this.headers['Authorization'] = `Bearer ${config.token}`;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private async _fetchWithRetry(url: string | URL, options: RequestInit, retries: number = 3): Promise<Response> {
|
|
27
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(url, options);
|
|
30
|
-
// Retry on rate limits (429) or server errors (5xx)
|
|
31
|
-
if ((res.status === 429 || res.status >= 500) && attempt < retries) {
|
|
32
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
33
|
-
console.warn(`[TelestackStorage] Network error ${res.status}. Retrying in ${delay}ms...`);
|
|
34
|
-
await new Promise(r => setTimeout(r, delay));
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
return res;
|
|
38
|
-
} catch (err: any) {
|
|
39
|
-
// Network failures (e.g. CORS, offline)
|
|
40
|
-
if (attempt < retries) {
|
|
41
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
42
|
-
console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${delay}ms...`);
|
|
43
|
-
await new Promise(r => setTimeout(r, delay));
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
throw new Error('Unreachable code in retry logic');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async get<T>(path: string, params?: Record<string, string | undefined>): Promise<T> {
|
|
53
|
-
const url = new URL(`${this.baseUrl}${path}`);
|
|
54
|
-
if (params) {
|
|
55
|
-
Object.entries(params).forEach(([k, v]) => {
|
|
56
|
-
if (v !== undefined) url.searchParams.set(k, v);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
const res = await this._fetchWithRetry(url.toString(), { headers: this.headers });
|
|
60
|
-
return this._handle<T>(res);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
64
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
65
|
-
method: 'POST',
|
|
66
|
-
headers: this.headers,
|
|
67
|
-
body: JSON.stringify(body),
|
|
68
|
-
});
|
|
69
|
-
return this._handle<T>(res);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async patch<T>(path: string, body?: unknown): Promise<T> {
|
|
73
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
74
|
-
method: 'PATCH',
|
|
75
|
-
headers: this.headers,
|
|
76
|
-
body: JSON.stringify(body),
|
|
77
|
-
});
|
|
78
|
-
return this._handle<T>(res);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async put<T>(path: string, body?: unknown): Promise<T> {
|
|
82
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
83
|
-
method: 'PUT',
|
|
84
|
-
headers: this.headers,
|
|
85
|
-
body: JSON.stringify(body),
|
|
86
|
-
});
|
|
87
|
-
return this._handle<T>(res);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async delete<T>(path: string): Promise<T> {
|
|
91
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
92
|
-
method: 'DELETE',
|
|
93
|
-
headers: this.headers,
|
|
94
|
-
});
|
|
95
|
-
return this._handle<T>(res);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Upload a file binary buffer directly to a presigned S3 URL. */
|
|
99
|
-
async uploadToPresignedUrl(url: string, data: Blob | ArrayBuffer, contentType: string, onProgress?: (p: number) => void): Promise<void> {
|
|
100
|
-
const maxRetries = 3;
|
|
101
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
102
|
-
try {
|
|
103
|
-
await new Promise<void>((resolve, reject) => {
|
|
104
|
-
const xhr = new XMLHttpRequest();
|
|
105
|
-
xhr.open('PUT', url);
|
|
106
|
-
xhr.setRequestHeader('Content-Type', contentType);
|
|
107
|
-
if (onProgress) {
|
|
108
|
-
xhr.upload.onprogress = (e) => {
|
|
109
|
-
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
xhr.onload = () => {
|
|
113
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
114
|
-
resolve();
|
|
115
|
-
} else if (xhr.status === 429 || xhr.status >= 500) {
|
|
116
|
-
reject(new Error(`Retryable network error: ${xhr.status}`));
|
|
117
|
-
} else {
|
|
118
|
-
reject(new TelestackError(`Upload failed: ${xhr.status}`, xhr.status));
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
xhr.onerror = () => reject(new Error('Retryable network error'));
|
|
122
|
-
xhr.send(data);
|
|
123
|
-
});
|
|
124
|
-
return; // Success, exit retry loop
|
|
125
|
-
} catch (err: any) {
|
|
126
|
-
if (err instanceof TelestackError || attempt >= maxRetries) {
|
|
127
|
-
throw err; // Fatal error or out of retries
|
|
128
|
-
}
|
|
129
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
130
|
-
console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${delay}ms...`);
|
|
131
|
-
await new Promise(r => setTimeout(r, delay));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private async _handle<T>(res: Response): Promise<T> {
|
|
137
|
-
const json = await res.json() as any;
|
|
138
|
-
if (!res.ok) throw new TelestackError(json.error || res.statusText, res.status, json.code);
|
|
139
|
-
return json as T;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export class TelestackError extends Error {
|
|
144
|
-
constructor(
|
|
145
|
-
message: string,
|
|
146
|
-
public readonly status: number,
|
|
147
|
-
public readonly code?: string
|
|
148
|
-
) {
|
|
149
|
-
super(message);
|
|
150
|
-
this.name = 'TelestackError';
|
|
151
|
-
}
|
|
152
|
-
}
|
package/src/ImageHelper.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// TelestackStorage Web SDK — ImageHelper (Compression)
|
|
3
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
export class ImageHelper {
|
|
6
|
-
/**
|
|
7
|
-
* Compresses and resizes an image Blob using the browser's native HTML5 Canvas.
|
|
8
|
-
* Only processes blobs where `type` starts with 'image/'.
|
|
9
|
-
* If it's not an image, it returns the original blob untouched.
|
|
10
|
-
*/
|
|
11
|
-
static async compress(
|
|
12
|
-
blob: Blob,
|
|
13
|
-
options: { maxWidth?: number; maxHeight?: number; quality?: number } = {}
|
|
14
|
-
): Promise<Blob> {
|
|
15
|
-
if (!blob.type.startsWith('image/')) {
|
|
16
|
-
return blob; // Only compress images
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const { maxWidth = 1920, maxHeight = 1080, quality = 0.8 } = options;
|
|
20
|
-
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
const img = new Image();
|
|
23
|
-
const url = URL.createObjectURL(blob);
|
|
24
|
-
|
|
25
|
-
img.onload = () => {
|
|
26
|
-
URL.revokeObjectURL(url);
|
|
27
|
-
|
|
28
|
-
let width = img.width;
|
|
29
|
-
let height = img.height;
|
|
30
|
-
|
|
31
|
-
// Calculate aspect ratio
|
|
32
|
-
if (width > maxWidth) {
|
|
33
|
-
height = Math.round((height * maxWidth) / width);
|
|
34
|
-
width = maxWidth;
|
|
35
|
-
}
|
|
36
|
-
if (height > maxHeight) {
|
|
37
|
-
width = Math.round((width * maxHeight) / height);
|
|
38
|
-
height = maxHeight;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Create a canvas and draw the resized image
|
|
42
|
-
const canvas = document.createElement('canvas');
|
|
43
|
-
canvas.width = width;
|
|
44
|
-
canvas.height = height;
|
|
45
|
-
|
|
46
|
-
const ctx = canvas.getContext('2d');
|
|
47
|
-
if (!ctx) {
|
|
48
|
-
return reject(new Error('Failed to get canvas 2d context for image compression'));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Draw image on canvas
|
|
52
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
53
|
-
|
|
54
|
-
// Export to Blob
|
|
55
|
-
// Prefer webp, fallback to jpeg. Keep original type if it's png and we want transparency (advanced use case)
|
|
56
|
-
// For a general "lazy SDK" compression, outputting webp or jpeg is best.
|
|
57
|
-
const outputType = blob.type === 'image/png' ? 'image/png' : 'image/webp';
|
|
58
|
-
|
|
59
|
-
canvas.toBlob(
|
|
60
|
-
(compressedBlob) => {
|
|
61
|
-
if (compressedBlob) {
|
|
62
|
-
resolve(compressedBlob);
|
|
63
|
-
} else {
|
|
64
|
-
reject(new Error('Canvas toBlob failed'));
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
outputType,
|
|
68
|
-
quality
|
|
69
|
-
);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
img.onerror = () => {
|
|
73
|
-
URL.revokeObjectURL(url);
|
|
74
|
-
reject(new Error('Failed to load image for compression'));
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
img.src = url;
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
package/src/QueryBuilder.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { HttpClient } from './HttpClient';
|
|
2
|
-
import { FileMetadata, SearchResult } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Fluent builder for searching files via Metadata or Full-Text queries.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* const files = await storage.query()
|
|
9
|
-
* .where('project', 'Q1')
|
|
10
|
-
* .nameContains('report')
|
|
11
|
-
* .limit(10)
|
|
12
|
-
* .get();
|
|
13
|
-
*/
|
|
14
|
-
export class QueryBuilder {
|
|
15
|
-
private _query = '';
|
|
16
|
-
private _metadata: Record<string, any> = {};
|
|
17
|
-
private _limit = 20;
|
|
18
|
-
|
|
19
|
-
constructor(private readonly client: HttpClient) { }
|
|
20
|
-
|
|
21
|
-
nameContains(text: string): this {
|
|
22
|
-
this._query = text;
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
where(key: string, value: any): this {
|
|
27
|
-
this._metadata[key] = value;
|
|
28
|
-
return this;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
limit(n: number): this {
|
|
32
|
-
this._limit = n;
|
|
33
|
-
return this;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async get(): Promise<FileMetadata[]> {
|
|
37
|
-
const params: Record<string, string | undefined> = {
|
|
38
|
-
q: this._query || undefined,
|
|
39
|
-
limit: String(this._limit),
|
|
40
|
-
metadata: Object.keys(this._metadata).length > 0 ? JSON.stringify(this._metadata) : undefined,
|
|
41
|
-
};
|
|
42
|
-
const res = await this.client.get<SearchResult>('/files/search', params);
|
|
43
|
-
return res.files;
|
|
44
|
-
}
|
|
45
|
-
}
|
package/src/StorageRef.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { CryptoHelper } from './CryptoHelper.js';
|
|
2
|
-
import { HttpClient } from './HttpClient.js';
|
|
3
|
-
import { FileMetadata, UploadOptions, DownloadOptions, TagSet } from './types';
|
|
4
|
-
import { UploadTask } from './UploadTask';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Base abstract reference to a location in Telestack Storage.
|
|
8
|
-
*/
|
|
9
|
-
export abstract class StorageRef {
|
|
10
|
-
constructor(
|
|
11
|
-
protected readonly client: HttpClient,
|
|
12
|
-
public readonly path: string,
|
|
13
|
-
public readonly tenantId: string,
|
|
14
|
-
) { }
|
|
15
|
-
|
|
16
|
-
/** Navigate to a highly specific child path. Automatically infers File or Directory Ref. */
|
|
17
|
-
child(childPath: string): StorageRef {
|
|
18
|
-
const normalizedParent = this.path ? (this.path.endsWith('/') ? this.path : this.path + '/') : '';
|
|
19
|
-
const fullPath = normalizedParent + childPath.replace(/^\//, '');
|
|
20
|
-
if (fullPath.endsWith('/')) {
|
|
21
|
-
return new DirRef(this.client, fullPath, this.tenantId);
|
|
22
|
-
}
|
|
23
|
-
return new FileRef(this.client, fullPath, this.tenantId);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Reference exclusively to a Directory (Prefix).
|
|
29
|
-
*/
|
|
30
|
-
export class DirRef extends StorageRef {
|
|
31
|
-
|
|
32
|
-
/** Retrieve all files immediately within this directory prefix. */
|
|
33
|
-
async listAll(limit: number = 100): Promise<FileMetadata[]> {
|
|
34
|
-
const res = await this.client.get<{ files: FileMetadata[] }>('/files', {
|
|
35
|
-
prefix: this.path,
|
|
36
|
-
limit: String(limit),
|
|
37
|
-
});
|
|
38
|
-
return res.files;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Reference exclusively to a File object.
|
|
44
|
-
*/
|
|
45
|
-
export class FileRef extends StorageRef {
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Upload a File/Blob, returning an UploadTask that can be observed, paused, and cancelled.
|
|
49
|
-
* Functions identically to Firebase Storage.
|
|
50
|
-
*/
|
|
51
|
-
put(data: File | Blob, options: UploadOptions): UploadTask {
|
|
52
|
-
const size = data instanceof File ? data.size : data.size;
|
|
53
|
-
const name = data instanceof File ? data.name : (this.path.split('/').pop() || 'file');
|
|
54
|
-
const contentType = data.type || 'application/octet-stream';
|
|
55
|
-
|
|
56
|
-
return new UploadTask(this.client, this.path, data, name, contentType, options);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Upload raw bytes directly.
|
|
61
|
-
*/
|
|
62
|
-
putBytes(data: ArrayBuffer, contentType: string, options: UploadOptions): UploadTask {
|
|
63
|
-
const name = this.path.split('/').pop() || 'file';
|
|
64
|
-
const blob = new Blob([data], { type: contentType });
|
|
65
|
-
return new UploadTask(this.client, this.path, blob, name, contentType, options);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Get a time-limited presigned download URL. */
|
|
69
|
-
async getDownloadUrl(options?: DownloadOptions): Promise<string> {
|
|
70
|
-
if (options?.versionId) {
|
|
71
|
-
const res = await this.client.get<{ downloadUrl: string }>(
|
|
72
|
-
`/files/version-url/${encodeURIComponent(this.path)}`,
|
|
73
|
-
{ versionId: options.versionId }
|
|
74
|
-
);
|
|
75
|
-
return res.downloadUrl;
|
|
76
|
-
}
|
|
77
|
-
const res = await this.client.get<{ downloadUrl: string }>(
|
|
78
|
-
`/files/download-url/${encodeURIComponent(this.path)}`
|
|
79
|
-
);
|
|
80
|
-
return res.downloadUrl;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Securely downloads and decrypts an E2EE file entirely within the browser.
|
|
85
|
-
* Requires the exact Base64 AES-GCM key used during `put()`.
|
|
86
|
-
*/
|
|
87
|
-
async getDecryptedBlob(encryptionKey: string): Promise<Blob> {
|
|
88
|
-
const downloadUrl = await this.getDownloadUrl();
|
|
89
|
-
|
|
90
|
-
// Fetch the raw encrypted bytes from the presigned URL
|
|
91
|
-
const res = await fetch(downloadUrl);
|
|
92
|
-
if (!res.ok) throw new Error(`Failed to download file from S3: ${res.status}`);
|
|
93
|
-
|
|
94
|
-
const encryptedBlob = await res.blob();
|
|
95
|
-
|
|
96
|
-
// We need the original MIME type to restore the Blob correctly.
|
|
97
|
-
// If not available on the fetch response, we can fetch metadata, but let's try to get it from the headers/metadata.
|
|
98
|
-
const metadata = await this.getMetadata();
|
|
99
|
-
|
|
100
|
-
return await CryptoHelper.decrypt(encryptedBlob, encryptionKey, metadata.content_type);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Get the file's metadata database record. */
|
|
104
|
-
async getMetadata(): Promise<FileMetadata> {
|
|
105
|
-
const res = await this.client.get<{ file: FileMetadata }>(
|
|
106
|
-
`/files/metadata/${encodeURIComponent(this.path)}`
|
|
107
|
-
);
|
|
108
|
-
return res.file;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Update custom JSON metadata on the file. */
|
|
112
|
-
async updateMetadata(metadata: Record<string, any>): Promise<FileMetadata> {
|
|
113
|
-
const res = await this.client.patch<{ file: FileMetadata }>(
|
|
114
|
-
`/files/metadata/${encodeURIComponent(this.path)}`,
|
|
115
|
-
{ metadata }
|
|
116
|
-
);
|
|
117
|
-
return res.file;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Permanently delete this file. */
|
|
121
|
-
async delete(): Promise<void> {
|
|
122
|
-
await this.client.delete(`/files/${encodeURIComponent(this.path)}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Enterprise ────────────────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
async listVersions(): Promise<{ versions: any[]; deleteMarkers: any[] }> {
|
|
128
|
-
return this.client.get(`/files/versions/${encodeURIComponent(this.path)}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async getVersionUrl(versionId: string): Promise<string> {
|
|
132
|
-
const res = await this.client.get<{ downloadUrl: string }>(
|
|
133
|
-
`/files/version-url/${encodeURIComponent(this.path)}`,
|
|
134
|
-
{ versionId }
|
|
135
|
-
);
|
|
136
|
-
return res.downloadUrl;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async getTags(): Promise<TagSet[]> {
|
|
140
|
-
const res = await this.client.get<{ tags: TagSet[] }>(
|
|
141
|
-
`/files/tags/${encodeURIComponent(this.path)}`
|
|
142
|
-
);
|
|
143
|
-
return res.tags;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async setTags(tags: TagSet[]): Promise<void> {
|
|
147
|
-
await this.client.put(`/files/tags/${encodeURIComponent(this.path)}`, { tags });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async setLegalHold(status: 'ON' | 'OFF'): Promise<void> {
|
|
151
|
-
await this.client.post(`/files/legal-hold/${encodeURIComponent(this.path)}`, { status });
|
|
152
|
-
}
|
|
153
|
-
}
|