@gravito/nebula 4.0.0 → 4.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/README.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # @gravito/nebula
2
2
 
3
- > The Standard Storage Orbit for Galaxy Architecture.
3
+ > The Standard Storage Orbit for Galaxy Architecture. Lightweight, multi-disk, and pluggable.
4
4
 
5
- Provides a unified file storage abstraction layer with multi-disk support and pluggable backends.
5
+ [![npm version](https://img.shields.io/npm/v/@gravito/nebula.svg)](https://www.npmjs.com/package/@gravito/nebula)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
+ [![Bun](https://img.shields.io/badge/Bun-1.0+-black.svg)](https://bun.sh/)
9
+
10
+ **@gravito/nebula** provides a unified file storage abstraction layer for Gravito applications. Built on the **Orbit** pattern, it acts as a bridge between the core micro-kernel and various storage backends (Local, S3, Memory, etc.), supporting multi-disk configurations and high extensibility via hooks.
11
+
12
+ ## ✨ Features
13
+
14
+ - 🪐 **Orbit Integration** - Seamlessly plugs into the PlanetCore micro-kernel.
15
+ - 💽 **Multi-Disk Support** - Manage multiple storage backends (disks) within a single application.
16
+ - 🔌 **Pluggable Drivers** - Built-in support for `local`, `memory`, and `null` drivers, with easy `custom` driver implementation.
17
+ - 🪝 **Powerful Hooks** - Intercept and modify storage operations (upload, delete, etc.) using Gravito's async Hook system.
18
+ - 🏢 **Enterprise Ready** - Automatic service registration in the IoC container and context-aware middleware.
19
+ - 🧪 **Test Friendly** - Includes a `MemoryStore` for fast, zero-side-effect unit testing.
20
+ - 🚀 **Modern** - Built for **Bun** with native TypeScript support.
6
21
 
7
22
  ## 📦 Installation
8
23
 
@@ -12,14 +27,15 @@ bun add @gravito/nebula
12
27
 
13
28
  ## 🚀 Quick Start
14
29
 
15
- ### Basic Usage
30
+ ### 1. Initialize with PlanetCore
16
31
 
17
32
  ```typescript
18
- import { PlanetCore } from '@gravito/core'
19
- import orbitStorage from '@gravito/nebula'
33
+ import { PlanetCore } from '@gravito/core';
34
+ import orbitStorage from '@gravito/nebula';
20
35
 
21
- const core = new PlanetCore()
36
+ const core = new PlanetCore();
22
37
 
38
+ // Quick install with options
23
39
  const storage = orbitStorage(core, {
24
40
  default: 'local',
25
41
  disks: {
@@ -29,326 +45,131 @@ const storage = orbitStorage(core, {
29
45
  baseUrl: '/uploads'
30
46
  }
31
47
  }
32
- })
48
+ });
49
+ ```
33
50
 
34
- // Use in routes
51
+ ### 2. Use in Routes (Middleware)
52
+
53
+ Nebula automatically injects the `storage` manager into the request context:
54
+
55
+ ```typescript
35
56
  core.app.post('/upload', async (c) => {
36
- const body = await c.req.parseBody()
37
- const file = body['file']
57
+ const body = await c.req.parseBody();
58
+ const file = body['file'];
38
59
 
39
60
  if (file instanceof File) {
40
- const storage = c.get('storage')
41
- await storage.put(file.name, file)
42
- return c.json({ url: storage.getUrl(file.name) })
61
+ const storage = c.get('storage'); // Resolved from context
62
+ await storage.put(`avatars/${file.name}`, file);
63
+
64
+ return c.json({
65
+ success: true,
66
+ url: storage.getUrl(`avatars/${file.name}`)
67
+ });
43
68
  }
44
69
 
45
- return c.text('No file uploaded', 400)
46
- })
70
+ return c.text('No file uploaded', 400);
71
+ });
47
72
  ```
48
73
 
49
- ---
50
-
51
74
  ## 🔧 Multi-Disk Configuration
52
75
 
76
+ You can define multiple disks and switch between them at runtime:
77
+
53
78
  ```typescript
54
79
  const storage = orbitStorage(core, {
55
80
  default: 'local',
56
81
  disks: {
57
- // Local disk
58
- local: {
59
- driver: 'local',
60
- root: './uploads',
61
- baseUrl: '/uploads'
62
- },
63
-
64
- // Memory disk (for testing)
65
- temp: {
66
- driver: 'memory'
67
- },
68
-
69
- // Null disk (no-op)
70
- null: {
71
- driver: 'null'
72
- },
73
-
74
- // Custom disk (e.g., S3)
82
+ local: { driver: 'local', root: './uploads', baseUrl: '/uploads' },
83
+ temp: { driver: 'memory' },
75
84
  s3: {
76
85
  driver: 'custom',
77
- store: new S3Store({
78
- bucket: 'my-bucket',
79
- region: 'us-east-1'
80
- })
86
+ store: new S3Store({ bucket: 'my-bucket' })
81
87
  }
82
88
  }
83
- })
89
+ });
84
90
 
85
- // Use default disk
86
- await storage.put('file.txt', 'content')
91
+ // Uses 'local' (default)
92
+ await storage.put('hello.txt', 'world');
87
93
 
88
- // Use specific disk
89
- await storage.disk('s3').put('important.pdf', pdfData)
90
- await storage.disk('temp').put('cache.json', jsonData)
94
+ // Uses 's3' disk explicitly
95
+ await storage.disk('s3').put('backup.zip', data);
91
96
  ```
92
97
 
93
- ---
94
-
95
98
  ## 📖 API Reference
96
99
 
97
- ### StorageManager
98
-
99
- The main storage manager returned by `orbitStorage()` or `c.get('storage')`.
100
-
101
- #### Methods
102
-
103
- ##### `disk(name?: string): StorageRepository`
104
-
105
- Get a specific disk repository. If `name` is not provided, returns the default disk.
106
-
107
- ```typescript
108
- const local = storage.disk('local')
109
- const s3 = storage.disk('s3')
110
- ```
111
-
112
- ##### `put(key: string, data: Blob | Buffer | string): Promise<void>`
113
-
114
- Store a file (using default disk).
115
-
116
- ```typescript
117
- await storage.put('file.txt', 'Hello World')
118
- await storage.put('image.png', imageBlob)
119
- ```
120
-
121
- ##### `get(key: string): Promise<Blob | null>`
122
-
123
- Retrieve a file (using default disk).
124
-
125
- ```typescript
126
- const data = await storage.get('file.txt')
127
- if (data) {
128
- console.log(await data.text())
129
- }
130
- ```
131
-
132
- ##### `delete(key: string): Promise<boolean>`
133
-
134
- Delete a file (using default disk). Returns `true` if deleted, `false` if file didn't exist.
135
-
136
- ```typescript
137
- const deleted = await storage.delete('old-file.txt')
138
- ```
139
-
140
- ##### `exists(key: string): Promise<boolean>` 🆕
141
-
142
- Check if a file exists (using default disk).
143
-
144
- ```typescript
145
- if (await storage.exists('config.json')) {
146
- // File exists
147
- }
148
- ```
149
-
150
- ##### `copy(from: string, to: string): Promise<void>` 🆕
151
-
152
- Copy a file (using default disk).
153
-
154
- ```typescript
155
- await storage.copy('original.txt', 'backup.txt')
156
- ```
157
-
158
- ##### `move(from: string, to: string): Promise<void>` 🆕
159
-
160
- Move/rename a file (using default disk).
161
-
162
- ```typescript
163
- await storage.move('temp.txt', 'final.txt')
164
- ```
165
-
166
- ##### `getMetadata(key: string): Promise<StorageMetadata | null>` 🆕
167
-
168
- Get file metadata (using default disk).
100
+ ### `StorageManager`
169
101
 
170
- ```typescript
171
- const meta = await storage.getMetadata('file.pdf')
172
- if (meta) {
173
- console.log(meta.size, meta.mimeType, meta.lastModified)
174
- }
175
- ```
176
-
177
- ##### `getUrl(key: string): string`
178
-
179
- Get the public URL for a file (using default disk).
180
-
181
- ```typescript
182
- const url = storage.getUrl('avatar.jpg')
183
- // "/uploads/avatar.jpg"
184
- ```
102
+ The central hub accessed via `c.get('storage')` or returned by `orbitStorage()`.
185
103
 
186
- ##### `getSignedUrl(key: string, expiresIn: number): Promise<string>` 🆕
104
+ - **`disk(name?: string)`**: Access a specific disk repository.
105
+ - **`put(key, data)`**: Store content (Default disk).
106
+ - **`get(key)`**: Retrieve content as a `Blob` (Default disk).
107
+ - **`delete(key)`**: Remove a file (Default disk).
108
+ - **`exists(key)`**: Check file existence (Default disk).
109
+ - **`copy(from, to)`**: Copy a file (Default disk).
110
+ - **`move(from, to)`**: Move/Rename a file (Default disk).
111
+ - **`getUrl(key)`**: Get public URL (Default disk).
112
+ - **`getSignedUrl(key, expires)`**: Get temporary signed URL (Default disk).
113
+ - **`getMetadata(key)`**: Get file metadata (size, mimeType, etc.).
114
+ - **`list(prefix?)`**: List files as an async iterable.
187
115
 
188
- Get a signed URL with expiration (if supported by the driver).
189
-
190
- ```typescript
191
- // Generate a URL that expires in 1 hour
192
- const signedUrl = await storage.disk('s3').getSignedUrl('private.pdf', 3600)
193
- ```
194
-
195
- ##### `list(prefix?: string): AsyncIterable<StorageItem>` 🆕
196
-
197
- List files in a directory (if supported by the driver).
198
-
199
- ```typescript
200
- for await (const item of storage.list('uploads/')) {
201
- console.log(item.key, item.size, item.lastModified)
202
- }
203
- ```
116
+ ### `StorageRepository`
204
117
 
205
- ---
118
+ Returned by `storage.disk('name')`. Implements the same storage methods as above but scoped to that specific disk.
206
119
 
207
120
  ## 🪝 Hooks
208
121
 
209
- Nebula integrates with Gravito's hook system for extensibility.
122
+ Nebula triggers various hooks during its lifecycle:
210
123
 
211
- | Hook | Type | Parameters | Description |
212
- |------|------|------------|-------------|
213
- | `storage:init` | Action | `{ manager: StorageManager }` | Fired when storage is initialized |
214
- | `storage:upload` | Filter | `data: Blob/Buffer/string, { key: string }` | Modify data before upload |
215
- | `storage:uploaded` | Action | `{ key: string }` | Triggered after successful upload |
216
- | `storage:hit` | Action | `{ key: string }` | File retrieved successfully |
217
- | `storage:miss` | Action | `{ key: string }` | File not found |
218
- | `storage:deleted` | Action | `{ key: string }` | File deleted |
219
- | `storage:copied` 🆕 | Action | `{ from: string, to: string }` | File copied |
220
- | `storage:moved` 🆕 | Action | `{ from: string, to: string }` | File moved |
124
+ | Hook | Type | Context | Description |
125
+ |------|------|---------|-------------|
126
+ | `storage:init` | Action | `{ manager }` | Fired on initialization |
127
+ | `storage:upload` | Filter | `data, { key }` | Modify data before it's saved |
128
+ | `storage:uploaded`| Action | `{ key }` | Fired after successful save |
129
+ | `storage:hit` | Action | `{ key }` | Fired when file is found/retrieved |
130
+ | `storage:miss` | Action | `{ key }` | Fired when file is not found |
131
+ | `storage:deleted` | Action | `{ key }` | Fired after file deletion |
221
132
 
222
- ### Example: Auto-resize Images on Upload
133
+ ### Example: Image Resizing
223
134
 
224
135
  ```typescript
225
136
  core.hooks.addFilter('storage:upload', async (data, context) => {
226
- if (context.key.endsWith('.jpg') || context.key.endsWith('.png')) {
227
- // Resize image using sharp, etc.
228
- return await resizeImage(data, { width: 1920 })
137
+ if (context.key.match(/\.(jpg|png)$/)) {
138
+ return await myImageProcessor.resize(data, 800);
229
139
  }
230
- return data
231
- })
232
- ```
233
-
234
- ### Example: Log All Uploads
235
-
236
- ```typescript
237
- core.hooks.addAction('storage:uploaded', async (context) => {
238
- core.logger.info(`File uploaded: ${context.key}`)
239
- })
140
+ return data;
141
+ });
240
142
  ```
241
143
 
242
- ---
144
+ ## 🔌 Custom Drivers
243
145
 
244
- ## 🔌 Custom Storage Drivers
245
-
246
- Implement the `StorageStore` interface to create custom drivers.
146
+ Implement the `StorageStore` interface to create your own backend:
247
147
 
248
148
  ```typescript
249
- import type { StorageStore, StorageMetadata } from '@gravito/nebula'
250
-
251
- class S3Store implements StorageStore {
252
- async put(key: string, data: Blob | Buffer | string): Promise<void> {
253
- // Upload to S3
254
- }
255
-
256
- async get(key: string): Promise<Blob | null> {
257
- // Download from S3
258
- }
259
-
260
- async delete(key: string): Promise<boolean> {
261
- // Delete from S3
262
- }
263
-
264
- async exists(key: string): Promise<boolean> {
265
- // Check if exists in S3
266
- }
149
+ import type { StorageStore, StorageMetadata } from '@gravito/nebula';
267
150
 
268
- async copy(from: string, to: string): Promise<void> {
269
- // S3 copy operation
270
- }
271
-
272
- async move(from: string, to: string): Promise<void> {
273
- await this.copy(from, to)
274
- await this.delete(from)
275
- }
276
-
277
- async getMetadata(key: string): Promise<StorageMetadata | null> {
278
- // Get S3 object metadata
279
- }
280
-
281
- getUrl(key: string): string {
282
- return `https://my-bucket.s3.amazonaws.com/${key}`
283
- }
284
-
285
- async getSignedUrl(key: string, expiresIn: number): Promise<string> {
286
- // Generate pre-signed URL
287
- }
151
+ class MyCustomStore implements StorageStore {
152
+ async put(key: string, data: Blob | Buffer | string): Promise<void> { /* ... */ }
153
+ async get(key: string): Promise<Blob | null> { /* ... */ }
154
+ async delete(key: string): Promise<boolean> { /* ... */ }
155
+ async exists(key: string): Promise<boolean> { /* ... */ }
156
+ getUrl(key: string): string { /* ... */ }
157
+ // ... implement other methods
288
158
  }
289
-
290
- // Use it
291
- const storage = orbitStorage(core, {
292
- disks: {
293
- s3: {
294
- driver: 'custom',
295
- store: new S3Store({ bucket: 'my-bucket' })
296
- }
297
- }
298
- })
299
159
  ```
300
160
 
301
- ---
302
-
303
161
  ## 🔄 Migration from v3.x
304
162
 
305
- ### Configuration Changes
306
-
307
- ```typescript
308
- // v3.x (Old)
309
- orbitStorage(core, {
310
- local: { root: './uploads', baseUrl: '/uploads' },
311
- exposeAs: 'storage'
312
- })
313
-
314
- // v4.0 (New)
315
- orbitStorage(core, {
316
- default: 'local',
317
- disks: {
318
- local: { driver: 'local', root: './uploads', baseUrl: '/uploads' }
319
- },
320
- exposeAs: 'storage'
321
- })
322
- ```
323
-
324
- **Note**: The old format is still supported for backward compatibility but is deprecated.
325
-
326
- ### Return Value Changes
327
-
328
- ```typescript
329
- // v3.x - Returns wrapped provider
330
- const storage = orbitStorage(core, { ... })
331
- await storage.put('file.txt', data)
332
-
333
- // v4.0 - Returns StorageManager
334
- const storage = orbitStorage(core, { ... })
335
- await storage.put('file.txt', data) // Same API!
336
- await storage.disk('s3').put('file.txt', data) // New!
337
- ```
338
-
339
- The API is backward compatible, but v4.0 adds multi-disk support via `disk()`.
340
-
341
- ### Type Changes
163
+ Nebula v4.0 introduces the **Manager** pattern for multi-disk support.
342
164
 
343
- | v3.x | v4.0 |
344
- |------|------|
345
- | `StorageProvider` | `StorageStore` |
346
- | `LocalStorageProvider` | `LocalStore` |
347
- | `OrbitStorageOptions` | `OrbitNebulaOptions` |
165
+ - **Breaking**: The configuration structure has moved under a `disks` property.
166
+ - **Compatibility**: The old flat configuration format is still supported but will trigger a deprecation warning.
167
+ - **Types**: `StorageProvider` is now `StorageStore`, and `LocalStorageProvider` is now `LocalStore`.
348
168
 
349
- Old type names are still exported with `@deprecated` warnings.
169
+ ## 🤝 Contributing
350
170
 
351
- ---
171
+ Contributions, issues and feature requests are welcome!
172
+ Feel free to check the [issues page](https://github.com/gravito-framework/gravito/issues).
352
173
 
353
174
  ## 📝 License
354
175
 
package/README.zh-TW.md CHANGED
@@ -1,37 +1,176 @@
1
1
  # @gravito/nebula
2
2
 
3
- > Gravito 的標準儲存 Orbit,提供檔案儲存抽象層。
3
+ > Galaxy 架構的標準儲存 Orbit。輕量、多磁碟支援且可插拔。
4
4
 
5
- ## 安裝
5
+ [![npm version](https://img.shields.io/npm/v/@gravito/nebula.svg)](https://www.npmjs.com/package/@gravito/nebula)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
+ [![Bun](https://img.shields.io/badge/Bun-1.0+-black.svg)](https://bun.sh/)
9
+
10
+ **@gravito/nebula** 為 Gravito 應用程式提供統一的檔案儲存抽象層。基於 **Orbit** 模式,它充當核心微核心與各種儲存後端(本地、S3、記憶體等)之間的橋樑,支援多磁碟配置並透過 Hook 系統提供高度可擴展性。
11
+
12
+ ## ✨ 特性
13
+
14
+ - 🪐 **Orbit 整合** - 無縫插入 PlanetCore 微核心。
15
+ - 💽 **多磁碟支援** - 在單個應用程式中管理多個儲存後端(磁碟)。
16
+ - 🔌 **可插拔驅動** - 內建支援 `local`、`memory` 與 `null` 驅動,並支援輕鬆實作 `custom` 驅動。
17
+ - 🪝 **強大的 Hooks** - 使用 Gravito 的異步 Hook 系統攔截並修改儲存操作(上傳、刪除等)。
18
+ - 🏢 **企業級設計** - 在 IoC 容器中自動註冊服務,並提供 Context 感知的中間件。
19
+ - 🧪 **測試友善** - 包含 `MemoryStore`,用於快速、無副作用的單元測試。
20
+ - 🚀 **現代化** - 專為 **Bun** 構建,原生支援 TypeScript。
21
+
22
+ ## 📦 安裝
6
23
 
7
24
  ```bash
8
25
  bun add @gravito/nebula
9
26
  ```
10
27
 
11
- ## 快速開始
28
+ ## 🚀 快速上手
29
+
30
+ ### 1. 初始化與 PlanetCore 整合
12
31
 
13
32
  ```typescript
14
- import { PlanetCore } from '@gravito/core'
15
- import orbitStorage from '@gravito/nebula'
33
+ import { PlanetCore } from '@gravito/core';
34
+ import orbitStorage from '@gravito/nebula';
16
35
 
17
- const core = new PlanetCore()
36
+ const core = new PlanetCore();
18
37
 
38
+ // 使用選項快速安裝
19
39
  const storage = orbitStorage(core, {
20
- local: {
21
- root: './uploads',
22
- baseUrl: '/uploads'
23
- },
24
- exposeAs: 'storage'
25
- })
40
+ default: 'local',
41
+ disks: {
42
+ local: {
43
+ driver: 'local',
44
+ root: './uploads',
45
+ baseUrl: '/uploads'
46
+ }
47
+ }
48
+ });
49
+ ```
26
50
 
27
- core.app.post('/upload', async (c) => {
28
- const body = await c.req.parseBody()
29
- const file = body['file']
51
+ ### 2. 在路由中使用(中間件)
52
+
53
+ Nebula 會自動將 `storage` 管理器注入請求上下文(Context):
30
54
 
55
+ ```typescript
56
+ core.app.post('/upload', async (c) => {
57
+ const body = await c.req.parseBody();
58
+ const file = body['file'];
59
+
31
60
  if (file instanceof File) {
32
- await storage.put(file.name, file)
33
- return c.json({ url: storage.getUrl(file.name) })
61
+ const storage = c.get('storage'); // 從上下文中解析
62
+ await storage.put(`avatars/${file.name}`, file);
63
+
64
+ return c.json({
65
+ success: true,
66
+ url: storage.getUrl(`avatars/${file.name}`)
67
+ });
68
+ }
69
+
70
+ return c.text('No file uploaded', 400);
71
+ });
72
+ ```
73
+
74
+ ## 🔧 多磁碟配置
75
+
76
+ 您可以定義多個磁碟並在運行時切換:
77
+
78
+ ```typescript
79
+ const storage = orbitStorage(core, {
80
+ default: 'local',
81
+ disks: {
82
+ local: { driver: 'local', root: './uploads', baseUrl: '/uploads' },
83
+ temp: { driver: 'memory' },
84
+ s3: {
85
+ driver: 'custom',
86
+ store: new S3Store({ bucket: 'my-bucket' })
87
+ }
34
88
  }
35
- return c.text('No file uploaded', 400)
36
- })
89
+ });
90
+
91
+ // 使用 'local' (預設)
92
+ await storage.put('hello.txt', 'world');
93
+
94
+ // 明確使用 's3' 磁碟
95
+ await storage.disk('s3').put('backup.zip', data);
37
96
  ```
97
+
98
+ ## 📖 API 參考
99
+
100
+ ### `StorageManager`
101
+
102
+ 透過 `c.get('storage')` 存取或由 `orbitStorage()` 回傳的中央管理器。
103
+
104
+ - **`disk(name?: string)`**: 存取特定的磁碟倉庫。
105
+ - **`put(key, data)`**: 儲存內容(預設磁碟)。
106
+ - **`get(key)`**: 取得內容為 `Blob`(預設磁碟)。
107
+ - **`delete(key)`**: 刪除檔案(預設磁碟)。
108
+ - **`exists(key)`**: 檢查檔案是否存在(預設磁碟)。
109
+ - **`copy(from, to)`**: 複製檔案(預設磁碟)。
110
+ - **`move(from, to)`**: 移動/重新命名檔案(預設磁碟)。
111
+ - **`getUrl(key)`**: 取得公開 URL(預設磁碟)。
112
+ - **`getSignedUrl(key, expires)`**: 取得臨時簽名 URL(預設磁碟)。
113
+ - **`getMetadata(key)`**: 取得檔案元數據(大小、MIME 類型等)。
114
+ - **`list(prefix?)`**: 列出檔案(異步叠代器)。
115
+
116
+ ### `StorageRepository`
117
+
118
+ 由 `storage.disk('name')` 回傳。實作與上述相同的儲存方法,但範圍限定於該特定磁碟。
119
+
120
+ ## 🪝 Hooks
121
+
122
+ Nebula 在其生命週期中觸發各種 Hook:
123
+
124
+ | Hook | 類型 | 上下文 | 描述 |
125
+ |------|------|---------|-------------|
126
+ | `storage:init` | Action | `{ manager }` | 初始化時觸發 |
127
+ | `storage:upload` | Filter | `data, { key }` | 在儲存前修改資料 |
128
+ | `storage:uploaded`| Action | `{ key }` | 儲存成功後觸發 |
129
+ | `storage:hit` | Action | `{ key }` | 找到/取得檔案時觸發 |
130
+ | `storage:miss` | Action | `{ key }` | 找不到檔案時觸發 |
131
+ | `storage:deleted` | Action | `{ key }` | 檔案刪除後觸發 |
132
+
133
+ ### 範例:圖片縮放
134
+
135
+ ```typescript
136
+ core.hooks.addFilter('storage:upload', async (data, context) => {
137
+ if (context.key.match(/\.(jpg|png)$/)) {
138
+ return await myImageProcessor.resize(data, 800);
139
+ }
140
+ return data;
141
+ });
142
+ ```
143
+
144
+ ## 🔌 自定義驅動
145
+
146
+ 實作 `StorageStore` 介面來建立您自己的後端:
147
+
148
+ ```typescript
149
+ import type { StorageStore, StorageMetadata } from '@gravito/nebula';
150
+
151
+ class MyCustomStore implements StorageStore {
152
+ async put(key: string, data: Blob | Buffer | string): Promise<void> { /* ... */ }
153
+ async get(key: string): Promise<Blob | null> { /* ... */ }
154
+ async delete(key: string): Promise<boolean> { /* ... */ }
155
+ async exists(key: string): Promise<boolean> { /* ... */ }
156
+ getUrl(key: string): string { /* ... */ }
157
+ // ... 實作其他方法
158
+ }
159
+ ```
160
+
161
+ ## 🔄 從 v3.x 遷移
162
+
163
+ Nebula v4.0 引入了 **Manager** 模式以支援多磁碟。
164
+
165
+ - **破壞性變更**:配置結構已移至 `disks` 屬性下。
166
+ - **相容性**:舊的扁平配置格式仍然支援,但會觸發棄用警告。
167
+ - **類型變更**:`StorageProvider` 現已更名為 `StorageStore`,`LocalStorageProvider` 更名為 `LocalStore`。
168
+
169
+ ## 🤝 貢獻
170
+
171
+ 歡迎提交貢獻、問題與功能請求!
172
+ 請隨時查看 [Issues 頁面](https://github.com/gravito-framework/gravito/issues)。
173
+
174
+ ## 📝 授權
175
+
176
+ MIT © [Carl Lee](https://github.com/gravito-framework/gravito)