@nestm/storage 0.1.0-alpha.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/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/SECURITY.md +25 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +36 -0
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -0
- package/dist/files-sdk/files-sdk.driver.js +352 -0
- package/dist/files-sdk/files-sdk.driver.js.map +1 -0
- package/dist/files-sdk/index.d.ts +3 -0
- package/dist/files-sdk/index.d.ts.map +1 -0
- package/dist/files-sdk/index.js +2 -0
- package/dist/files-sdk/index.js.map +1 -0
- package/dist/gateway/index.d.ts +3 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/storage-gateway-fastify-parser.d.ts +9 -0
- package/dist/gateway/storage-gateway-fastify-parser.d.ts.map +1 -0
- package/dist/gateway/storage-gateway-fastify-parser.js +50 -0
- package/dist/gateway/storage-gateway-fastify-parser.js.map +1 -0
- package/dist/gateway/storage-gateway.controller.d.ts +31 -0
- package/dist/gateway/storage-gateway.controller.d.ts.map +1 -0
- package/dist/gateway/storage-gateway.controller.js +601 -0
- package/dist/gateway/storage-gateway.controller.js.map +1 -0
- package/dist/gateway/storage-gateway.guard.d.ts +7 -0
- package/dist/gateway/storage-gateway.guard.d.ts.map +1 -0
- package/dist/gateway/storage-gateway.guard.js +40 -0
- package/dist/gateway/storage-gateway.guard.js.map +1 -0
- package/dist/gateway/storage-gateway.module.d.ts +6 -0
- package/dist/gateway/storage-gateway.module.d.ts.map +1 -0
- package/dist/gateway/storage-gateway.module.js +98 -0
- package/dist/gateway/storage-gateway.module.js.map +1 -0
- package/dist/gateway/storage-gateway.tokens.d.ts +3 -0
- package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -0
- package/dist/gateway/storage-gateway.tokens.js +3 -0
- package/dist/gateway/storage-gateway.tokens.js.map +1 -0
- package/dist/gateway/storage-gateway.types.d.ts +65 -0
- package/dist/gateway/storage-gateway.types.d.ts.map +1 -0
- package/dist/gateway/storage-gateway.types.js +13 -0
- package/dist/gateway/storage-gateway.types.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/inject-storage.decorator.d.ts +2 -0
- package/dist/inject-storage.decorator.d.ts.map +1 -0
- package/dist/inject-storage.decorator.js +6 -0
- package/dist/inject-storage.decorator.js.map +1 -0
- package/dist/internal/settle-many.d.ts +7 -0
- package/dist/internal/settle-many.d.ts.map +1 -0
- package/dist/internal/settle-many.js +62 -0
- package/dist/internal/settle-many.js.map +1 -0
- package/dist/storage-module.options.d.ts +62 -0
- package/dist/storage-module.options.d.ts.map +1 -0
- package/dist/storage-module.options.js +2 -0
- package/dist/storage-module.options.js.map +1 -0
- package/dist/storage-upload-control.d.ts +22 -0
- package/dist/storage-upload-control.d.ts.map +1 -0
- package/dist/storage-upload-control.js +139 -0
- package/dist/storage-upload-control.js.map +1 -0
- package/dist/storage.client.d.ts +51 -0
- package/dist/storage.client.d.ts.map +1 -0
- package/dist/storage.client.js +395 -0
- package/dist/storage.client.js.map +1 -0
- package/dist/storage.driver.d.ts +18 -0
- package/dist/storage.driver.d.ts.map +1 -0
- package/dist/storage.driver.js +2 -0
- package/dist/storage.driver.js.map +1 -0
- package/dist/storage.error.d.ts +39 -0
- package/dist/storage.error.d.ts.map +1 -0
- package/dist/storage.error.js +49 -0
- package/dist/storage.error.js.map +1 -0
- package/dist/storage.module.d.ts +11 -0
- package/dist/storage.module.d.ts.map +1 -0
- package/dist/storage.module.js +170 -0
- package/dist/storage.module.js.map +1 -0
- package/dist/storage.service.d.ts +12 -0
- package/dist/storage.service.d.ts.map +1 -0
- package/dist/storage.service.js +246 -0
- package/dist/storage.service.js.map +1 -0
- package/dist/storage.tokens.d.ts +6 -0
- package/dist/storage.tokens.d.ts.map +1 -0
- package/dist/storage.tokens.js +13 -0
- package/dist/storage.tokens.js.map +1 -0
- package/dist/storage.types.d.ts +244 -0
- package/dist/storage.types.d.ts.map +1 -0
- package/dist/storage.types.js +2 -0
- package/dist/storage.types.js.map +1 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +112 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @nestm/storage
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
The project intends to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
after the initial experimental releases.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.1.0-alpha.0] - 2026-07-30
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- NestJS 12 dynamic module with local-by-default root and feature registration.
|
|
16
|
+
- Default and named stores through `StorageService.use()` and `@InjectStorage()`.
|
|
17
|
+
- NestM-owned streaming, buffered, bulk, search, signing, resumable-upload, and
|
|
18
|
+
error contracts backed by `files-sdk`.
|
|
19
|
+
- Cross-store streaming transfer and mirror/sync workflows.
|
|
20
|
+
- Optional guard-required Express/Fastify HTTP gateway.
|
|
21
|
+
- Node 22/24 CI and Changesets-based alpha publishing through npm OIDC.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for helping improve `@nestm/storage`.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js 22.12 or newer
|
|
8
|
+
- Corepack
|
|
9
|
+
- Git
|
|
10
|
+
|
|
11
|
+
Enable the package manager declared by the repository:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
corepack enable
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Local setup
|
|
18
|
+
|
|
19
|
+
1. Fork and clone the repository.
|
|
20
|
+
2. Create a focused branch from `main`.
|
|
21
|
+
3. Install the locked dependencies:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pnpm install --frozen-lockfile
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. Make the change and add or update tests.
|
|
28
|
+
5. Run:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pnpm run check
|
|
32
|
+
pnpm run test
|
|
33
|
+
pnpm run verify:pack
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
6. Add a Changeset for user-visible changes:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pnpm changeset
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Design guidelines
|
|
43
|
+
|
|
44
|
+
- Keep root public contracts owned by `@nestm/storage`; upstream
|
|
45
|
+
`files-sdk` types belong only in the explicit `files-sdk` bridge.
|
|
46
|
+
- Preserve streaming and backpressure. Do not buffer an object unless the
|
|
47
|
+
method name and a tested byte limit make that behavior explicit.
|
|
48
|
+
- Keep provider-specific dependencies optional and outside the build output.
|
|
49
|
+
- Fail closed when a requested capability cannot be honored.
|
|
50
|
+
- Keep the HTTP gateway optional, deny-by-default, and protected by Nest guards.
|
|
51
|
+
- Keep `StorageModule` local by default.
|
|
52
|
+
- Include `.js` suffixes for local imports compiled as Node ESM.
|
|
53
|
+
|
|
54
|
+
Tests should cover the memory driver contract, named Nest registration,
|
|
55
|
+
duplicate isolation, transfer/sync behavior, and both Express and Fastify for
|
|
56
|
+
gateway changes. Keep tests deterministic and close every Nest application.
|
|
57
|
+
|
|
58
|
+
## One-time npm bootstrap
|
|
59
|
+
|
|
60
|
+
The first prerelease must be published interactively from a clean checkout of
|
|
61
|
+
`main`:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm publish --access public --tag alpha
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then configure npm Trusted Publishing:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm trust github @nestm/storage \
|
|
71
|
+
--file release.yml \
|
|
72
|
+
--repository nestm-dev/storage \
|
|
73
|
+
--environment release \
|
|
74
|
+
--allow-publish
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Subsequent releases use GitHub OIDC with provenance and keep prereleases on the
|
|
78
|
+
`alpha` dist-tag.
|
|
79
|
+
|
|
80
|
+
## Security issues
|
|
81
|
+
|
|
82
|
+
Do not open a public issue for a suspected vulnerability. Follow
|
|
83
|
+
[SECURITY.md](./SECURITY.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kauan Guesser
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# @nestm/storage
|
|
2
|
+
|
|
3
|
+
NestJS 12 storage integration with named stores, explicit streaming I/O,
|
|
4
|
+
cross-store workflows, and an optional guarded HTTP gateway.
|
|
5
|
+
|
|
6
|
+
The package uses [`files-sdk`](https://github.com/haydenbleasel/files-sdk) as
|
|
7
|
+
its provider engine, but owns the API injected into Nest applications. Provider
|
|
8
|
+
SDK types, errors, and `files.raw` do not leak through the root package.
|
|
9
|
+
|
|
10
|
+
> This package targets the NestJS 12 prerelease line and is itself published on
|
|
11
|
+
> the `alpha` dist-tag.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js 22.12 or newer
|
|
16
|
+
- NestJS `12.0.0-alpha.5` or newer in the Nest 12 prerelease line
|
|
17
|
+
- ESM
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add @nestm/storage@alpha files-sdk@2.2.2
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install only the native SDKs required by the chosen provider. For example:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
# S3 and S3-compatible providers
|
|
29
|
+
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-presigned-post \
|
|
30
|
+
@aws-sdk/s3-request-presigner @aws-sdk/lib-storage
|
|
31
|
+
|
|
32
|
+
# Google Cloud Storage
|
|
33
|
+
pnpm add @google-cloud/storage google-auth-library
|
|
34
|
+
|
|
35
|
+
# Azure Blob Storage
|
|
36
|
+
pnpm add @azure/storage-blob @azure/core-auth @azure/identity
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`files-sdk` currently declares its optional Nest peer for Nest 10 and 11. This
|
|
40
|
+
library does not import `files-sdk/nestjs`; the Nest 12 integration is entirely
|
|
41
|
+
owned here. A package manager may nevertheless report that temporary optional
|
|
42
|
+
peer mismatch while Nest 12 remains prerelease.
|
|
43
|
+
|
|
44
|
+
NestJS 12 alpha also has prerelease peer declarations that npm may reject under
|
|
45
|
+
its strict resolver. If npm reports an `ERESOLVE` error for Nest's own peers,
|
|
46
|
+
install with `npm install --legacy-peer-deps`; pnpm works with the repository's
|
|
47
|
+
checked-in peer-version policy.
|
|
48
|
+
|
|
49
|
+
## Configure named stores
|
|
50
|
+
|
|
51
|
+
Create provider adapters with `files-sdk`, wrap them through the explicit
|
|
52
|
+
bridge, and register the resulting drivers:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { Module } from '@nestjs/common';
|
|
56
|
+
import { s3 } from 'files-sdk/s3';
|
|
57
|
+
import { gcs } from 'files-sdk/gcs';
|
|
58
|
+
import { createFilesSdkDriver } from '@nestm/storage/files-sdk';
|
|
59
|
+
import { StorageModule } from '@nestm/storage';
|
|
60
|
+
|
|
61
|
+
export const StorageKey = {
|
|
62
|
+
MEDIA: 'media',
|
|
63
|
+
ARCHIVE: 'archive',
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
@Module({
|
|
67
|
+
imports: [
|
|
68
|
+
StorageModule.forRoot({
|
|
69
|
+
default: StorageKey.MEDIA,
|
|
70
|
+
stores: [
|
|
71
|
+
{
|
|
72
|
+
name: StorageKey.MEDIA,
|
|
73
|
+
driver: createFilesSdkDriver({
|
|
74
|
+
adapter: s3({
|
|
75
|
+
bucket: 'media',
|
|
76
|
+
region: 'us-east-1',
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: StorageKey.ARCHIVE,
|
|
82
|
+
driver: createFilesSdkDriver({
|
|
83
|
+
adapter: gcs({
|
|
84
|
+
bucket: 'archive',
|
|
85
|
+
}),
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
exports: [StorageModule],
|
|
92
|
+
})
|
|
93
|
+
export class AppStorageModule {}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`StorageModule` is not global by default. Import the configured module wherever
|
|
97
|
+
its exports are needed, re-export it from an infrastructure module as above, or
|
|
98
|
+
set `isGlobal: true` deliberately.
|
|
99
|
+
|
|
100
|
+
Inject one store directly:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Injectable } from '@nestjs/common';
|
|
104
|
+
import { InjectStorage, type StorageClient } from '@nestm/storage';
|
|
105
|
+
|
|
106
|
+
@Injectable()
|
|
107
|
+
export class AvatarService {
|
|
108
|
+
constructor(
|
|
109
|
+
@InjectStorage(StorageKey.MEDIA)
|
|
110
|
+
private readonly media: StorageClient,
|
|
111
|
+
) {}
|
|
112
|
+
|
|
113
|
+
upload(userId: string, body: ReadableStream<Uint8Array>) {
|
|
114
|
+
return this.media.upload(`avatars/${userId}.png`, body, {
|
|
115
|
+
contentType: 'image/png',
|
|
116
|
+
multipart: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or select stores through the manager:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { Injectable } from '@nestjs/common';
|
|
126
|
+
import { StorageService } from '@nestm/storage';
|
|
127
|
+
|
|
128
|
+
@Injectable()
|
|
129
|
+
export class ArchiveService {
|
|
130
|
+
constructor(private readonly storage: StorageService) {}
|
|
131
|
+
|
|
132
|
+
async archive(key: string) {
|
|
133
|
+
const object = await this.storage.use(StorageKey.MEDIA).downloadStream(key);
|
|
134
|
+
return this.storage
|
|
135
|
+
.use(StorageKey.ARCHIVE)
|
|
136
|
+
.upload(`retained/${key}`, object.body, {
|
|
137
|
+
contentType: object.contentType,
|
|
138
|
+
...(object.metadata !== undefined && {
|
|
139
|
+
metadata: object.metadata,
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`storage.use()` selects the configured default. When several stores are
|
|
147
|
+
registered without `default`, callers must pass a name.
|
|
148
|
+
|
|
149
|
+
### Async registration
|
|
150
|
+
|
|
151
|
+
Store names remain static because Nest must create their injection tokens
|
|
152
|
+
before an async factory runs:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
StorageModule.forRootAsync({
|
|
156
|
+
default: 'media',
|
|
157
|
+
imports: [ConfigModule],
|
|
158
|
+
stores: [
|
|
159
|
+
{
|
|
160
|
+
name: 'media',
|
|
161
|
+
inject: [ConfigService],
|
|
162
|
+
useFactory: (config: ConfigService) =>
|
|
163
|
+
createFilesSdkDriver({
|
|
164
|
+
adapter: s3({
|
|
165
|
+
bucket: config.getOrThrow('MEDIA_BUCKET'),
|
|
166
|
+
region: config.getOrThrow('AWS_REGION'),
|
|
167
|
+
}),
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`useClass` and `useExisting` are also supported through
|
|
175
|
+
`StorageDriverFactory#createStorageDriver(name)`. Feature-owned stores use the
|
|
176
|
+
same shapes through `forFeature()` and `forFeatureAsync()` and export only
|
|
177
|
+
their named `@InjectStorage(name)` clients; the root `StorageService` remains
|
|
178
|
+
isolated to the stores declared by `forRoot`.
|
|
179
|
+
|
|
180
|
+
Duplicate names within one registration fail immediately. Separate feature
|
|
181
|
+
modules retain their own DI scope rather than mutating an application-wide
|
|
182
|
+
registry. Names are case-sensitive and cannot contain leading or trailing
|
|
183
|
+
whitespace.
|
|
184
|
+
|
|
185
|
+
## Storage API
|
|
186
|
+
|
|
187
|
+
`StorageClient` exposes:
|
|
188
|
+
|
|
189
|
+
- `upload`, `downloadStream`, `head`, `exists`, `delete`, `copy`, and `move`;
|
|
190
|
+
- `list`, cursor-aware `listAll`, and lazy `search`;
|
|
191
|
+
- `signDownload` and discriminated PUT/POST `signUpload`;
|
|
192
|
+
- `uploadMany`, `downloadMany`, `headMany`, `existsMany`, and `deleteMany`;
|
|
193
|
+
- `file(key)` handles;
|
|
194
|
+
- provider capability inspection; and
|
|
195
|
+
- pause/resume/abort through `StorageUploadControl`.
|
|
196
|
+
|
|
197
|
+
Downloads are streaming by default:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const object = await storage.use('media').downloadStream('video.mp4');
|
|
201
|
+
// object.body is a Web ReadableStream<Uint8Array>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The explicit `downloadBytes`, `downloadText`, and `downloadJson` helpers default
|
|
205
|
+
to a 10 MiB in-memory limit:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const manifest = await storage
|
|
209
|
+
.use('media')
|
|
210
|
+
.downloadJson<{ version: number }>('manifest.json', {
|
|
211
|
+
maxBytes: 256 * 1024,
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Node `Readable` uploads are accepted and converted to Web streams without
|
|
216
|
+
buffering. Provider capability gaps fail closed with `StorageError` rather than
|
|
217
|
+
silently discarding a range, metadata, or cache-control request.
|
|
218
|
+
|
|
219
|
+
### Resumable uploads
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { StorageUploadControl } from '@nestm/storage';
|
|
223
|
+
|
|
224
|
+
const control = new StorageUploadControl();
|
|
225
|
+
const upload = media.upload('large.bin', file, {
|
|
226
|
+
control,
|
|
227
|
+
multipart: { partSize: 8 * 1024 * 1024, concurrency: 4 },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
control.pause();
|
|
231
|
+
const token = control.toJSON(); // opaque and JSON-serializable
|
|
232
|
+
control.resume();
|
|
233
|
+
await upload;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Persisted tokens are intentionally opaque and versioned. Restore one with
|
|
237
|
+
`StorageUploadControl.from(token)`. Resumable uploads require a repeatable,
|
|
238
|
+
known-length body; a one-shot stream cannot be resumed.
|
|
239
|
+
|
|
240
|
+
### Cross-store transfer and sync
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
await storage.transfer({
|
|
244
|
+
from: 'media',
|
|
245
|
+
to: 'archive',
|
|
246
|
+
prefix: 'uploads/',
|
|
247
|
+
concurrency: 4,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const preview = await storage.sync({
|
|
251
|
+
from: 'media',
|
|
252
|
+
to: 'archive',
|
|
253
|
+
compare: 'size',
|
|
254
|
+
prune: true,
|
|
255
|
+
dryRun: true,
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Object bodies stream directly between stores. Key lists and metadata are
|
|
260
|
+
collected to provide deterministic progress and sync plans. Partial failures
|
|
261
|
+
are returned in `errors`. `prune: true` is destructive; use `dryRun` first and
|
|
262
|
+
scope the destination with `destinationPrefix`.
|
|
263
|
+
|
|
264
|
+
## Optional HTTP gateway
|
|
265
|
+
|
|
266
|
+
The gateway lives at `@nestm/storage/gateway` and is never mounted by
|
|
267
|
+
`StorageModule`.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { Module } from '@nestjs/common';
|
|
271
|
+
import {
|
|
272
|
+
StorageGatewayModule,
|
|
273
|
+
StorageGatewayOperation,
|
|
274
|
+
} from '@nestm/storage/gateway';
|
|
275
|
+
|
|
276
|
+
@Module({
|
|
277
|
+
imports: [
|
|
278
|
+
AppStorageModule,
|
|
279
|
+
AuthModule,
|
|
280
|
+
StorageGatewayModule.register({
|
|
281
|
+
imports: [AppStorageModule, AuthModule],
|
|
282
|
+
store: 'media',
|
|
283
|
+
guards: [JwtAuthGuard],
|
|
284
|
+
mode: 'hybrid',
|
|
285
|
+
operations: [
|
|
286
|
+
StorageGatewayOperation.DOWNLOAD,
|
|
287
|
+
StorageGatewayOperation.UPLOAD,
|
|
288
|
+
StorageGatewayOperation.HEAD,
|
|
289
|
+
StorageGatewayOperation.LIST,
|
|
290
|
+
StorageGatewayOperation.SIGN_DOWNLOAD,
|
|
291
|
+
StorageGatewayOperation.SIGN_UPLOAD,
|
|
292
|
+
],
|
|
293
|
+
maxUploadBytes: 100 * 1024 * 1024,
|
|
294
|
+
maxListResults: 1000,
|
|
295
|
+
maxSearchResults: 1000,
|
|
296
|
+
proxyInlineContentTypes: ['image/jpeg', 'image/png'],
|
|
297
|
+
}),
|
|
298
|
+
],
|
|
299
|
+
})
|
|
300
|
+
export class AppModule {}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Registration fails without at least one existing Nest guard. The only bypass is
|
|
304
|
+
the explicit `allowUnauthenticated: true` development escape hatch. Operations
|
|
305
|
+
are deny-by-default and must be allowlisted individually.
|
|
306
|
+
|
|
307
|
+
Proxy downloads default to `Content-Disposition: attachment` and always send
|
|
308
|
+
`X-Content-Type-Options: nosniff`. Add only trusted, non-active MIME types to
|
|
309
|
+
`proxyInlineContentTypes` when browser rendering is required. Search responses
|
|
310
|
+
are capped by `maxSearchResults`, and list pages by `maxListResults` (both
|
|
311
|
+
1,000 by default).
|
|
312
|
+
|
|
313
|
+
The fixed gateway prefix is `/storage`:
|
|
314
|
+
|
|
315
|
+
| Method | Path | Operation |
|
|
316
|
+
| -------- | --------------------------- | ------------------------------------ |
|
|
317
|
+
| `GET` | `/storage/object?key=...` | signed redirect or streamed download |
|
|
318
|
+
| `PUT` | `/storage/object?key=...` | streamed proxy upload |
|
|
319
|
+
| `HEAD` | `/storage/metadata?key=...` | object metadata |
|
|
320
|
+
| `DELETE` | `/storage/object?key=...` | delete |
|
|
321
|
+
| `GET` | `/storage/list` | list one page |
|
|
322
|
+
| `GET` | `/storage/search` | search keys |
|
|
323
|
+
| `POST` | `/storage/sign-download` | sign download |
|
|
324
|
+
| `POST` | `/storage/sign-upload` | sign direct upload |
|
|
325
|
+
| `POST` | `/storage/copy` | copy |
|
|
326
|
+
| `POST` | `/storage/move` | move |
|
|
327
|
+
|
|
328
|
+
Proxy uploads use `Content-Type: application/octet-stream`; put the stored MIME
|
|
329
|
+
type in `X-Storage-Content-Type`. This keeps Express and Fastify uploads as raw
|
|
330
|
+
streams and avoids global body-parser changes. Generic multipart/form-data is
|
|
331
|
+
not mounted by the gateway—prefer direct signed uploads or handle form parsing
|
|
332
|
+
in an application controller.
|
|
333
|
+
|
|
334
|
+
If an application has already registered an
|
|
335
|
+
`application/octet-stream` parser, the gateway leaves it in place; that parser
|
|
336
|
+
must enforce its own body limit and expose either a byte buffer or readable
|
|
337
|
+
stream. Without an existing parser, the built-in Fastify parser is restricted
|
|
338
|
+
to the marked gateway upload route.
|
|
339
|
+
|
|
340
|
+
`hybrid` mode prefers a signed download when both `download` and
|
|
341
|
+
`signDownload` are allowed and the provider advertises support, then uses the
|
|
342
|
+
streaming proxy when signing is unavailable. `proxy` disables signing routes;
|
|
343
|
+
`signed` disables proxy uploads and downloads.
|
|
344
|
+
|
|
345
|
+
## Errors and capabilities
|
|
346
|
+
|
|
347
|
+
Every thrown engine/provider failure is normalized to `StorageError`. Branch on
|
|
348
|
+
`error.code`, not provider classes:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { isStorageError, StorageErrorCode } from '@nestm/storage';
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await media.head(key);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (isStorageError(error) && error.code === StorageErrorCode.NOT_FOUND) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Capability flags cover range reads, native byte-level upload progress,
|
|
364
|
+
delimiter listing, metadata, cache control, resumable uploads, server-side
|
|
365
|
+
copy, and signed transfers.
|
|
366
|
+
Provider-specific native clients are intentionally not exposed from the root
|
|
367
|
+
package.
|
|
368
|
+
|
|
369
|
+
## Local and in-memory stores
|
|
370
|
+
|
|
371
|
+
The test helper wraps the `files-sdk` in-memory adapter:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { createMemoryStorageDriver } from '@nestm/storage/testing';
|
|
375
|
+
|
|
376
|
+
StorageModule.forRoot({
|
|
377
|
+
stores: [{ name: 'test', driver: createMemoryStorageDriver() }],
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
For local filesystem storage, import `fs` from `files-sdk/fs` and pass it to
|
|
382
|
+
`createFilesSdkDriver` exactly like a cloud adapter.
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Until the first stable release, security fixes target the newest published
|
|
6
|
+
prerelease. Pin and test the exact alpha version used with the matching NestJS
|
|
7
|
+
12 prerelease.
|
|
8
|
+
|
|
9
|
+
## Reporting a vulnerability
|
|
10
|
+
|
|
11
|
+
Do not disclose a suspected vulnerability in a public issue, discussion, or
|
|
12
|
+
pull request. Use GitHub's private vulnerability reporting flow:
|
|
13
|
+
|
|
14
|
+
<https://github.com/nestm-dev/storage/security/advisories/new>
|
|
15
|
+
|
|
16
|
+
Include the package and Nest versions, a minimal reproduction, expected and
|
|
17
|
+
observed behavior, likely impact, and any tested mitigation.
|
|
18
|
+
|
|
19
|
+
Reports about authorization bypasses in the optional gateway, unsafe signed URL
|
|
20
|
+
behavior, unbounded buffering, path/key confusion, credential leakage,
|
|
21
|
+
cross-store pruning, or dependency compromise are especially useful.
|
|
22
|
+
|
|
23
|
+
For a vulnerability originating in NestJS, `files-sdk`, or a provider SDK,
|
|
24
|
+
follow that project's security policy as well. You may still report it here
|
|
25
|
+
privately when this integration needs a mitigation.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Adapter, type FilesOptions } from 'files-sdk';
|
|
2
|
+
import type { StorageDriver } from '../storage.driver.js';
|
|
3
|
+
import type { StorageBody, StorageDownloadOptions, StorageListOptions, StorageListResult, StorageObject, StorageObjectMetadata, StorageOperationOptions, StorageSearchOptions, StorageSignedDownloadOptions, StorageSignedUpload, StorageSignedUploadOptions, StorageUploadOptions, StorageUploadResult } from '../storage.types.js';
|
|
4
|
+
export type FilesSdkDriverOptions<AdapterType extends Adapter> = FilesOptions<AdapterType>;
|
|
5
|
+
export declare class FilesSdkStorageDriver<AdapterType extends Adapter = Adapter> implements StorageDriver {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(options: FilesSdkDriverOptions<AdapterType>);
|
|
8
|
+
get name(): string;
|
|
9
|
+
get capabilities(): {
|
|
10
|
+
cacheControl: boolean;
|
|
11
|
+
delimiter: boolean;
|
|
12
|
+
metadata: boolean;
|
|
13
|
+
rangeRead: boolean;
|
|
14
|
+
resumableUpload: boolean;
|
|
15
|
+
serverSideCopy: boolean;
|
|
16
|
+
signedDownload: {
|
|
17
|
+
supported: boolean;
|
|
18
|
+
maxExpiresIn?: number;
|
|
19
|
+
};
|
|
20
|
+
signedUpload: "runtime";
|
|
21
|
+
nativeUploadProgress: boolean;
|
|
22
|
+
};
|
|
23
|
+
upload(key: string, body: StorageBody, options?: StorageUploadOptions): Promise<StorageUploadResult>;
|
|
24
|
+
download(key: string, options?: StorageDownloadOptions): Promise<StorageObject>;
|
|
25
|
+
head(key: string, options?: StorageOperationOptions): Promise<StorageObjectMetadata>;
|
|
26
|
+
exists(key: string, options?: StorageOperationOptions): Promise<boolean>;
|
|
27
|
+
delete(key: string, options?: StorageOperationOptions): Promise<void>;
|
|
28
|
+
copy(sourceKey: string, destinationKey: string, options?: StorageOperationOptions): Promise<void>;
|
|
29
|
+
move(sourceKey: string, destinationKey: string, options?: StorageOperationOptions): Promise<void>;
|
|
30
|
+
list(options?: StorageListOptions): Promise<StorageListResult>;
|
|
31
|
+
search(pattern: string | RegExp, options?: StorageSearchOptions): AsyncIterable<StorageObjectMetadata>;
|
|
32
|
+
signDownload(key: string, options?: StorageSignedDownloadOptions): Promise<string>;
|
|
33
|
+
signUpload(key: string, options: StorageSignedUploadOptions): Promise<StorageSignedUpload>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createFilesSdkDriver<AdapterType extends Adapter>(options: FilesSdkDriverOptions<AdapterType>): FilesSdkStorageDriver<AdapterType>;
|
|
36
|
+
//# sourceMappingURL=files-sdk.driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files-sdk.driver.d.ts","sourceRoot":"","sources":["../../src/files-sdk/files-sdk.driver.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,OAAO,EAGZ,KAAK,YAAY,EAUlB,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EACV,WAAW,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EAEvB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,qBAAqB,CAAC,WAAW,SAAS,OAAO,IAC3D,YAAY,CAAC,WAAW,CAAC,CAAC;AAyS5B,qBAAa,qBAAqB,CAChC,WAAW,SAAS,OAAO,GAAG,OAAO,CACrC,YAAW,aAAa;;gBAIZ,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC;IAKvD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,YAAY;;;;;;;;;;;;;MAaf;IAEK,MAAM,CACV,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,mBAAmB,CAAC;IAQzB,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,aAAa,CAAC;IAUnB,IAAI,CACR,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,qBAAqB,CAAC;IAMjC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAIxE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrE,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC;IAMhB,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC;IAMV,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAapE,MAAM,CACJ,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,aAAa,CAAC,qBAAqB,CAAC;IAoBvC,YAAY,CACV,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,4BAA4B,GACrC,OAAO,CAAC,MAAM,CAAC;IAMlB,UAAU,CACR,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,mBAAmB,CAAC;CAahC;AAED,wBAAgB,oBAAoB,CAAC,WAAW,SAAS,OAAO,EAC9D,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAC1C,qBAAqB,CAAC,WAAW,CAAC,CAEpC"}
|