@h-ai/storage 0.1.0-alpha9 → 1.0.0-beta.2
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.en.md +287 -0
- package/README.ja.md +287 -0
- package/README.md +235 -59
- package/dist/browser.d.ts +491 -102
- package/dist/browser.js +3 -2
- package/dist/{chunk-KHZOR6CR.js → chunk-ANAESRWS.js} +39 -12
- package/dist/chunk-ANAESRWS.js.map +1 -0
- package/dist/chunk-P65WYXJG.js +33 -0
- package/dist/chunk-P65WYXJG.js.map +1 -0
- package/dist/chunk-TLZY4IP6.js +360 -0
- package/dist/chunk-TLZY4IP6.js.map +1 -0
- package/dist/client/index.d.ts +57 -48
- package/dist/client/index.js +1 -1
- package/dist/node.d.ts +15 -11
- package/dist/node.js +597 -180
- package/dist/node.js.map +1 -1
- package/dist/schemas.d.ts +103 -0
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -0
- package/package.json +22 -16
- package/dist/api/index.d.ts +0 -167
- package/dist/api/index.js +0 -105
- package/dist/api/index.js.map +0 -1
- package/dist/chunk-KHZOR6CR.js.map +0 -1
- package/dist/chunk-U427BQIQ.js +0 -266
- package/dist/chunk-U427BQIQ.js.map +0 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @h-ai/storage
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
|
|
6
|
+
|
|
7
|
+
## Introduction
|
|
8
|
+
|
|
9
|
+
An object storage module with a unified `storage` entry point for S3-compatible storage and local filesystems.
|
|
10
|
+
|
|
11
|
+
Connections become available only after a successful health check. Failed initialization disposes its candidate; `error.ext.cleanupPending === true` identifies cleanup that still needs retry. `storage.close()` returns `Promise<HaiResult<void>>`, waits for initialization, and coalesces concurrent close calls. Once cleanup starts, `isInitialized` is false and configuration/operations are unavailable. A failed cleanup retains the resource: the next `close()` or `init()` retries disposal before allocating a replacement. Always inspect the close result.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const closed = await storage.close()
|
|
15
|
+
if (!closed.success) {
|
|
16
|
+
// Retry once after addressing the cleanup failure; report another failure.
|
|
17
|
+
const retried = await storage.close()
|
|
18
|
+
if (!retried.success)
|
|
19
|
+
throw new Error(retried.error.message)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
S3 disposes its normal client and its separate batch-delete client (SDK request retries disabled) independently. One failure does not prevent the other cleanup; successfully disposed clients are not destroyed again.
|
|
24
|
+
|
|
25
|
+
## Core features
|
|
26
|
+
|
|
27
|
+
- S3 protocol (AWS S3 / MinIO / RustFS / OSS, etc.)
|
|
28
|
+
- Local filesystem
|
|
29
|
+
|
|
30
|
+
## Getting started
|
|
31
|
+
|
|
32
|
+
### Quick start
|
|
33
|
+
|
|
34
|
+
#### Node.js server
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { storage } from '@h-ai/storage'
|
|
38
|
+
|
|
39
|
+
// 初始化(S3)
|
|
40
|
+
await storage.init({
|
|
41
|
+
type: 's3',
|
|
42
|
+
bucket: 'my-bucket',
|
|
43
|
+
region: 'us-east-1',
|
|
44
|
+
accessKeyId: process.env.HAI_STORAGE_ACCESSKEYID!,
|
|
45
|
+
secretAccessKey: process.env.HAI_STORAGE_SECRETACCESSKEY!,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 或本地存储
|
|
49
|
+
await storage.init({ type: 'local', root: '/data/uploads' })
|
|
50
|
+
|
|
51
|
+
// 上传/下载
|
|
52
|
+
await storage.file.put('uploads/image.png', imageBuffer, { contentType: 'image/png' })
|
|
53
|
+
const file = await storage.file.get('uploads/image.png')
|
|
54
|
+
|
|
55
|
+
// 签名 URL
|
|
56
|
+
const url = await storage.presign.getUrl('uploads/image.png', { expiresIn: 3600 })
|
|
57
|
+
if (!url.success) {
|
|
58
|
+
// local 不支持预签名;呈现错误或使用应用已有的鉴权文件接口。
|
|
59
|
+
// 失败结果不包含上传/下载 URL。
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 关闭连接
|
|
63
|
+
const closeResult = await storage.close()
|
|
64
|
+
if (!closeResult.success) {
|
|
65
|
+
throw new Error(closeResult.error.message)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Browser client
|
|
70
|
+
|
|
71
|
+
The browser entry exports only client capabilities (signed URL uploads/downloads), without the `storage` service object.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { apiClient } from '@h-ai/api-client'
|
|
75
|
+
import { downloadAndSave, uploadWithPresignedUrl } from '@h-ai/storage/client'
|
|
76
|
+
|
|
77
|
+
// apiClient 已初始化,服务端使用 S3。
|
|
78
|
+
const signed = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
|
|
79
|
+
if (!signed.success)
|
|
80
|
+
throw new Error(signed.error.message)
|
|
81
|
+
await uploadWithPresignedUrl(signed.data.url, file)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> Note: `@h-ai/storage` does not include HTTP routes. Implement the signed URL API in the application layer.
|
|
85
|
+
|
|
86
|
+
### HTTP API contracts
|
|
87
|
+
|
|
88
|
+
Public HTTP contracts come from `@h-ai/api-contract`; `createStorageProcedures({ storage, authorize })` in `@h-ai/serv` binds them to this module.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { apiClient } from '@h-ai/api-client'
|
|
92
|
+
|
|
93
|
+
const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
|
|
94
|
+
if (!result.success)
|
|
95
|
+
throw new Error(result.error.message)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Presigning requires S3. Local `getUrl()` / `putUrl()` return `HaiStorageError.UNSUPPORTED_OPERATION` (`hai:storage:018`), and `publicUrl()` returns `null`; no virtual URL is returned. serv and apiClient preserve the failure, so check `success` before consuming a URL. Use the application's existing authenticated file endpoints for local development; this module includes no HTTP file route or signature-verification service.
|
|
99
|
+
|
|
100
|
+
### API overview
|
|
101
|
+
|
|
102
|
+
- `storage.file`
|
|
103
|
+
- `put/get/getStream/head/exists/delete/deleteMany/copy`
|
|
104
|
+
- `storage.dir`
|
|
105
|
+
- `list/delete`
|
|
106
|
+
- `storage.presign`
|
|
107
|
+
- `getUrl/putUrl/publicUrl`
|
|
108
|
+
- `storage.checksum`
|
|
109
|
+
- `generate(data, options?) / verify(data, expected, options?)`
|
|
110
|
+
|
|
111
|
+
`file.put()` accepts Buffer, Uint8Array, strings and `AsyncIterable<Uint8Array>` (including Node Readable), with bounded S3 multipart uploads for streams. `getStream()` returns a byte stream with Range and `signal` cancellation. Initial failures return HaiResult; consumers handle I/O/cancellation errors during iteration. `get()` still buffers the complete file and is intended for small files.
|
|
112
|
+
|
|
113
|
+
`{ ifNoneMatch: '*' }` uses standard S3 conditional writes or atomic local publication to reject overwrites with `ALREADY_EXISTS`. Unsupported S3 conditions fail without falling back to an overwrite. Failed/cancelled uploads clean temporary files or unfinished parts; cancellation cannot undo an already published object.
|
|
114
|
+
|
|
115
|
+
`file.deleteMany(keys)` returns `StorageDeleteResult`. Success data is `{ deletedKeys, failures: [] }`; any unconfirmed deletion returns `OPERATION_FAILED` with progress in `error.ext?.deleteMany` (precondition errors such as uninitialized storage may omit progress). Duplicate inputs are processed once, and returned keys exclude the configured prefix. S3 batches contain at most 1000 keys and verify every `Deleted`/`Errors` receipt. HTTP 200 alone is insufficient; missing, duplicate or unknown receipts produce `unknown` outcomes. Local deletion uses at most 32 concurrent operations.
|
|
116
|
+
|
|
117
|
+
Only explicit `InternalError / ServiceUnavailable / SlowDown / RequestTimeout` items receive up to three attempts. Confirmed successes and permission failures are not resent. A dedicated `DeleteObjects` client disables SDK request retries. Network interruption immediately produces `unknown`; later unsent batches are `notAttempted`, without replaying unknown outcomes. `retryable` describes the fault category; callers must reconcile unknown results and concurrent writes before retrying. `dir.delete()` propagates failures but cannot prevent concurrent new objects. S3 deletes omit VersionId and may create delete markers in versioned buckets; historical versions are not purged.
|
|
118
|
+
|
|
119
|
+
`@h-ai/serv` / `@h-ai/api-client` expose the same success data and failure progress through `storage.files.deleteMany`.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const result = await storage.file.deleteMany(['a.txt', 'b.txt'])
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
const progress = result.error.ext?.deleteMany
|
|
125
|
+
// Reconcile unknown outcomes with application writes before compensating.
|
|
126
|
+
const knownFailures = progress?.failures.filter(item => item.outcome === 'failed' && item.retryable) ?? []
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`checksum` is a stateless Node tool requiring no initialization. It generates/verifies the actual input bytes and consumes streams once. Defaults: SHA-256, lowercase hex. Options: `algorithm: 'sha256' | 'sha384' | 'sha512' | 'sha1' | 'md5'`, `encoding: 'hex' | 'base64'`, and `signal`. Mismatches return `ok(false)`; invalid format/algorithm and read failures return errors. MD5/SHA-1 are for compatibility integrity checks, not security authentication. ETags and S3 multipart composite checksums are not whole-file digests. The tool does not require compatible services to implement AWS checksum extensions.
|
|
131
|
+
|
|
132
|
+
Cancelling a checksum stops waiting for the next chunk and requests closure of the input iterator. Local `getStream()` rejects directories with `INVALID_PATH` before returning a stream.
|
|
133
|
+
|
|
134
|
+
> Note: `publicUrl` returns a string only when S3's `publicUrl` is configured; otherwise it returns `null`.
|
|
135
|
+
|
|
136
|
+
> Note: `storage.config` returns a **redacted configuration snapshot**. S3 secret fields are replaced with `[REDACTED]`, so it cannot be used for reinitialization.
|
|
137
|
+
|
|
138
|
+
### Client helpers
|
|
139
|
+
|
|
140
|
+
`@h-ai/storage/client` also provides these browser utilities:
|
|
141
|
+
|
|
142
|
+
- `uploadWithPresignedUrl(url, data, options)`
|
|
143
|
+
- `downloadWithPresignedUrl(url, options)`
|
|
144
|
+
- `downloadAndSave(url, { filename })`
|
|
145
|
+
- `getFileExtension(file)`
|
|
146
|
+
- `getMimeType(extension)`
|
|
147
|
+
- `formatFileSize(bytes)`
|
|
148
|
+
|
|
149
|
+
### Configuration
|
|
150
|
+
|
|
151
|
+
- **S3**: requires `bucket / region / accessKeyId / secretAccessKey`; optional `createBucket / endpoint / publicEndpoint / forcePathStyle / prefix / publicUrl`.
|
|
152
|
+
- **Local**: requires `root`; optional `directoryMode / fileMode`.
|
|
153
|
+
|
|
154
|
+
MinIO and RustFS share the S3 provider with `type: 's3'`; no separate storage type is needed. For self-hosted instances, set the S3 API endpoint (usually port 9000) and enable path-style access.
|
|
155
|
+
|
|
156
|
+
`createBucket` defaults to `false`, requiring an existing bucket. When enabled, `storage.init()` checks `HeadBucket` and creates the bucket only on an explicit 404. Existing buckets and objects are preserved. Permission, network, creation, and subsequent connection-check failures return failure, dispose clients, and leave storage uninitialized. A concurrent `BucketAlreadyOwnedByYou` is accepted only after another access check. Creation omits LocationConstraint for AWS `us-east-1` and uses the configured region elsewhere. Default self-hosted S3 deployments enable `HAI_STORAGE_CREATEBUCKET=true`; external S3 creation requires explicit permission.
|
|
157
|
+
|
|
158
|
+
Use `createBucket: ${HAI_STORAGE_CREATEBUCKET:false}` in YAML for environment control; direct `storage.init()` input requires a boolean.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const initResult = await storage.init({
|
|
162
|
+
type: 's3',
|
|
163
|
+
endpoint: 'http://localhost:9000',
|
|
164
|
+
bucket: 'my-bucket',
|
|
165
|
+
region: 'us-east-1',
|
|
166
|
+
accessKeyId: process.env.HAI_STORAGE_ACCESSKEYID!,
|
|
167
|
+
secretAccessKey: process.env.HAI_STORAGE_SECRETACCESSKEY!,
|
|
168
|
+
forcePathStyle: true,
|
|
169
|
+
createBucket: true,
|
|
170
|
+
})
|
|
171
|
+
if (!initResult.success) {
|
|
172
|
+
throw new Error(initResult.error.message)
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Internal connections and browser access
|
|
177
|
+
|
|
178
|
+
`endpoint` handles server-side CRUD, bucket creation and health checks. Optional `publicEndpoint` selects the external address for `presign.getUrl/putUrl`; omission preserves endpoint-based signing. It accepts a proxy directory such as `https://app.example.com/delivery/files`. Path segments allow letters, numbers, underscores, dots and hyphens, excluding dot segments, repeated slashes and encoded separators. Do not include the bucket, credentials, query or fragment. `publicUrl` constructs public-resource URLs without authorizing private objects.
|
|
179
|
+
|
|
180
|
+
Self-hosted Compose uses `http://storage:9000` internally. The example generates `/delivery/files/hai/private/report.zip?...`; an origin-only endpoint generates `/hai/private/report.zip?...`. A proxy directory selects path-style external signatures; internal operations still follow `forcePathStyle`. Nginx removes the directory and forwards the original S3 signature path, query and Host directly to Storage. Large downloads and Range bypass Serv. Fullstack reuses Web Nginx; SvelteKit SSR and independent APIs receive a gateway. Storage may sit under the website directory but cannot cover API, asset or health paths. Gateway/access changes require a new full baseline; partial updates and rollback retain installed settings.
|
|
181
|
+
|
|
182
|
+
External AWS S3, R2 and existing gateways use their own accessible endpoints; kept/external storage does not receive a local sidecar route. HTTPS ingress must preserve the signed Host, original URI and query. Cross-origin browsers require precise CORS at the actual S3 service. The gateway does not replace bucket authorization or CORS. Treat signed URLs as temporary credentials, limit expiry and exclude them from logs. A GET signature is not a HEAD signature. Local storage does not support presigning or expose private directories through Nginx.
|
|
183
|
+
|
|
184
|
+
For empty self-hosted bucket/region settings, deployment injects hai/us-east-1; these are not Storage Schema defaults. The object proxy applies CSP sandbox, nosniff, no-referrer and private,no-store to isolate untrusted attachments, including same-origin HTML. Use it for file delivery, not executable website scripts. Configure equivalent isolation at external S3/CDN endpoints.
|
|
185
|
+
|
|
186
|
+
#### Operation logging
|
|
187
|
+
|
|
188
|
+
Enable file, directory, and presigned URL operation logs with `operationLog`. Logs are emitted at the actual Local/S3 provider operations, without a shared wrapper in `storage-main.ts`. File contents are never logged; uploads log only the byte length.
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
# config/_storage.yml
|
|
192
|
+
type: local
|
|
193
|
+
root: ./data/uploads
|
|
194
|
+
operationLog:
|
|
195
|
+
read: false # file.get/head/exists, dir.list, presign.getUrl/publicUrl
|
|
196
|
+
write: false # file.put/delete/deleteMany/copy, dir.delete, presign.putUrl
|
|
197
|
+
maxLength: 1000 # 参数序列化后的最大输出长度,超出会截断
|
|
198
|
+
level: debug # info | debug | trace,默认 debug
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
await storage.init({
|
|
203
|
+
type: 'local',
|
|
204
|
+
root: './data/uploads',
|
|
205
|
+
operationLog: {
|
|
206
|
+
read: true,
|
|
207
|
+
write: true,
|
|
208
|
+
maxLength: 500,
|
|
209
|
+
level: 'debug',
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`storage.presign.putUrl()` currently supports signing options such as `contentType` and `expiresIn`, but no `maxSize` constraint.
|
|
215
|
+
|
|
216
|
+
Local `storage.dir.list()` treats `prefix` as a key string prefix (for example, `reports/2026-`) and paginates in UTF-8 byte order. `maxKeys` is an integer from 1 to 1000, default 1000; files and common prefixes share that budget. `delimiter` folds keys at its first occurrence after the prefix into one `commonPrefixes` entry. An empty delimiter disables grouping; empty physical directories produce no entries. The grouping and counting rules follow [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html).
|
|
217
|
+
|
|
218
|
+
`isTruncated: true` and `nextContinuationToken` appear only when another entry exists. Local cursors use no process cache and bind the canonical root directory, prefix and delimiter. Independent instances or restarted processes using the same root can continue a page, with a different `maxKeys` if needed. Malformed, unsupported-version or mismatched-scope cursors return `INVALID_LIST_OPTIONS`. Cursors grant no authorization and provide no snapshot: authorize the prefix, expect concurrent changes to affect later pages, and use the same canonical mount path. Each local request still scans and sorts matching directories; narrow the prefix or use S3 for large directories.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
let continuationToken: string | undefined
|
|
222
|
+
do {
|
|
223
|
+
const page = await storage.dir.list({ prefix: 'reports/2026-', maxKeys: 100, continuationToken })
|
|
224
|
+
if (!page.success)
|
|
225
|
+
return page
|
|
226
|
+
// Consume page.data.files and page.data.commonPrefixes.
|
|
227
|
+
continuationToken = page.data.nextContinuationToken
|
|
228
|
+
} while (continuationToken)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Error handling
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { HaiStorageError, storage } from '@h-ai/storage'
|
|
235
|
+
|
|
236
|
+
const result = await storage.file.get('image.png')
|
|
237
|
+
if (!result.success && result.error.code === HaiStorageError.NOT_INITIALIZED.code) {
|
|
238
|
+
// 存储未初始化
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Common error codes:
|
|
243
|
+
|
|
244
|
+
| Error code | code | Description |
|
|
245
|
+
| --------------------------------------- | ----------------- | ---------------------------------------------------------------------- |
|
|
246
|
+
| `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | Not initialized |
|
|
247
|
+
| `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | Connection failed |
|
|
248
|
+
| `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | Operation failed |
|
|
249
|
+
| `HaiStorageError.NOT_FOUND` | `hai:storage:003` | File not found |
|
|
250
|
+
| `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | Permission denied |
|
|
251
|
+
| `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | Presigned URL generation failed |
|
|
252
|
+
| `HaiStorageError.UNSUPPORTED_OPERATION` | `hai:storage:018` | Operation unsupported by the provider, including local presigning |
|
|
253
|
+
| `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | Invalid local list options or cursor, including mismatched query scope |
|
|
254
|
+
| `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | Invalid configuration |
|
|
255
|
+
|
|
256
|
+
## Production shared state
|
|
257
|
+
|
|
258
|
+
Production requires shared S3 or a compatible service; local is for development/explicit tests. Applications own object authorization and conditional read-modify-write commits. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md)。
|
|
259
|
+
|
|
260
|
+
## Input limits and HTTP capabilities
|
|
261
|
+
|
|
262
|
+
Object keys are limited to 1024 UTF-8 bytes and reject absolute paths, backslashes, control characters and dot segments. Each public HTTP batch delete accepts 1–1000 entries. `@h-ai/storage/schemas` exposes the shared browser-safe rules. Presign expiry is an integer from 1 to 604800 seconds (Node default 3600, HTTP default 900). Upload signing accepts only key/contentType/expiresIn; contentLength/maxSize/metadata/checksum are unsupported. Invalid input returns HTTP 400 and the typed client maps it to VALIDATION_FAILED. FileUpload limits guide file selection; applications must validate actual objects on the server. Node streaming/checksum/conditional-write options have no default HTTP endpoints. See the [capability and authorization matrix (Chinese)](README.md#存储输入约束与跨端能力).
|
|
263
|
+
|
|
264
|
+
## Browser upload cancellation
|
|
265
|
+
|
|
266
|
+
`uploadWithPresignedUrl(url, data, { onProgress, abortController })` uses native XHR for progress and fetch when `onProgress` is omitted. An already aborted controller sends no request. Cancellation during setup or upload, external XHR abort, network/timeout errors, and progress callback exceptions settle once with a failure and remove this operation's listeners. Aborting after completion does not touch the old XHR. Cancellation only stops the local request; it cannot roll back an object already written remotely. Check the object state and authorize deletion separately.
|
|
267
|
+
|
|
268
|
+
## Development and testing
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
pnpm --filter @h-ai/storage test
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
S3 integration tests require Docker or a compatible runtime. The same initialization, file, directory, presigning, and real HTTP upload/download cases run against both MinIO and RustFS; container startup failures are not skipped.
|
|
275
|
+
|
|
276
|
+
- MinIO: [`ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z`](https://github.com/coollabsio/minio), a community image built from upstream source.
|
|
277
|
+
- RustFS: [`ghcr.io/rustfs/rustfs:1.0.1`](https://github.com/rustfs/rustfs), the official image.
|
|
278
|
+
|
|
279
|
+
Containers use temporary test credentials. Buckets are created through the S3 API after service readiness; containers and anonymous volumes are removed when the final lease is released.
|
|
280
|
+
|
|
281
|
+
Source JSDoc follows the [shared conventions](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证): public examples use published entry points, and internal examples are checked in their module scope. After building, run `pnpm check:jsdoc --package storage`; compiling examples does not replace backend behavior tests.
|
|
282
|
+
|
|
283
|
+
For the real Chrome regression gate, install Chrome, set `HAI_STORAGE_RUN_BROWSER_GATES=1`, then run `pnpm --filter @h-ai/storage exec vitest run tests/storage-client-browser.test.ts`. It sends actual HTTP PUT requests and checks native cancellation and listener cleanup; a missing browser fails the gate.
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
Apache-2.0
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @h-ai/storage
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/core](../core/README.ja.md)
|
|
6
|
+
|
|
7
|
+
## 概要
|
|
8
|
+
|
|
9
|
+
共通の `storage` 入口で S3 互換ストレージとローカルファイルシステムを操作するオブジェクトストレージモジュールです。
|
|
10
|
+
|
|
11
|
+
接続はヘルスチェック成功後に公開します。初期化失敗時は候補リソースを解放し、解放にも失敗した場合は初期化エラーの `error.ext.cleanupPending` が `true` になります。`storage.close()` は `Promise<HaiResult<void>>` を返し、初期化を待機して並行する終了処理をまとめます。解放開始後は `isInitialized` が `false` となり、設定と操作は利用できません。解放に失敗したリソースは保持され、次の `close()` または `init()` で再試行します。解放が成功するまで代替接続は作成しません。終了結果を必ず確認してください。
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const closed = await storage.close()
|
|
15
|
+
if (!closed.success) {
|
|
16
|
+
// 障害への対処後に一度再試行し、再失敗はアプリへ報告します。
|
|
17
|
+
const retried = await storage.close()
|
|
18
|
+
if (!retried.success)
|
|
19
|
+
throw new Error(retried.error.message)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
S3 の通常クライアントと、リクエスト再試行を無効化した一括削除クライアントは個別に解放します。一方の失敗でも他方は解放し、解放済みクライアントは再度破棄しません。
|
|
24
|
+
|
|
25
|
+
## 主な機能
|
|
26
|
+
|
|
27
|
+
- S3 プロトコル(AWS S3 / MinIO / RustFS / OSS など)
|
|
28
|
+
- ローカルファイルシステム
|
|
29
|
+
|
|
30
|
+
## 使い始める
|
|
31
|
+
|
|
32
|
+
### クイックスタート
|
|
33
|
+
|
|
34
|
+
#### Node.js サーバー
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { storage } from '@h-ai/storage'
|
|
38
|
+
|
|
39
|
+
// 初始化(S3)
|
|
40
|
+
await storage.init({
|
|
41
|
+
type: 's3',
|
|
42
|
+
bucket: 'my-bucket',
|
|
43
|
+
region: 'us-east-1',
|
|
44
|
+
accessKeyId: process.env.HAI_STORAGE_ACCESSKEYID!,
|
|
45
|
+
secretAccessKey: process.env.HAI_STORAGE_SECRETACCESSKEY!,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 或本地存储
|
|
49
|
+
await storage.init({ type: 'local', root: '/data/uploads' })
|
|
50
|
+
|
|
51
|
+
// 上传/下载
|
|
52
|
+
await storage.file.put('uploads/image.png', imageBuffer, { contentType: 'image/png' })
|
|
53
|
+
const file = await storage.file.get('uploads/image.png')
|
|
54
|
+
|
|
55
|
+
// 签名 URL
|
|
56
|
+
const url = await storage.presign.getUrl('uploads/image.png', { expiresIn: 3600 })
|
|
57
|
+
if (!url.success) {
|
|
58
|
+
// local 不支持预签名;呈现错误或使用应用已有的鉴权文件接口。
|
|
59
|
+
// 失败结果不包含上传/下载 URL。
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 关闭连接
|
|
63
|
+
const closeResult = await storage.close()
|
|
64
|
+
if (!closeResult.success) {
|
|
65
|
+
throw new Error(closeResult.error.message)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### ブラウザークライアント
|
|
70
|
+
|
|
71
|
+
ブラウザー環境ではクライアント機能(署名付き URL のアップロード・ダウンロード)のみをエクスポートし、`storage` サービスオブジェクトは含みません。
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { apiClient } from '@h-ai/api-client'
|
|
75
|
+
import { downloadAndSave, uploadWithPresignedUrl } from '@h-ai/storage/client'
|
|
76
|
+
|
|
77
|
+
// apiClient 已初始化,服务端使用 S3。
|
|
78
|
+
const signed = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
|
|
79
|
+
if (!signed.success)
|
|
80
|
+
throw new Error(signed.error.message)
|
|
81
|
+
await uploadWithPresignedUrl(signed.data.url, file)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> 注:`@h-ai/storage` に HTTP ルートは組み込まれていません。署名付き URL API はアプリケーション層で実装してください。
|
|
85
|
+
|
|
86
|
+
### HTTP API 契約
|
|
87
|
+
|
|
88
|
+
公開 HTTP API は `@h-ai/api-contract` で定義し、`@h-ai/serv` の `createStorageProcedures({ storage, authorize })` で本モジュールの機能に接続します。
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { apiClient } from '@h-ai/api-client'
|
|
92
|
+
|
|
93
|
+
const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
|
|
94
|
+
if (!result.success)
|
|
95
|
+
throw new Error(result.error.message)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
署名付き URL は S3 のみ対応します。local の `getUrl()` / `putUrl()` は `HaiStorageError.UNSUPPORTED_OPERATION`(`hai:storage:018`)、`publicUrl()` は `null` を返し、仮想 URL は返しません。serv と apiClient は失敗結果を保持するため、URL を使う前に `success` を確認してください。ローカル開発ではアプリの既存の認証付きファイル API を利用します。このモジュールに HTTP ファイルルートや署名検証サービスは含まれません。
|
|
99
|
+
|
|
100
|
+
### API 一覧
|
|
101
|
+
|
|
102
|
+
- `storage.file`
|
|
103
|
+
- `put/get/getStream/head/exists/delete/deleteMany/copy`
|
|
104
|
+
- `storage.dir`
|
|
105
|
+
- `list/delete`
|
|
106
|
+
- `storage.presign`
|
|
107
|
+
- `getUrl/putUrl/publicUrl`
|
|
108
|
+
- `storage.checksum`
|
|
109
|
+
- `generate(data, options?) / verify(data, expected, options?)`
|
|
110
|
+
|
|
111
|
+
`file.put()` は Buffer、Uint8Array、文字列と `AsyncIterable<Uint8Array>`(Node Readable 等)を受け付け、ストリームはバッファ量を制限した S3 multipart で送信します。`getStream()` は Range と `signal` による中止に対応するバイトストリームを返します。初期エラーは HaiResult、反復中の I/O・中止エラーは呼び出し側で処理します。`get()` は従来どおり全体を Buffer に読み込み、小さいファイル向けです。
|
|
112
|
+
|
|
113
|
+
`{ ifNoneMatch: '*' }` は標準 S3 条件付き書き込み、またはローカルの原子的公開で上書きを拒否し、競合時に `ALREADY_EXISTS` を返します。非対応の S3 サービスではエラーとし、上書きに切り替えません。失敗・中止時は一時ファイルや未完了パートを削除しますが、公開済みオブジェクトは中止で取り消せません。
|
|
114
|
+
|
|
115
|
+
`file.deleteMany(keys)` は `StorageDeleteResult` を返します。成功時の data は `{ deletedKeys, failures: [] }`、削除未完了の項目がある場合は `OPERATION_FAILED` と `error.ext?.deleteMany` に進捗を返します(未初期化などの事前エラーには進捗がない場合があります)。重複入力は一度だけ処理し、返す key に設定 prefix を含めません。S3 は最大 1000 件ずつ送信し、すべての `Deleted` / `Errors` を照合します。HTTP 200 でも全件成功とは限らず、欠落・重複・不明な応答は `unknown` になります。ローカル削除の並行数は最大 32 件です。
|
|
116
|
+
|
|
117
|
+
明示的な `InternalError / ServiceUnavailable / SlowDown / RequestTimeout` の項目だけ最大 3 回試行し、成功済み・権限エラーは再送しません。`DeleteObjects` 専用クライアントは SDK のリクエスト再試行を無効化します。通信が中断した項目は直ちに `unknown`、後続の未送信バッチは `notAttempted` とし、不明な結果を再送しません。`retryable` は障害の分類であり、再試行前に不明な結果と並行書き込みを確認してください。`dir.delete()` は削除エラーを伝播しますが、並行して追加されるオブジェクトは防げません。S3 の削除は VersionId を指定しないため、バージョニングが有効な場合は削除マーカーを作成することがあり、過去のバージョンは消去しません。
|
|
118
|
+
|
|
119
|
+
`@h-ai/serv` / `@h-ai/api-client` の `storage.files.deleteMany` も同じ成功データと失敗進捗を返します。
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const result = await storage.file.deleteMany(['a.txt', 'b.txt'])
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
const progress = result.error.ext?.deleteMany
|
|
125
|
+
// 不明な結果と業務上の書き込みを確認してから補償する。
|
|
126
|
+
const knownFailures = progress?.failures.filter(item => item.outcome === 'failed' && item.retryable) ?? []
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`checksum` は初期化不要の Node 専用ツールです。実際の入力バイトを生成・検証し、ストリームは一度だけ消費します。既定値は SHA-256、小文字 hex。`algorithm: 'sha256' | 'sha384' | 'sha512' | 'sha1' | 'md5'`、`encoding: 'hex' | 'base64'`、`signal` を指定できます。不一致は `ok(false)`、不正形式・アルゴリズムや読み取り失敗はエラーです。MD5/SHA-1 は互換性確認のみで、認証用ではありません。ETag と multipart の複合 checksum はファイル全体の摘要と異なり、AWS checksum 拡張の実装は強制しません。
|
|
131
|
+
|
|
132
|
+
checksum の中止時は次のチャンクを待つ処理を終了し、入力イテレーターの終了を要求します。ローカルの `getStream()` はディレクトリをストリームとして返さず、その場で `INVALID_PATH` を返します。
|
|
133
|
+
|
|
134
|
+
> 注:`publicUrl` は S3 の `publicUrl` を設定した場合のみ文字列を返し、それ以外は `null` を返します。
|
|
135
|
+
|
|
136
|
+
> 注:`storage.config` は**機密情報をマスクした設定スナップショット**を返します。S3 の秘密情報は `[REDACTED]` に置換され、再初期化には使用できません。
|
|
137
|
+
|
|
138
|
+
### クライアントヘルパー
|
|
139
|
+
|
|
140
|
+
`@h-ai/storage/client` は以下のブラウザー用ツールも提供します:
|
|
141
|
+
|
|
142
|
+
- `uploadWithPresignedUrl(url, data, options)`
|
|
143
|
+
- `downloadWithPresignedUrl(url, options)`
|
|
144
|
+
- `downloadAndSave(url, { filename })`
|
|
145
|
+
- `getFileExtension(file)`
|
|
146
|
+
- `getMimeType(extension)`
|
|
147
|
+
- `formatFileSize(bytes)`
|
|
148
|
+
|
|
149
|
+
### 設定
|
|
150
|
+
|
|
151
|
+
- **S3**:`bucket / region / accessKeyId / secretAccessKey` は必須。`createBucket / endpoint / publicEndpoint / forcePathStyle / prefix / publicUrl` は任意。
|
|
152
|
+
- **Local**:`root` は必須。`directoryMode / fileMode` は任意。
|
|
153
|
+
|
|
154
|
+
MinIO と RustFS は S3 Provider を共有し、`type: 's3'` を使います。専用のストレージ種別は不要です。自前のインスタンスでは S3 API endpoint(通常ポート 9000)とパス形式を設定します。
|
|
155
|
+
|
|
156
|
+
`createBucket` の既定値は `false` で、既存バケットが必要です。`true` では `storage.init()` が HeadBucket を確認し、明示的な 404 の場合のみ作成します。既存バケットとオブジェクトは保持します。権限・ネットワーク・作成・後続の接続確認が失敗すると、失敗結果を返しクライアントを解放して未初期化状態を維持します。同時作成の `BucketAlreadyOwnedByYou` は再度アクセスを確認してから受け入れます。AWS `us-east-1` では LocationConstraint を省略し、他のリージョンでは設定値を渡します。標準の自前 S3 デプロイでは `HAI_STORAGE_CREATEBUCKET=true` を有効にし、外部 S3 は必要な権限に応じて明示的に設定します。
|
|
157
|
+
|
|
158
|
+
YAML では `createBucket: ${HAI_STORAGE_CREATEBUCKET:false}` で環境変数を参照し、`storage.init()` への直接入力は真偽値を使います。
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const initResult = await storage.init({
|
|
162
|
+
type: 's3',
|
|
163
|
+
endpoint: 'http://localhost:9000',
|
|
164
|
+
bucket: 'my-bucket',
|
|
165
|
+
region: 'us-east-1',
|
|
166
|
+
accessKeyId: process.env.HAI_STORAGE_ACCESSKEYID!,
|
|
167
|
+
secretAccessKey: process.env.HAI_STORAGE_SECRETACCESSKEY!,
|
|
168
|
+
forcePathStyle: true,
|
|
169
|
+
createBucket: true,
|
|
170
|
+
})
|
|
171
|
+
if (!initResult.success) {
|
|
172
|
+
throw new Error(initResult.error.message)
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### 内部接続とブラウザーからの直接アクセス
|
|
177
|
+
|
|
178
|
+
`endpoint` はサーバーの CRUD、バケット作成、接続確認に使います。`publicEndpoint` は `presign.getUrl/putUrl` の外部アドレスで既定値はなく、省略時は endpoint を使います。`https://app.example.com/delivery/files` のようなプロキシパスを指定できます。パスには英数字、アンダースコア、ドット、ハイフンを使用でき、`.`/`..`、連続スラッシュ、符号化された区切りは使用できません。バケット、認証情報、クエリ、フラグメントは含めません。`publicUrl` は公開 URL の組み立て用で、私有オブジェクトを認可しません。
|
|
179
|
+
|
|
180
|
+
自前 Compose の内部接続先は `http://storage:9000` です。上記の外部 URL は `/delivery/files/hai/private/report.zip?...`、パス省略時は `/hai/private/report.zip?...` になります。パス指定時の外部署名はパス形式を使い、内部操作は `forcePathStyle` に従います。Nginx は入口パスだけを除去し、本来の S3 署名パス、クエリ、Host を保持して Storage へ直接転送します。大容量と Range は Serv を経由しません。Fullstack は Web Nginx、SSR/API は gateway を使用します。サイト配下に置けますが API、資源、ヘルスパスとの競合は拒否します。公開設定変更には完全デプロイが必要で、部分更新と復旧は既存設定を保持します。
|
|
181
|
+
|
|
182
|
+
外部 S3/R2 や既存ゲートウェイには独自の到達可能な端点を使用し、外部/保持対象ストレージにローカル sidecar ルートは生成しません。HTTPS 入口では署名対象 Host、元の URI、クエリを保持します。クロスオリジンでは実際の S3 に適切な CORS を設定してください。ゲートウェイはバケット認可や CORS を代替しません。署名付き URL は短期認証情報として扱い、有効期間とログを管理します。GET 署名は HEAD には使えません。Local は署名 URL に非対応で、非公開ディレクトリを Nginx に公開しません。
|
|
183
|
+
|
|
184
|
+
自前バケット/リージョンが空の場合はデプロイが hai/us-east-1 を注入します。Storage Schema の既定値ではありません。オブジェクトプロキシは CSP sandbox、nosniff、no-referrer、private,no-store を設定し、同一オリジンの HTML を含む信頼できない添付ファイルを隔離します。ファイル配信用で、実行可能なサイトスクリプトは配信しません。外部 S3/CDN にも同等の分離を設定してください。
|
|
185
|
+
|
|
186
|
+
#### 操作ログ
|
|
187
|
+
|
|
188
|
+
`operationLog` でファイル・ディレクトリー・署名付き URL の操作ログを有効にできます。Local/S3 Provider の実際の操作箇所で出力し、`storage-main.ts` で一括ラップしません。ファイル内容は記録せず、アップロードデータはバイト数のみを記録します。
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
# config/_storage.yml
|
|
192
|
+
type: local
|
|
193
|
+
root: ./data/uploads
|
|
194
|
+
operationLog:
|
|
195
|
+
read: false # file.get/head/exists, dir.list, presign.getUrl/publicUrl
|
|
196
|
+
write: false # file.put/delete/deleteMany/copy, dir.delete, presign.putUrl
|
|
197
|
+
maxLength: 1000 # 参数序列化后的最大输出长度,超出会截断
|
|
198
|
+
level: debug # info | debug | trace,默认 debug
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
await storage.init({
|
|
203
|
+
type: 'local',
|
|
204
|
+
root: './data/uploads',
|
|
205
|
+
operationLog: {
|
|
206
|
+
read: true,
|
|
207
|
+
write: true,
|
|
208
|
+
maxLength: 500,
|
|
209
|
+
level: 'debug',
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`storage.presign.putUrl()` は現在 `contentType` や `expiresIn` などの署名パラメーターに対応しますが、`maxSize` 制限は提供しません。
|
|
215
|
+
|
|
216
|
+
ローカルの `storage.dir.list()` は `prefix` を key の文字列接頭辞(例:`reports/2026-`)として扱い、UTF-8 バイト順でページ分割します。`maxKeys` は 1–1000 の整数で既定値は 1000、`files` と `commonPrefixes` の合計件数に適用します。`delimiter` は prefix の後で最初に現れる区切りまでを一つの `commonPrefixes` 項目にまとめます。空文字列ではグループ化せず、空の物理ディレクトリーは項目になりません。グループ化と件数の規則は [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) に基づきます。
|
|
217
|
+
|
|
218
|
+
後続項目がある場合にだけ `isTruncated: true` と `nextContinuationToken` を返します。ローカルカーソルはプロセス内キャッシュを使わず、正規化したルートディレクトリー、prefix、delimiter に結び付きます。同じルートを使用する別インスタンスや再起動後も継続でき、次ページの `maxKeys` は変更可能です。不正な形式、未対応バージョン、範囲が異なるカーソルは `INVALID_LIST_OPTIONS` になります。カーソルは認可情報でもスナップショットでもありません。prefix の認可は引き続き必要で、同時更新は後続ページに影響し、異なるマウントパスは同じ範囲として扱いません。各リクエストで対象ディレクトリーの走査と整列を行うため、大きな範囲では prefix を絞るか S3 を使用してください。
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
let continuationToken: string | undefined
|
|
222
|
+
do {
|
|
223
|
+
const page = await storage.dir.list({ prefix: 'reports/2026-', maxKeys: 100, continuationToken })
|
|
224
|
+
if (!page.success)
|
|
225
|
+
return page
|
|
226
|
+
// page.data.files と page.data.commonPrefixes を処理。
|
|
227
|
+
continuationToken = page.data.nextContinuationToken
|
|
228
|
+
} while (continuationToken)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### エラー処理
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { HaiStorageError, storage } from '@h-ai/storage'
|
|
235
|
+
|
|
236
|
+
const result = await storage.file.get('image.png')
|
|
237
|
+
if (!result.success && result.error.code === HaiStorageError.NOT_INITIALIZED.code) {
|
|
238
|
+
// 存储未初始化
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
主なエラーコード:
|
|
243
|
+
|
|
244
|
+
| エラーコード | code | 説明 |
|
|
245
|
+
| --------------------------------------- | ----------------- | ------------------------------------------------------------ |
|
|
246
|
+
| `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | 未初期化 |
|
|
247
|
+
| `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | 接続に失敗 |
|
|
248
|
+
| `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | 操作に失敗 |
|
|
249
|
+
| `HaiStorageError.NOT_FOUND` | `hai:storage:003` | ファイルが存在しない |
|
|
250
|
+
| `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | 権限不足 |
|
|
251
|
+
| `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | 署名付き URL の生成に失敗 |
|
|
252
|
+
| `HaiStorageError.UNSUPPORTED_OPERATION` | `hai:storage:018` | Provider が操作をサポートしない(local の署名付き URL など) |
|
|
253
|
+
| `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | ローカル一覧の引数・カーソルが不正、または検索範囲が不一致 |
|
|
254
|
+
| `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 設定エラー |
|
|
255
|
+
|
|
256
|
+
## 本番の共有状態
|
|
257
|
+
|
|
258
|
+
本番は共有 S3 または互換サービスを使います。local は開発・明示的テスト用です。オブジェクト認可と読取後更新の条件付きコミットはアプリが担当します。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
|
|
259
|
+
|
|
260
|
+
## 入力制約と HTTP 機能
|
|
261
|
+
|
|
262
|
+
キーは UTF-8 で最大 1024 バイトで、絶対パス、バックスラッシュ、制御文字、ドットセグメントを拒否します。公開 HTTP 一括削除は一度に 1–1000 件です。共通規則はブラウザー対応の `@h-ai/storage/schemas` から参照できます。署名有効期限は整数 1–604800 秒(Node 既定 3600、HTTP 既定 900)。アップロード署名の入力は key/contentType/expiresIn のみで、contentLength/maxSize/metadata/checksum は未対応です。不正入力は HTTP 400、typed client では VALIDATION_FAILED です。FileUpload の制限は選択時の補助であり、実際のオブジェクトはサーバーで検証してください。Node のストリーム/checksum/条件付き書き込みには既定の HTTP エンドポイントがありません。[機能と認可の対応表(中国語)](README.md#存储输入约束与跨端能力)を参照してください。
|
|
263
|
+
|
|
264
|
+
## ブラウザーアップロードのキャンセル
|
|
265
|
+
|
|
266
|
+
`uploadWithPresignedUrl(url, data, { onProgress, abortController })` は進捗通知にネイティブXHRを使用し、`onProgress` を省略するとfetchを使用します。事前にキャンセルされた場合は送信しません。準備中・送信中のキャンセル、外部からのXHR abort、通信・タイムアウト、進捗コールバックの例外は一度だけ失敗として確定し、この操作のリスナーを解除します。完了後のキャンセルは古いXHRを操作しません。キャンセルはローカル要求の停止であり、リモートに保存済みのオブジェクトを取り消せません。状態を確認してから、別途認可して削除してください。
|
|
267
|
+
|
|
268
|
+
## 開発とテスト
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
pnpm --filter @h-ai/storage test
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
S3 統合テストには Docker または互換ランタイムが必要です。初期化、ファイル、ディレクトリー、署名付き URL、実際の HTTP アップロード・ダウンロードの同じテストを MinIO と RustFS で実行し、コンテナー起動失敗をスキップしません。
|
|
275
|
+
|
|
276
|
+
- MinIO:[`ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z`](https://github.com/coollabsio/minio)。上流ソースからビルドしたコミュニティーイメージ。
|
|
277
|
+
- RustFS:[`ghcr.io/rustfs/rustfs:1.0.1`](https://github.com/rustfs/rustfs)。公式イメージ。
|
|
278
|
+
|
|
279
|
+
コンテナーは一時生成したテスト認証情報を使用します。サービスの準備完了後に S3 API でバケットを作成し、最後のリース解放時にコンテナーと匿名ボリュームを削除します。
|
|
280
|
+
|
|
281
|
+
ソースの JSDoc は[共通規約](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)に従います。公開例は実際の公開入口、内部例は元のモジュールスコープで型検査します。ビルド後に `pnpm check:jsdoc --package storage` を実行してください。例の型検査は実バックエンドの動作検証の代わりにはなりません。
|
|
282
|
+
|
|
283
|
+
実Chromeの回帰ゲートはChromeを用意し、`HAI_STORAGE_RUN_BROWSER_GATES=1` を設定して `pnpm --filter @h-ai/storage exec vitest run tests/storage-client-browser.test.ts` を実行します。実際のHTTP PUTでネイティブキャンセルとリスナー解放を検証し、ブラウザーがなければ失敗します。
|
|
284
|
+
|
|
285
|
+
## ライセンス
|
|
286
|
+
|
|
287
|
+
Apache-2.0
|