@withvlibe/storage-sdk 1.0.0 → 1.1.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/react.js ADDED
@@ -0,0 +1,668 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ VlibeStorage: () => VlibeStorage,
24
+ useStorage: () => useStorage,
25
+ useStorageUsage: () => useStorageUsage
26
+ });
27
+ module.exports = __toCommonJS(react_exports);
28
+
29
+ // src/VlibeStorage.ts
30
+ var DEFAULT_BASE_URL = "https://vlibe.app";
31
+ var VlibeStorage = class {
32
+ constructor(config) {
33
+ this.authToken = null;
34
+ this.storageConfig = null;
35
+ this.appId = config.appId;
36
+ this.appSecret = config.appSecret;
37
+ this.baseUrl = config.baseUrl?.replace(/\/$/, "") || DEFAULT_BASE_URL;
38
+ }
39
+ /**
40
+ * Set the user's auth token (required for authenticated requests)
41
+ */
42
+ setAuthToken(token) {
43
+ this.authToken = token;
44
+ }
45
+ /**
46
+ * Clear the auth token
47
+ */
48
+ clearAuthToken() {
49
+ this.authToken = null;
50
+ }
51
+ /**
52
+ * Get storage configuration from the server
53
+ * This fetches the current storage provider and public URL base
54
+ */
55
+ async getStorageConfig() {
56
+ if (this.storageConfig) {
57
+ return this.storageConfig;
58
+ }
59
+ try {
60
+ const response = await this.request("/storage/config");
61
+ if (response.success && response.data) {
62
+ this.storageConfig = response.data;
63
+ return response.data;
64
+ }
65
+ } catch {
66
+ }
67
+ return {
68
+ provider: "wasabi",
69
+ publicUrlBase: "https://s3.eu-central-2.wasabisys.com/vlibe.com"
70
+ };
71
+ }
72
+ /**
73
+ * Clear cached storage config (call if config might have changed)
74
+ */
75
+ clearStorageConfigCache() {
76
+ this.storageConfig = null;
77
+ }
78
+ /**
79
+ * Make an authenticated API request
80
+ */
81
+ async request(path, options = {}) {
82
+ const url = `${this.baseUrl}/api${path}`;
83
+ const headers = {
84
+ "X-App-Id": this.appId,
85
+ "X-App-Secret": this.appSecret,
86
+ ...options.headers || {}
87
+ };
88
+ if (this.authToken) {
89
+ headers["Authorization"] = `Bearer ${this.authToken}`;
90
+ }
91
+ const response = await fetch(url, {
92
+ ...options,
93
+ headers
94
+ });
95
+ const data = await response.json();
96
+ if (!response.ok && !data.error) {
97
+ return {
98
+ success: false,
99
+ error: `HTTP ${response.status}: ${response.statusText}`
100
+ };
101
+ }
102
+ return data;
103
+ }
104
+ // ============================================
105
+ // UPLOAD METHODS
106
+ // ============================================
107
+ /**
108
+ * Upload a file directly
109
+ */
110
+ async upload(file, options = {}) {
111
+ const formData = new FormData();
112
+ formData.append("file", file);
113
+ if (options.folder) {
114
+ formData.append("folder", options.folder);
115
+ }
116
+ if (options.isPublic) {
117
+ formData.append("isPublic", "true");
118
+ }
119
+ const response = await this.request("/storage", {
120
+ method: "POST",
121
+ body: formData
122
+ });
123
+ if (!response.success || !response.data) {
124
+ throw new Error(response.error || "Upload failed");
125
+ }
126
+ return response.data;
127
+ }
128
+ /**
129
+ * Get a presigned URL for direct upload to storage
130
+ * Useful for large files or when you want upload progress
131
+ */
132
+ async getUploadUrl(filename, mimeType, size, options = {}) {
133
+ const response = await this.request("/storage/upload-url", {
134
+ method: "POST",
135
+ headers: {
136
+ "Content-Type": "application/json"
137
+ },
138
+ body: JSON.stringify({
139
+ filename,
140
+ mimeType,
141
+ size,
142
+ folder: options.folder,
143
+ isPublic: options.isPublic
144
+ })
145
+ });
146
+ if (!response.success || !response.data) {
147
+ throw new Error(response.error || "Failed to get upload URL");
148
+ }
149
+ return response.data;
150
+ }
151
+ /**
152
+ * Upload a file using presigned URL (for progress tracking)
153
+ */
154
+ async uploadWithProgress(file, options = {}) {
155
+ const { uploadUrl, key, expiresIn } = await this.getUploadUrl(
156
+ file.name,
157
+ file.type,
158
+ file.size,
159
+ options
160
+ );
161
+ await new Promise((resolve, reject) => {
162
+ const xhr = new XMLHttpRequest();
163
+ if (options.onProgress) {
164
+ xhr.upload.addEventListener("progress", (e) => {
165
+ if (e.lengthComputable) {
166
+ const percent = Math.round(e.loaded / e.total * 100);
167
+ options.onProgress(percent);
168
+ }
169
+ });
170
+ }
171
+ xhr.addEventListener("load", () => {
172
+ if (xhr.status >= 200 && xhr.status < 300) {
173
+ resolve();
174
+ } else {
175
+ reject(new Error(`Upload failed with status ${xhr.status}`));
176
+ }
177
+ });
178
+ xhr.addEventListener("error", () => {
179
+ reject(new Error("Upload failed"));
180
+ });
181
+ xhr.open("PUT", uploadUrl);
182
+ xhr.setRequestHeader("Content-Type", file.type);
183
+ xhr.send(file);
184
+ });
185
+ const response = await this.request("/storage/upload-url", {
186
+ method: "PUT",
187
+ headers: {
188
+ "Content-Type": "application/json"
189
+ },
190
+ body: JSON.stringify({
191
+ key,
192
+ filename: file.name,
193
+ mimeType: file.type,
194
+ size: file.size,
195
+ folder: options.folder,
196
+ isPublic: options.isPublic
197
+ })
198
+ });
199
+ if (!response.success || !response.data) {
200
+ throw new Error(response.error || "Failed to confirm upload");
201
+ }
202
+ return response.data;
203
+ }
204
+ // ============================================
205
+ // DOWNLOAD METHODS
206
+ // ============================================
207
+ /**
208
+ * Get a signed download URL for a file
209
+ */
210
+ async getDownloadUrl(fileId, expiresIn) {
211
+ const params = new URLSearchParams({ download: "true" });
212
+ if (expiresIn) {
213
+ params.set("expiresIn", expiresIn.toString());
214
+ }
215
+ const response = await this.request(
216
+ `/storage/${fileId}?${params}`
217
+ );
218
+ if (!response.success || !response.data) {
219
+ throw new Error(response.error || "Failed to get download URL");
220
+ }
221
+ return response.data.url;
222
+ }
223
+ /**
224
+ * Get a public URL for a file (only works for public files in the public path)
225
+ * Returns the direct URL based on the configured storage provider
226
+ *
227
+ * Note: This is a sync method that uses cached config. Call getStorageConfig()
228
+ * first to ensure the config is loaded, or use getPublicUrlAsync() for a
229
+ * guaranteed fresh URL.
230
+ */
231
+ getPublicUrl(key) {
232
+ if (this.storageConfig) {
233
+ const base = this.storageConfig.publicUrlBase.replace(/\/$/, "");
234
+ return `${base}/${key}`;
235
+ }
236
+ const region = "eu-central-2";
237
+ const bucket = "vlibe.com";
238
+ return `https://s3.${region}.wasabisys.com/${bucket}/${key}`;
239
+ }
240
+ /**
241
+ * Get a public URL for a file (async version that ensures fresh config)
242
+ * Returns the direct URL based on the configured storage provider
243
+ */
244
+ async getPublicUrlAsync(key) {
245
+ const config = await this.getStorageConfig();
246
+ const base = config.publicUrlBase.replace(/\/$/, "");
247
+ return `${base}/${key}`;
248
+ }
249
+ /**
250
+ * Check if a storage key is in the public path
251
+ */
252
+ isPublicKey(key) {
253
+ return key.startsWith("vlibe-storage/public/");
254
+ }
255
+ // ============================================
256
+ // FILE MANAGEMENT
257
+ // ============================================
258
+ /**
259
+ * List files
260
+ */
261
+ async list(options = {}) {
262
+ const params = new URLSearchParams();
263
+ if (options.folder !== void 0) {
264
+ params.set("folder", options.folder);
265
+ }
266
+ if (options.limit) {
267
+ params.set("limit", options.limit.toString());
268
+ }
269
+ if (options.offset) {
270
+ params.set("offset", options.offset.toString());
271
+ }
272
+ const response = await this.request(
273
+ `/storage?${params}`
274
+ );
275
+ if (!response.success || !response.data) {
276
+ throw new Error(response.error || "Failed to list files");
277
+ }
278
+ return response.data;
279
+ }
280
+ /**
281
+ * Get a single file's metadata
282
+ */
283
+ async get(fileId) {
284
+ const response = await this.request(`/storage/${fileId}`);
285
+ if (!response.success) {
286
+ if (response.error?.includes("not found")) {
287
+ return null;
288
+ }
289
+ throw new Error(response.error || "Failed to get file");
290
+ }
291
+ return response.data || null;
292
+ }
293
+ /**
294
+ * Delete a file
295
+ */
296
+ async delete(fileId) {
297
+ const response = await this.request(`/storage/${fileId}`, {
298
+ method: "DELETE"
299
+ });
300
+ if (!response.success) {
301
+ if (response.error?.includes("not found")) {
302
+ return false;
303
+ }
304
+ throw new Error(response.error || "Failed to delete file");
305
+ }
306
+ return true;
307
+ }
308
+ /**
309
+ * Delete multiple files
310
+ */
311
+ async deleteMany(fileIds) {
312
+ const deleted = [];
313
+ const failed = [];
314
+ const batchSize = 10;
315
+ for (let i = 0; i < fileIds.length; i += batchSize) {
316
+ const batch = fileIds.slice(i, i + batchSize);
317
+ const results = await Promise.allSettled(
318
+ batch.map((id) => this.delete(id))
319
+ );
320
+ results.forEach((result, index) => {
321
+ const id = batch[index];
322
+ if (result.status === "fulfilled" && result.value) {
323
+ deleted.push(id);
324
+ } else {
325
+ failed.push(id);
326
+ }
327
+ });
328
+ }
329
+ return { deleted, failed };
330
+ }
331
+ // ============================================
332
+ // USAGE & LIMITS
333
+ // ============================================
334
+ /**
335
+ * Get storage usage statistics
336
+ */
337
+ async getUsage() {
338
+ const response = await this.request("/storage/usage");
339
+ if (!response.success || !response.data) {
340
+ throw new Error(response.error || "Failed to get usage");
341
+ }
342
+ return response.data;
343
+ }
344
+ /**
345
+ * Check if a file of given size can be uploaded
346
+ */
347
+ async canUpload(size) {
348
+ const response = await this.request("/storage/usage", {
349
+ method: "POST",
350
+ headers: {
351
+ "Content-Type": "application/json"
352
+ },
353
+ body: JSON.stringify({ size })
354
+ });
355
+ if (!response.success || !response.data) {
356
+ throw new Error(response.error || "Failed to check upload");
357
+ }
358
+ return response.data;
359
+ }
360
+ // ============================================
361
+ // FOLDER MANAGEMENT
362
+ // ============================================
363
+ /**
364
+ * List folders
365
+ */
366
+ async listFolders(options = {}) {
367
+ const params = new URLSearchParams();
368
+ if (options.parentId !== void 0) {
369
+ params.set("parentId", options.parentId === null ? "null" : options.parentId);
370
+ }
371
+ if (options.projectId) {
372
+ params.set("projectId", options.projectId);
373
+ }
374
+ const response = await this.request(`/storage/folders?${params}`);
375
+ if (!response.success || !response.data) {
376
+ throw new Error(response.error || "Failed to list folders");
377
+ }
378
+ return response.data;
379
+ }
380
+ /**
381
+ * Create a folder
382
+ */
383
+ async createFolder(name, options = {}) {
384
+ const response = await this.request("/storage/folders", {
385
+ method: "POST",
386
+ headers: {
387
+ "Content-Type": "application/json"
388
+ },
389
+ body: JSON.stringify({
390
+ name,
391
+ parentId: options.parentId,
392
+ projectId: options.projectId,
393
+ isProjectRoot: options.isProjectRoot
394
+ })
395
+ });
396
+ if (!response.success || !response.data) {
397
+ throw new Error(response.error || "Failed to create folder");
398
+ }
399
+ return response.data;
400
+ }
401
+ /**
402
+ * Rename a folder
403
+ */
404
+ async renameFolder(folderId, name) {
405
+ const response = await this.request(`/storage/folders/${folderId}`, {
406
+ method: "PATCH",
407
+ headers: {
408
+ "Content-Type": "application/json"
409
+ },
410
+ body: JSON.stringify({ name })
411
+ });
412
+ if (!response.success || !response.data) {
413
+ throw new Error(response.error || "Failed to rename folder");
414
+ }
415
+ return response.data;
416
+ }
417
+ /**
418
+ * Delete a folder (must be empty)
419
+ */
420
+ async deleteFolder(folderId) {
421
+ const response = await this.request(`/storage/folders/${folderId}`, {
422
+ method: "DELETE"
423
+ });
424
+ if (!response.success) {
425
+ if (response.error?.includes("not found")) {
426
+ return false;
427
+ }
428
+ throw new Error(response.error || "Failed to delete folder");
429
+ }
430
+ return true;
431
+ }
432
+ // ============================================
433
+ // FILE COPY & REFERENCE
434
+ // ============================================
435
+ /**
436
+ * Copy a file to another project
437
+ * Creates a new database record pointing to the same S3 object
438
+ */
439
+ async copyFileToProject(fileId, targetProjectId) {
440
+ const response = await this.request("/storage/files/copy", {
441
+ method: "POST",
442
+ headers: {
443
+ "Content-Type": "application/json"
444
+ },
445
+ body: JSON.stringify({ fileId, targetProjectId })
446
+ });
447
+ if (!response.success || !response.data) {
448
+ throw new Error(response.error || "Failed to copy file");
449
+ }
450
+ return response.data;
451
+ }
452
+ /**
453
+ * Create a file reference (link file to another project without copying)
454
+ * Does not count against storage quota
455
+ */
456
+ async createFileReference(fileId, targetProjectId) {
457
+ const response = await this.request("/storage/files/reference", {
458
+ method: "POST",
459
+ headers: {
460
+ "Content-Type": "application/json"
461
+ },
462
+ body: JSON.stringify({ fileId, targetProjectId })
463
+ });
464
+ if (!response.success || !response.data) {
465
+ throw new Error(response.error || "Failed to create file reference");
466
+ }
467
+ return response.data;
468
+ }
469
+ };
470
+
471
+ // src/hooks/useStorage.ts
472
+ var import_react = require("react");
473
+ function useStorage(client, options = {}) {
474
+ const { folder, autoFetch = true, limit = 50 } = options;
475
+ const [files, setFiles] = (0, import_react.useState)([]);
476
+ const [total, setTotal] = (0, import_react.useState)(0);
477
+ const [loading, setLoading] = (0, import_react.useState)(false);
478
+ const [error, setError] = (0, import_react.useState)(null);
479
+ const [uploading, setUploading] = (0, import_react.useState)(false);
480
+ const [uploadProgress, setUploadProgress] = (0, import_react.useState)(0);
481
+ const [hasMore, setHasMore] = (0, import_react.useState)(false);
482
+ const offsetRef = (0, import_react.useRef)(0);
483
+ const fetchFiles = (0, import_react.useCallback)(
484
+ async (append = false) => {
485
+ setLoading(true);
486
+ setError(null);
487
+ try {
488
+ const listOptions = {
489
+ folder,
490
+ limit,
491
+ offset: append ? offsetRef.current : 0
492
+ };
493
+ const result = await client.list(listOptions);
494
+ if (append) {
495
+ setFiles((prev) => [...prev, ...result.files]);
496
+ } else {
497
+ setFiles(result.files);
498
+ offsetRef.current = 0;
499
+ }
500
+ setTotal(result.total);
501
+ setHasMore(result.hasMore);
502
+ offsetRef.current = (append ? offsetRef.current : 0) + result.files.length;
503
+ } catch (err) {
504
+ setError(err instanceof Error ? err.message : "Failed to fetch files");
505
+ } finally {
506
+ setLoading(false);
507
+ }
508
+ },
509
+ [client, folder, limit]
510
+ );
511
+ (0, import_react.useEffect)(() => {
512
+ if (autoFetch) {
513
+ fetchFiles();
514
+ }
515
+ }, [autoFetch, fetchFiles]);
516
+ const upload = (0, import_react.useCallback)(
517
+ async (file, uploadOptions = {}) => {
518
+ setUploading(true);
519
+ setUploadProgress(0);
520
+ setError(null);
521
+ try {
522
+ const result = await client.upload(file, {
523
+ ...uploadOptions,
524
+ folder: uploadOptions.folder ?? folder
525
+ });
526
+ await fetchFiles();
527
+ return result;
528
+ } catch (err) {
529
+ const message = err instanceof Error ? err.message : "Upload failed";
530
+ setError(message);
531
+ throw err;
532
+ } finally {
533
+ setUploading(false);
534
+ setUploadProgress(0);
535
+ }
536
+ },
537
+ [client, folder, fetchFiles]
538
+ );
539
+ const uploadWithProgress = (0, import_react.useCallback)(
540
+ async (file, uploadOptions = {}) => {
541
+ setUploading(true);
542
+ setUploadProgress(0);
543
+ setError(null);
544
+ try {
545
+ const result = await client.uploadWithProgress(file, {
546
+ ...uploadOptions,
547
+ folder: uploadOptions.folder ?? folder,
548
+ onProgress: (progress) => {
549
+ setUploadProgress(progress);
550
+ uploadOptions.onProgress?.(progress);
551
+ }
552
+ });
553
+ await fetchFiles();
554
+ return result;
555
+ } catch (err) {
556
+ const message = err instanceof Error ? err.message : "Upload failed";
557
+ setError(message);
558
+ throw err;
559
+ } finally {
560
+ setUploading(false);
561
+ setUploadProgress(0);
562
+ }
563
+ },
564
+ [client, folder, fetchFiles]
565
+ );
566
+ const remove = (0, import_react.useCallback)(
567
+ async (fileId) => {
568
+ setError(null);
569
+ try {
570
+ const deleted = await client.delete(fileId);
571
+ if (deleted) {
572
+ setFiles((prev) => prev.filter((f) => f.id !== fileId));
573
+ setTotal((prev) => prev - 1);
574
+ }
575
+ return deleted;
576
+ } catch (err) {
577
+ const message = err instanceof Error ? err.message : "Delete failed";
578
+ setError(message);
579
+ throw err;
580
+ }
581
+ },
582
+ [client]
583
+ );
584
+ const refresh = (0, import_react.useCallback)(async () => {
585
+ await fetchFiles(false);
586
+ }, [fetchFiles]);
587
+ const loadMore = (0, import_react.useCallback)(async () => {
588
+ if (hasMore && !loading) {
589
+ await fetchFiles(true);
590
+ }
591
+ }, [hasMore, loading, fetchFiles]);
592
+ return {
593
+ files,
594
+ total,
595
+ loading,
596
+ error,
597
+ upload,
598
+ uploadWithProgress,
599
+ uploadProgress,
600
+ uploading,
601
+ remove,
602
+ refresh,
603
+ loadMore,
604
+ hasMore
605
+ };
606
+ }
607
+
608
+ // src/hooks/useStorageUsage.ts
609
+ var import_react2 = require("react");
610
+ function useStorageUsage(client, options = {}) {
611
+ const { autoFetch = true, refreshInterval = 0 } = options;
612
+ const [usage, setUsage] = (0, import_react2.useState)(null);
613
+ const [loading, setLoading] = (0, import_react2.useState)(false);
614
+ const [error, setError] = (0, import_react2.useState)(null);
615
+ const fetchUsage = (0, import_react2.useCallback)(async () => {
616
+ setLoading(true);
617
+ setError(null);
618
+ try {
619
+ const stats = await client.getUsage();
620
+ setUsage(stats);
621
+ } catch (err) {
622
+ setError(err instanceof Error ? err.message : "Failed to fetch usage");
623
+ } finally {
624
+ setLoading(false);
625
+ }
626
+ }, [client]);
627
+ (0, import_react2.useEffect)(() => {
628
+ if (autoFetch) {
629
+ fetchUsage();
630
+ }
631
+ }, [autoFetch, fetchUsage]);
632
+ (0, import_react2.useEffect)(() => {
633
+ if (refreshInterval > 0) {
634
+ const interval = setInterval(fetchUsage, refreshInterval);
635
+ return () => clearInterval(interval);
636
+ }
637
+ }, [refreshInterval, fetchUsage]);
638
+ const canUpload = (0, import_react2.useCallback)(
639
+ (size) => {
640
+ if (!usage) return true;
641
+ if (usage.storageLimit === 0) return true;
642
+ return usage.bytesUsed + size <= usage.storageLimit;
643
+ },
644
+ [usage]
645
+ );
646
+ const usagePercent = usage?.usagePercent ?? 0;
647
+ const isLimitReached = usagePercent >= 100;
648
+ const isNearLimit = usagePercent >= 80;
649
+ const usageFormatted = usage ? `${usage.bytesUsedFormatted} of ${usage.storageLimitFormatted}` : "Loading...";
650
+ return {
651
+ usage,
652
+ loading,
653
+ error,
654
+ canUpload,
655
+ refresh: fetchUsage,
656
+ usagePercent,
657
+ usageFormatted,
658
+ isLimitReached,
659
+ isNearLimit
660
+ };
661
+ }
662
+ // Annotate the CommonJS export names for ESM import in node:
663
+ 0 && (module.exports = {
664
+ VlibeStorage,
665
+ useStorage,
666
+ useStorageUsage
667
+ });
668
+ //# sourceMappingURL=react.js.map