hazo_files 1.0.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.
@@ -0,0 +1,964 @@
1
+ # Adding Custom Storage Modules
2
+
3
+ This guide walks you through creating custom storage modules for hazo_files. By implementing the `StorageModule` interface, you can integrate any storage backend (S3, Dropbox, OneDrive, WebDAV, etc.) with the unified hazo_files API.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Module Architecture](#module-architecture)
8
+ 2. [Quick Start](#quick-start)
9
+ 3. [Step-by-Step Guide](#step-by-step-guide)
10
+ 4. [Interface Reference](#interface-reference)
11
+ 5. [Best Practices](#best-practices)
12
+ 6. [Testing](#testing)
13
+ 7. [Examples](#examples)
14
+
15
+ ## Module Architecture
16
+
17
+ ### Overview
18
+
19
+ ```
20
+ Your Custom Module
21
+
22
+ ├── Extends BaseStorageModule
23
+ │ ├── Common utilities (path, results)
24
+ │ ├── Configuration management
25
+ │ └── Tree building logic
26
+
27
+ ├── Implements StorageModule interface
28
+ │ ├── All required operations
29
+ │ └── Provider-specific logic
30
+
31
+ └── Integrates with storage backend
32
+ └── S3, Dropbox, custom API, etc.
33
+ ```
34
+
35
+ ### Key Components
36
+
37
+ 1. **BaseStorageModule**: Abstract base class providing common functionality
38
+ 2. **StorageModule Interface**: Contract defining all required operations
39
+ 3. **Provider-Specific Logic**: Your implementation using the storage backend API
40
+ 4. **Configuration**: Provider-specific config added to `HazoFilesConfig`
41
+
42
+ ## Quick Start
43
+
44
+ ### Minimal Example
45
+
46
+ ```typescript
47
+ import { BaseStorageModule } from 'hazo_files';
48
+ import type { StorageProvider, HazoFilesConfig, OperationResult, FileItem } from 'hazo_files';
49
+
50
+ // 1. Define your storage provider type
51
+ export type CustomStorageProvider = 'my_storage';
52
+
53
+ // 2. Define configuration interface
54
+ export interface MyStorageConfig {
55
+ apiKey: string;
56
+ endpoint: string;
57
+ }
58
+
59
+ // 3. Extend BaseStorageModule
60
+ export class MyStorageModule extends BaseStorageModule {
61
+ readonly provider: StorageProvider = 'my_storage' as StorageProvider;
62
+ private apiKey: string = '';
63
+ private endpoint: string = '';
64
+
65
+ async initialize(config: HazoFilesConfig): Promise<void> {
66
+ await super.initialize(config);
67
+ const myConfig = this.getProviderConfig<MyStorageConfig>();
68
+ this.apiKey = myConfig.apiKey;
69
+ this.endpoint = myConfig.endpoint;
70
+ // Initialize your storage client
71
+ }
72
+
73
+ async uploadFile(source, remotePath, options?): Promise<OperationResult<FileItem>> {
74
+ this.ensureInitialized();
75
+ try {
76
+ // Implement upload logic
77
+ const fileItem = await this.performUpload(source, remotePath);
78
+ return this.successResult(fileItem);
79
+ } catch (error) {
80
+ return this.errorResult(`Upload failed: ${(error as Error).message}`);
81
+ }
82
+ }
83
+
84
+ // Implement other required methods...
85
+ }
86
+
87
+ // 4. Export factory function
88
+ export function createMyStorageModule(): MyStorageModule {
89
+ return new MyStorageModule();
90
+ }
91
+ ```
92
+
93
+ ### Registration
94
+
95
+ ```typescript
96
+ import { registerModule } from 'hazo_files';
97
+ import { createMyStorageModule } from './my-storage-module';
98
+
99
+ // Register your module
100
+ registerModule('my_storage', createMyStorageModule);
101
+
102
+ // Now you can use it
103
+ const fm = await createInitializedFileManager({
104
+ config: {
105
+ provider: 'my_storage',
106
+ my_storage: {
107
+ apiKey: 'your-api-key',
108
+ endpoint: 'https://api.example.com'
109
+ }
110
+ }
111
+ });
112
+ ```
113
+
114
+ ## Step-by-Step Guide
115
+
116
+ ### Step 1: Setup
117
+
118
+ Create a new file for your module:
119
+
120
+ ```bash
121
+ mkdir -p src/modules/my-storage
122
+ touch src/modules/my-storage/index.ts
123
+ touch src/modules/my-storage/client.ts
124
+ ```
125
+
126
+ ### Step 2: Define Types
127
+
128
+ ```typescript
129
+ // src/modules/my-storage/types.ts
130
+ import type { StorageProvider } from 'hazo_files';
131
+
132
+ // Extend StorageProvider type
133
+ export type MyStorageProvider = 'my_storage';
134
+
135
+ // Configuration interface
136
+ export interface MyStorageConfig {
137
+ apiKey: string;
138
+ endpoint: string;
139
+ bucket?: string;
140
+ region?: string;
141
+ }
142
+
143
+ // Add to HazoFilesConfig
144
+ declare module 'hazo_files' {
145
+ interface HazoFilesConfig {
146
+ my_storage?: MyStorageConfig;
147
+ }
148
+ }
149
+ ```
150
+
151
+ ### Step 3: Create Storage Client
152
+
153
+ ```typescript
154
+ // src/modules/my-storage/client.ts
155
+ import axios from 'axios';
156
+
157
+ export class MyStorageClient {
158
+ private apiKey: string;
159
+ private endpoint: string;
160
+
161
+ constructor(apiKey: string, endpoint: string) {
162
+ this.apiKey = apiKey;
163
+ this.endpoint = endpoint;
164
+ }
165
+
166
+ async uploadFile(path: string, data: Buffer): Promise<any> {
167
+ const response = await axios.post(
168
+ `${this.endpoint}/files`,
169
+ data,
170
+ {
171
+ headers: {
172
+ 'Authorization': `Bearer ${this.apiKey}`,
173
+ 'Content-Type': 'application/octet-stream',
174
+ 'X-File-Path': path,
175
+ }
176
+ }
177
+ );
178
+ return response.data;
179
+ }
180
+
181
+ async downloadFile(path: string): Promise<Buffer> {
182
+ const response = await axios.get(
183
+ `${this.endpoint}/files/${encodeURIComponent(path)}`,
184
+ {
185
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
186
+ responseType: 'arraybuffer',
187
+ }
188
+ );
189
+ return Buffer.from(response.data);
190
+ }
191
+
192
+ async listFiles(path: string): Promise<any[]> {
193
+ const response = await axios.get(
194
+ `${this.endpoint}/files`,
195
+ {
196
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
197
+ params: { path },
198
+ }
199
+ );
200
+ return response.data.files;
201
+ }
202
+
203
+ async deleteFile(path: string): Promise<void> {
204
+ await axios.delete(
205
+ `${this.endpoint}/files/${encodeURIComponent(path)}`,
206
+ {
207
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
208
+ }
209
+ );
210
+ }
211
+
212
+ // Add other methods as needed
213
+ }
214
+ ```
215
+
216
+ ### Step 4: Implement Module
217
+
218
+ ```typescript
219
+ // src/modules/my-storage/index.ts
220
+ import { BaseStorageModule } from '../../common/base-module';
221
+ import { FileNotFoundError, DirectoryNotFoundError } from '../../common/errors';
222
+ import { generateId, createFileItem, createFolderItem } from '../../common/utils';
223
+ import { getMimeType } from '../../common/mime-types';
224
+ import { MyStorageClient } from './client';
225
+ import type {
226
+ StorageProvider,
227
+ HazoFilesConfig,
228
+ FileItem,
229
+ FolderItem,
230
+ FileSystemItem,
231
+ OperationResult,
232
+ UploadOptions,
233
+ DownloadOptions,
234
+ MoveOptions,
235
+ RenameOptions,
236
+ ListOptions,
237
+ } from '../../types';
238
+ import type { MyStorageConfig } from './types';
239
+
240
+ export class MyStorageModule extends BaseStorageModule {
241
+ readonly provider: StorageProvider = 'my_storage' as StorageProvider;
242
+ private client: MyStorageClient | null = null;
243
+
244
+ async initialize(config: HazoFilesConfig): Promise<void> {
245
+ await super.initialize(config);
246
+
247
+ const myConfig = this.getProviderConfig<MyStorageConfig>();
248
+ this.client = new MyStorageClient(myConfig.apiKey, myConfig.endpoint);
249
+ }
250
+
251
+ async createDirectory(path: string): Promise<OperationResult<FolderItem>> {
252
+ this.ensureInitialized();
253
+
254
+ try {
255
+ const normalized = this.normalizePath(path);
256
+
257
+ // Call your storage API to create directory
258
+ await this.client!.createDirectory(normalized);
259
+
260
+ // Create FolderItem
261
+ const folder = createFolderItem({
262
+ id: generateId(),
263
+ name: this.getBaseName(normalized),
264
+ path: normalized,
265
+ createdAt: new Date(),
266
+ modifiedAt: new Date(),
267
+ });
268
+
269
+ return this.successResult(folder);
270
+ } catch (error) {
271
+ return this.errorResult(`Failed to create directory: ${(error as Error).message}`);
272
+ }
273
+ }
274
+
275
+ async removeDirectory(path: string, recursive = false): Promise<OperationResult> {
276
+ this.ensureInitialized();
277
+
278
+ try {
279
+ const normalized = this.normalizePath(path);
280
+
281
+ // Check if directory is empty (if not recursive)
282
+ if (!recursive) {
283
+ const contents = await this.client!.listFiles(normalized);
284
+ if (contents.length > 0) {
285
+ return this.errorResult('Directory is not empty');
286
+ }
287
+ }
288
+
289
+ await this.client!.deleteDirectory(normalized, recursive);
290
+ return this.successResult();
291
+ } catch (error) {
292
+ return this.errorResult(`Failed to remove directory: ${(error as Error).message}`);
293
+ }
294
+ }
295
+
296
+ async uploadFile(
297
+ source: string | Buffer | ReadableStream,
298
+ remotePath: string,
299
+ options: UploadOptions = {}
300
+ ): Promise<OperationResult<FileItem>> {
301
+ this.ensureInitialized();
302
+
303
+ try {
304
+ const normalized = this.normalizePath(remotePath);
305
+ const fileName = this.getBaseName(normalized);
306
+
307
+ // Convert source to Buffer
308
+ let buffer: Buffer;
309
+ if (typeof source === 'string') {
310
+ const fs = await import('fs');
311
+ buffer = await fs.promises.readFile(source);
312
+ } else if (Buffer.isBuffer(source)) {
313
+ buffer = source;
314
+ } else {
315
+ // ReadableStream to Buffer
316
+ const chunks: Uint8Array[] = [];
317
+ const reader = source.getReader();
318
+ while (true) {
319
+ const { done, value } = await reader.read();
320
+ if (done) break;
321
+ chunks.push(value);
322
+ }
323
+ buffer = Buffer.concat(chunks.map(c => Buffer.from(c)));
324
+ }
325
+
326
+ // Upload to storage
327
+ const result = await this.client!.uploadFile(normalized, buffer);
328
+
329
+ // Track progress if callback provided
330
+ if (options.onProgress) {
331
+ options.onProgress(100, buffer.length, buffer.length);
332
+ }
333
+
334
+ // Create FileItem
335
+ const fileItem = createFileItem({
336
+ id: result.id || generateId(),
337
+ name: fileName,
338
+ path: normalized,
339
+ size: buffer.length,
340
+ mimeType: getMimeType(fileName),
341
+ createdAt: new Date(result.created_at),
342
+ modifiedAt: new Date(result.modified_at),
343
+ metadata: result.metadata,
344
+ });
345
+
346
+ return this.successResult(fileItem);
347
+ } catch (error) {
348
+ return this.errorResult(`Upload failed: ${(error as Error).message}`);
349
+ }
350
+ }
351
+
352
+ async downloadFile(
353
+ remotePath: string,
354
+ localPath?: string,
355
+ options: DownloadOptions = {}
356
+ ): Promise<OperationResult<Buffer | string>> {
357
+ this.ensureInitialized();
358
+
359
+ try {
360
+ const normalized = this.normalizePath(remotePath);
361
+ const buffer = await this.client!.downloadFile(normalized);
362
+
363
+ if (options.onProgress) {
364
+ options.onProgress(100, buffer.length, buffer.length);
365
+ }
366
+
367
+ if (localPath) {
368
+ const fs = await import('fs');
369
+ const path = await import('path');
370
+ await fs.promises.mkdir(path.dirname(localPath), { recursive: true });
371
+ await fs.promises.writeFile(localPath, buffer);
372
+ return this.successResult(localPath);
373
+ }
374
+
375
+ return this.successResult(buffer);
376
+ } catch (error) {
377
+ return this.errorResult(`Download failed: ${(error as Error).message}`);
378
+ }
379
+ }
380
+
381
+ async moveItem(
382
+ sourcePath: string,
383
+ destinationPath: string,
384
+ options: MoveOptions = {}
385
+ ): Promise<OperationResult<FileSystemItem>> {
386
+ this.ensureInitialized();
387
+
388
+ try {
389
+ const normalizedSource = this.normalizePath(sourcePath);
390
+ const normalizedDest = this.normalizePath(destinationPath);
391
+
392
+ const result = await this.client!.moveFile(normalizedSource, normalizedDest);
393
+
394
+ // Convert result to FileSystemItem
395
+ const item = result.is_directory
396
+ ? createFolderItem({
397
+ id: result.id,
398
+ name: this.getBaseName(normalizedDest),
399
+ path: normalizedDest,
400
+ createdAt: new Date(result.created_at),
401
+ modifiedAt: new Date(result.modified_at),
402
+ })
403
+ : createFileItem({
404
+ id: result.id,
405
+ name: this.getBaseName(normalizedDest),
406
+ path: normalizedDest,
407
+ size: result.size,
408
+ mimeType: result.mime_type,
409
+ createdAt: new Date(result.created_at),
410
+ modifiedAt: new Date(result.modified_at),
411
+ });
412
+
413
+ return this.successResult(item);
414
+ } catch (error) {
415
+ return this.errorResult(`Move failed: ${(error as Error).message}`);
416
+ }
417
+ }
418
+
419
+ async deleteFile(path: string): Promise<OperationResult> {
420
+ this.ensureInitialized();
421
+
422
+ try {
423
+ const normalized = this.normalizePath(path);
424
+ await this.client!.deleteFile(normalized);
425
+ return this.successResult();
426
+ } catch (error) {
427
+ return this.errorResult(`Delete failed: ${(error as Error).message}`);
428
+ }
429
+ }
430
+
431
+ async renameFile(
432
+ path: string,
433
+ newName: string,
434
+ options: RenameOptions = {}
435
+ ): Promise<OperationResult<FileItem>> {
436
+ this.ensureInitialized();
437
+
438
+ try {
439
+ const normalized = this.normalizePath(path);
440
+ const parentPath = this.getParentPath(normalized);
441
+ const newPath = this.joinPath(parentPath, newName);
442
+
443
+ const result = await this.client!.renameFile(normalized, newName);
444
+
445
+ const fileItem = createFileItem({
446
+ id: result.id,
447
+ name: newName,
448
+ path: newPath,
449
+ size: result.size,
450
+ mimeType: result.mime_type,
451
+ createdAt: new Date(result.created_at),
452
+ modifiedAt: new Date(result.modified_at),
453
+ });
454
+
455
+ return this.successResult(fileItem);
456
+ } catch (error) {
457
+ return this.errorResult(`Rename failed: ${(error as Error).message}`);
458
+ }
459
+ }
460
+
461
+ async renameFolder(
462
+ path: string,
463
+ newName: string,
464
+ options: RenameOptions = {}
465
+ ): Promise<OperationResult<FolderItem>> {
466
+ this.ensureInitialized();
467
+
468
+ try {
469
+ const normalized = this.normalizePath(path);
470
+ const parentPath = this.getParentPath(normalized);
471
+ const newPath = this.joinPath(parentPath, newName);
472
+
473
+ const result = await this.client!.renameFolder(normalized, newName);
474
+
475
+ const folderItem = createFolderItem({
476
+ id: result.id,
477
+ name: newName,
478
+ path: newPath,
479
+ createdAt: new Date(result.created_at),
480
+ modifiedAt: new Date(result.modified_at),
481
+ });
482
+
483
+ return this.successResult(folderItem);
484
+ } catch (error) {
485
+ return this.errorResult(`Rename failed: ${(error as Error).message}`);
486
+ }
487
+ }
488
+
489
+ async listDirectory(
490
+ path: string,
491
+ options: ListOptions = {}
492
+ ): Promise<OperationResult<FileSystemItem[]>> {
493
+ this.ensureInitialized();
494
+
495
+ try {
496
+ const normalized = this.normalizePath(path);
497
+ const files = await this.client!.listFiles(normalized);
498
+
499
+ const items: FileSystemItem[] = files.map(file => {
500
+ const itemPath = this.joinPath(normalized, file.name);
501
+
502
+ if (file.is_directory) {
503
+ return createFolderItem({
504
+ id: file.id,
505
+ name: file.name,
506
+ path: itemPath,
507
+ createdAt: new Date(file.created_at),
508
+ modifiedAt: new Date(file.modified_at),
509
+ });
510
+ }
511
+
512
+ return createFileItem({
513
+ id: file.id,
514
+ name: file.name,
515
+ path: itemPath,
516
+ size: file.size,
517
+ mimeType: file.mime_type || getMimeType(file.name),
518
+ createdAt: new Date(file.created_at),
519
+ modifiedAt: new Date(file.modified_at),
520
+ });
521
+ });
522
+
523
+ // Apply filter if provided
524
+ const filtered = options.filter
525
+ ? items.filter(options.filter)
526
+ : items;
527
+
528
+ return this.successResult(filtered);
529
+ } catch (error) {
530
+ return this.errorResult(`List failed: ${(error as Error).message}`);
531
+ }
532
+ }
533
+
534
+ async getItem(path: string): Promise<OperationResult<FileSystemItem>> {
535
+ this.ensureInitialized();
536
+
537
+ try {
538
+ const normalized = this.normalizePath(path);
539
+ const info = await this.client!.getFileInfo(normalized);
540
+
541
+ const item = info.is_directory
542
+ ? createFolderItem({
543
+ id: info.id,
544
+ name: this.getBaseName(normalized),
545
+ path: normalized,
546
+ createdAt: new Date(info.created_at),
547
+ modifiedAt: new Date(info.modified_at),
548
+ })
549
+ : createFileItem({
550
+ id: info.id,
551
+ name: this.getBaseName(normalized),
552
+ path: normalized,
553
+ size: info.size,
554
+ mimeType: info.mime_type,
555
+ createdAt: new Date(info.created_at),
556
+ modifiedAt: new Date(info.modified_at),
557
+ });
558
+
559
+ return this.successResult(item);
560
+ } catch (error) {
561
+ return this.errorResult(`Get item failed: ${(error as Error).message}`);
562
+ }
563
+ }
564
+
565
+ async exists(path: string): Promise<boolean> {
566
+ this.ensureInitialized();
567
+
568
+ try {
569
+ const normalized = this.normalizePath(path);
570
+ return await this.client!.fileExists(normalized);
571
+ } catch {
572
+ return false;
573
+ }
574
+ }
575
+ }
576
+
577
+ // Factory function
578
+ export function createMyStorageModule(): MyStorageModule {
579
+ return new MyStorageModule();
580
+ }
581
+
582
+ // Export types
583
+ export type { MyStorageConfig } from './types';
584
+ ```
585
+
586
+ ### Step 5: Register Module
587
+
588
+ ```typescript
589
+ // src/modules/index.ts
590
+ import { createMyStorageModule } from './my-storage';
591
+
592
+ // Register module
593
+ registerModule('my_storage', createMyStorageModule);
594
+
595
+ // Export
596
+ export { MyStorageModule, createMyStorageModule } from './my-storage';
597
+ export type { MyStorageConfig } from './my-storage/types';
598
+ ```
599
+
600
+ ### Step 6: Update Package Exports
601
+
602
+ ```typescript
603
+ // src/index.ts
604
+ export {
605
+ MyStorageModule,
606
+ createMyStorageModule,
607
+ } from './modules';
608
+
609
+ export type { MyStorageConfig } from './modules';
610
+ ```
611
+
612
+ ## Interface Reference
613
+
614
+ ### Required Methods
615
+
616
+ Every storage module must implement these methods:
617
+
618
+ ```typescript
619
+ interface StorageModule {
620
+ // Lifecycle
621
+ readonly provider: StorageProvider;
622
+ initialize(config: HazoFilesConfig): Promise<void>;
623
+
624
+ // Directory operations
625
+ createDirectory(path: string): Promise<OperationResult<FolderItem>>;
626
+ removeDirectory(path: string, recursive?: boolean): Promise<OperationResult>;
627
+
628
+ // File operations
629
+ uploadFile(source, remotePath, options?): Promise<OperationResult<FileItem>>;
630
+ downloadFile(remotePath, localPath?, options?): Promise<OperationResult<Buffer | string>>;
631
+ moveItem(sourcePath, destinationPath, options?): Promise<OperationResult<FileSystemItem>>;
632
+ deleteFile(path: string): Promise<OperationResult>;
633
+ renameFile(path, newName, options?): Promise<OperationResult<FileItem>>;
634
+ renameFolder(path, newName, options?): Promise<OperationResult<FolderItem>>;
635
+
636
+ // Query operations
637
+ listDirectory(path, options?): Promise<OperationResult<FileSystemItem[]>>;
638
+ getItem(path: string): Promise<OperationResult<FileSystemItem>>;
639
+ exists(path: string): Promise<boolean>;
640
+ getFolderTree(path?, depth?): Promise<OperationResult<TreeNode[]>>;
641
+ }
642
+ ```
643
+
644
+ ### BaseStorageModule Utilities
645
+
646
+ Available protected methods from `BaseStorageModule`:
647
+
648
+ ```typescript
649
+ // Configuration
650
+ protected ensureInitialized(): void
651
+ protected getProviderConfig<T>(): T
652
+
653
+ // Path utilities
654
+ protected normalizePath(path: string): string
655
+ protected joinPath(...segments: string[]): string
656
+ protected getBaseName(path: string): string
657
+ protected getParentPath(path: string): string
658
+
659
+ // Result helpers
660
+ protected successResult<T>(data?: T): OperationResult<T>
661
+ protected errorResult(error: string): OperationResult
662
+
663
+ // Tree building (can override for optimization)
664
+ protected async buildTree(path, maxDepth, currentDepth): Promise<TreeNode[]>
665
+ ```
666
+
667
+ ## Best Practices
668
+
669
+ ### 1. Path Normalization
670
+
671
+ Always normalize paths at the start of each operation:
672
+
673
+ ```typescript
674
+ async uploadFile(source, remotePath, options?): Promise<OperationResult<FileItem>> {
675
+ this.ensureInitialized();
676
+ const normalized = this.normalizePath(remotePath); // ← Always do this first
677
+ // Rest of implementation
678
+ }
679
+ ```
680
+
681
+ ### 2. Error Handling
682
+
683
+ Use try-catch and return OperationResult:
684
+
685
+ ```typescript
686
+ async uploadFile(...): Promise<OperationResult<FileItem>> {
687
+ try {
688
+ // Implementation
689
+ return this.successResult(fileItem);
690
+ } catch (error) {
691
+ if (error instanceof MySpecificError) {
692
+ return this.errorResult(error.message);
693
+ }
694
+ return this.errorResult(`Unexpected error: ${(error as Error).message}`);
695
+ }
696
+ }
697
+ ```
698
+
699
+ ### 3. Progress Tracking
700
+
701
+ Call progress callback if provided:
702
+
703
+ ```typescript
704
+ if (options?.onProgress) {
705
+ // During upload/download
706
+ options.onProgress(
707
+ (bytesTransferred / totalBytes) * 100,
708
+ bytesTransferred,
709
+ totalBytes
710
+ );
711
+ }
712
+ ```
713
+
714
+ ### 4. Source Type Handling
715
+
716
+ Handle all three source types in uploadFile:
717
+
718
+ ```typescript
719
+ async uploadFile(source, remotePath, options?): Promise<OperationResult<FileItem>> {
720
+ let buffer: Buffer;
721
+
722
+ if (typeof source === 'string') {
723
+ // File path
724
+ buffer = await fs.promises.readFile(source);
725
+ } else if (Buffer.isBuffer(source)) {
726
+ // Already a buffer
727
+ buffer = source;
728
+ } else {
729
+ // ReadableStream
730
+ buffer = await streamToBuffer(source);
731
+ }
732
+
733
+ // Use buffer for upload
734
+ }
735
+ ```
736
+
737
+ ### 5. Metadata Preservation
738
+
739
+ Include provider-specific metadata:
740
+
741
+ ```typescript
742
+ const fileItem = createFileItem({
743
+ id: apiResult.id,
744
+ name: fileName,
745
+ path: normalized,
746
+ size: apiResult.size,
747
+ mimeType: apiResult.mimeType || getMimeType(fileName),
748
+ createdAt: new Date(apiResult.createdAt),
749
+ modifiedAt: new Date(apiResult.modifiedAt),
750
+ metadata: {
751
+ // Provider-specific metadata
752
+ storageClass: apiResult.storageClass,
753
+ etag: apiResult.etag,
754
+ versionId: apiResult.versionId,
755
+ },
756
+ });
757
+ ```
758
+
759
+ ### 6. Configuration Validation
760
+
761
+ Validate configuration in initialize():
762
+
763
+ ```typescript
764
+ async initialize(config: HazoFilesConfig): Promise<void> {
765
+ await super.initialize(config);
766
+
767
+ const myConfig = this.getProviderConfig<MyStorageConfig>();
768
+
769
+ if (!myConfig.apiKey) {
770
+ throw new ConfigurationError('API key is required');
771
+ }
772
+
773
+ if (!myConfig.endpoint) {
774
+ throw new ConfigurationError('Endpoint is required');
775
+ }
776
+
777
+ // Initialize client
778
+ this.client = new MyStorageClient(myConfig);
779
+ }
780
+ ```
781
+
782
+ ## Testing
783
+
784
+ ### Unit Tests
785
+
786
+ ```typescript
787
+ import { describe, it, expect, beforeEach } from 'vitest';
788
+ import { MyStorageModule } from './my-storage';
789
+
790
+ describe('MyStorageModule', () => {
791
+ let module: MyStorageModule;
792
+
793
+ beforeEach(async () => {
794
+ module = new MyStorageModule();
795
+ await module.initialize({
796
+ provider: 'my_storage',
797
+ my_storage: {
798
+ apiKey: 'test-key',
799
+ endpoint: 'https://api.test.com',
800
+ },
801
+ });
802
+ });
803
+
804
+ it('should create directory', async () => {
805
+ const result = await module.createDirectory('/test');
806
+ expect(result.success).toBe(true);
807
+ expect(result.data?.name).toBe('test');
808
+ });
809
+
810
+ it('should upload file', async () => {
811
+ const buffer = Buffer.from('test content');
812
+ const result = await module.uploadFile(buffer, '/test.txt');
813
+ expect(result.success).toBe(true);
814
+ expect(result.data?.size).toBe(buffer.length);
815
+ });
816
+
817
+ // Add more tests...
818
+ });
819
+ ```
820
+
821
+ ### Integration Tests
822
+
823
+ ```typescript
824
+ describe('MyStorageModule Integration', () => {
825
+ it('should upload and download file', async () => {
826
+ const module = new MyStorageModule();
827
+ await module.initialize({
828
+ provider: 'my_storage',
829
+ my_storage: {
830
+ apiKey: process.env.MY_STORAGE_API_KEY!,
831
+ endpoint: process.env.MY_STORAGE_ENDPOINT!,
832
+ },
833
+ });
834
+
835
+ // Upload
836
+ const content = Buffer.from('Hello, World!');
837
+ const uploadResult = await module.uploadFile(content, '/test.txt');
838
+ expect(uploadResult.success).toBe(true);
839
+
840
+ // Download
841
+ const downloadResult = await module.downloadFile('/test.txt');
842
+ expect(downloadResult.success).toBe(true);
843
+ expect(downloadResult.data).toEqual(content);
844
+
845
+ // Cleanup
846
+ await module.deleteFile('/test.txt');
847
+ });
848
+ });
849
+ ```
850
+
851
+ ## Examples
852
+
853
+ ### Example 1: AWS S3 Module (Sketch)
854
+
855
+ ```typescript
856
+ import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
857
+ import { BaseStorageModule } from 'hazo_files';
858
+
859
+ export class S3StorageModule extends BaseStorageModule {
860
+ readonly provider = 's3' as StorageProvider;
861
+ private s3Client: S3Client | null = null;
862
+ private bucket: string = '';
863
+
864
+ async initialize(config: HazoFilesConfig): Promise<void> {
865
+ await super.initialize(config);
866
+ const s3Config = this.getProviderConfig<S3Config>();
867
+
868
+ this.s3Client = new S3Client({
869
+ region: s3Config.region,
870
+ credentials: {
871
+ accessKeyId: s3Config.accessKeyId,
872
+ secretAccessKey: s3Config.secretAccessKey,
873
+ },
874
+ });
875
+
876
+ this.bucket = s3Config.bucket;
877
+ }
878
+
879
+ async uploadFile(source, remotePath, options?): Promise<OperationResult<FileItem>> {
880
+ // Convert source to Buffer
881
+ const buffer = await this.sourceToBuffer(source);
882
+
883
+ // Upload to S3
884
+ await this.s3Client!.send(new PutObjectCommand({
885
+ Bucket: this.bucket,
886
+ Key: remotePath,
887
+ Body: buffer,
888
+ }));
889
+
890
+ // Create FileItem
891
+ // Return result
892
+ }
893
+
894
+ // Implement other methods...
895
+ }
896
+ ```
897
+
898
+ ### Example 2: Dropbox Module (Sketch)
899
+
900
+ ```typescript
901
+ import { Dropbox } from 'dropbox';
902
+ import { BaseStorageModule } from 'hazo_files';
903
+
904
+ export class DropboxStorageModule extends BaseStorageModule {
905
+ readonly provider = 'dropbox' as StorageProvider;
906
+ private dbx: Dropbox | null = null;
907
+
908
+ async initialize(config: HazoFilesConfig): Promise<void> {
909
+ await super.initialize(config);
910
+ const dropboxConfig = this.getProviderConfig<DropboxConfig>();
911
+
912
+ this.dbx = new Dropbox({
913
+ accessToken: dropboxConfig.accessToken,
914
+ });
915
+ }
916
+
917
+ async uploadFile(source, remotePath, options?): Promise<OperationResult<FileItem>> {
918
+ const buffer = await this.sourceToBuffer(source);
919
+
920
+ const response = await this.dbx!.filesUpload({
921
+ path: remotePath,
922
+ contents: buffer,
923
+ });
924
+
925
+ // Create FileItem from response
926
+ // Return result
927
+ }
928
+
929
+ // Implement other methods...
930
+ }
931
+ ```
932
+
933
+ ## Checklist
934
+
935
+ Use this checklist when creating a new module:
936
+
937
+ - [ ] Created module file structure
938
+ - [ ] Defined configuration interface
939
+ - [ ] Extended BaseStorageModule
940
+ - [ ] Implemented all required interface methods
941
+ - [ ] Added path normalization to all methods
942
+ - [ ] Implemented proper error handling with OperationResult
943
+ - [ ] Added progress tracking for upload/download
944
+ - [ ] Handled all source types (string, Buffer, ReadableStream)
945
+ - [ ] Created factory function
946
+ - [ ] Registered module
947
+ - [ ] Updated package exports
948
+ - [ ] Added TypeScript type definitions
949
+ - [ ] Wrote unit tests
950
+ - [ ] Wrote integration tests
951
+ - [ ] Updated documentation
952
+ - [ ] Added example usage
953
+
954
+ ## Support
955
+
956
+ For questions or issues:
957
+
958
+ - Review existing modules: `src/modules/local` and `src/modules/google-drive`
959
+ - Check main documentation: [README.md](../README.md)
960
+ - Visit GitHub: [https://github.com/pub12/hazo_files](https://github.com/pub12/hazo_files)
961
+
962
+ ---
963
+
964
+ Happy module development!