@dataclouder/nest-storage 0.0.9 → 0.0.10
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 +66 -0
- package/docs/frontend-integration.md +98 -0
- package/docs/index.md +45 -0
- package/docs/storage-diagram.excalidraw +856 -0
- package/docs/storage-providers.md +80 -0
- package/docs/universal-storage.md +48 -0
- package/docs/usage.md +145 -0
- package/package.json +4 -2
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @dataclouder/nest-storage
|
|
2
|
+
|
|
3
|
+
NestJS library for storage services, providing a unified interface for Google Cloud Storage, Cloudflare R2, Minio, and Local storage.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @dataclouder/nest-storage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Multi-Provider Support**: Integrated support for Google Cloud Storage, Cloudflare R2, and Minio.
|
|
14
|
+
- **Unified Interface**: Use the same methods regardless of the underlying storage service.
|
|
15
|
+
- **Image Processing**: Automatic conversion of images to WebP format for optimized web delivery.
|
|
16
|
+
- **Batch Operations**: Easily remove all storage files referenced within complex data objects or by URLs.
|
|
17
|
+
- **Security**: Generate signed URLs for temporary access to private files.
|
|
18
|
+
- **Auditable**: Built-in support for tracking uploads with auditable metadata.
|
|
19
|
+
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
For detailed guides and API references, please check the documentation files:
|
|
23
|
+
|
|
24
|
+
- [Getting Started](./docs/index.md)
|
|
25
|
+
- [Usage Guide](./docs/usage.md)
|
|
26
|
+
- [Storage Providers](./docs/storage-providers.md)
|
|
27
|
+
- [Frontend Integration](./docs/frontend-integration.md)
|
|
28
|
+
- [Universal Storage](./docs/universal-storage.md)
|
|
29
|
+
|
|
30
|
+
## Basic Usage
|
|
31
|
+
|
|
32
|
+
1. **Register the module:**
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { StorageModule } from '@dataclouder/nest-storage';
|
|
36
|
+
|
|
37
|
+
@Module({
|
|
38
|
+
imports: [
|
|
39
|
+
StorageModule.register({
|
|
40
|
+
provider: 'GOOGLE', // or 'CLOUDFLARE', 'MINIO', 'LOCAL'
|
|
41
|
+
bucket: 'my-bucket',
|
|
42
|
+
// ... provider specific config
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
export class AppModule {}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. **Inject the service:**
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { StorageService } from '@dataclouder/nest-storage';
|
|
53
|
+
|
|
54
|
+
@Injectable()
|
|
55
|
+
export class MyService {
|
|
56
|
+
constructor(private storageService: StorageService) {}
|
|
57
|
+
|
|
58
|
+
async uploadFile(file: Buffer, filename: string) {
|
|
59
|
+
return await this.storageService.upload(file, filename);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
Developed by [dataclouder](https://github.com/dataclouder).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Frontend Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to connect your frontend application (e.g., Angular) to the Nest Storage service.
|
|
4
|
+
|
|
5
|
+
## API Endpoint
|
|
6
|
+
|
|
7
|
+
- **URL**: `YOUR_BASE_URL/storage/upload`
|
|
8
|
+
- **Method**: `POST`
|
|
9
|
+
- **Content-Type**: `multipart/form-data`
|
|
10
|
+
|
|
11
|
+
## Request Parameters
|
|
12
|
+
|
|
13
|
+
Metadata is passed via **Query Parameters** in the URL, while the file itself is sent in the `multipart/form-data` body.
|
|
14
|
+
|
|
15
|
+
### Query Parameters
|
|
16
|
+
|
|
17
|
+
| Parameter | Type | Required | Description |
|
|
18
|
+
| :--- | :--- | :--- | :--- |
|
|
19
|
+
| `path` | `string` | No | The target directory/path in storage (e.g., `avatars`, `docs/2024`). |
|
|
20
|
+
| `provider` | `string` | No | The storage provider to use (`google`, `cloudflare`, `local`). |
|
|
21
|
+
| `keepOriginalName` | `boolean` | No | If `true`, the file will keep its original name. If `false` (default), a timestamp prefix will be added. |
|
|
22
|
+
|
|
23
|
+
### Multipart Body
|
|
24
|
+
|
|
25
|
+
| Field | Type | Required | Description |
|
|
26
|
+
| :--- | :--- | :--- | :--- |
|
|
27
|
+
| `file` | `File` | **Yes** | The file object to be uploaded. |
|
|
28
|
+
|
|
29
|
+
> [!TIP]
|
|
30
|
+
> Using Query Parameters for metadata is the most reliable way to ensure the backend receives these values before processing the file stream.
|
|
31
|
+
|
|
32
|
+
## Example Implementation (Angular)
|
|
33
|
+
|
|
34
|
+
### 1. Storage Service
|
|
35
|
+
|
|
36
|
+
Create a service to handle the upload logic:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Injectable } from '@angular/core';
|
|
40
|
+
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
41
|
+
import { Observable } from 'rxjs';
|
|
42
|
+
|
|
43
|
+
@Injectable({
|
|
44
|
+
providedIn: 'root'
|
|
45
|
+
})
|
|
46
|
+
export class FileUploadService {
|
|
47
|
+
private apiUrl = 'http://localhost:8091/storage/upload';
|
|
48
|
+
|
|
49
|
+
constructor(private http: HttpClient) {}
|
|
50
|
+
|
|
51
|
+
uploadFile(file: File, path?: string, provider?: string, keepOriginalName?: boolean): Observable<any> {
|
|
52
|
+
const formData = new FormData();
|
|
53
|
+
formData.append('file', file);
|
|
54
|
+
|
|
55
|
+
let params = new HttpParams();
|
|
56
|
+
if (path) params = params.set('path', path);
|
|
57
|
+
if (provider) params = params.set('provider', provider);
|
|
58
|
+
if (keepOriginalName) params = params.set('keepOriginalName', 'true');
|
|
59
|
+
|
|
60
|
+
return this.http.post(this.apiUrl, formData, { params });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Component Usage
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
onFileSelected(event: any) {
|
|
69
|
+
const file: File = event.target.files[0];
|
|
70
|
+
if (file) {
|
|
71
|
+
this.uploadService.uploadFile(file, 'user-uploads', 'cloudflare').subscribe({
|
|
72
|
+
next: (response) => {
|
|
73
|
+
console.log('Upload successful', response.url);
|
|
74
|
+
},
|
|
75
|
+
error: (err) => {
|
|
76
|
+
console.error('Upload failed', err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Handling Response
|
|
84
|
+
|
|
85
|
+
The service returns a `CloudFileStorage` object:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"url": "https://storage.googleapis.com/bucket/path/to/file.png",
|
|
90
|
+
"path": "path/to/file.png",
|
|
91
|
+
"bucket": "my-bucket",
|
|
92
|
+
"provider": "google",
|
|
93
|
+
"auditable": {
|
|
94
|
+
"createdBy": "system",
|
|
95
|
+
"updatedBy": "system"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Nest Storage Library
|
|
2
|
+
|
|
3
|
+
The `@polilan/nest-storage` library provides a robust and flexible storage abstraction layer for NestJS applications. It simplifies file management by supporting multiple storage providers through a unified interface, allowing developers to switch between providers with minimal configuration changes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-Provider Support**: Integrated support for Google Cloud Storage, Cloudflare R2, and Minio.
|
|
8
|
+
- **Unified Interface**: Use the same methods regardless of the underlying storage service.
|
|
9
|
+
- **Image Processing**: Automatic conversion of images to WebP format for optimized web delivery.
|
|
10
|
+
- **Batch Operations**: Easily remove all storage files referenced within complex data objects or by URLs.
|
|
11
|
+
- **Security**: Generate signed URLs for temporary access to private files.
|
|
12
|
+
- **Auditable**: Built-in support for tracking uploads with auditable metadata.
|
|
13
|
+
- **Simplified Integration**: Support for metadata via URL Query Parameters to avoid multipart ordering issues.
|
|
14
|
+
|
|
15
|
+
## File Size Limits
|
|
16
|
+
|
|
17
|
+
By default, the application is configured to support file uploads up to **100MB**. This limit is defined at the application level in `src/main.ts` using two configurations:
|
|
18
|
+
|
|
19
|
+
1. **Fastify Body Limit**: Controls the maximum total request size.
|
|
20
|
+
2. **Multipart File Size**: Controls the maximum size for individual files.
|
|
21
|
+
|
|
22
|
+
To increase these limits, modify the following in `src/main.ts`:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// src/main.ts
|
|
26
|
+
const app = await NestFactory.create<NestFastifyApplication>(
|
|
27
|
+
AppModule,
|
|
28
|
+
new FastifyAdapter({ bodyLimit: 104857600 }), // 100MB
|
|
29
|
+
{ rawBody: true },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
app.register(multipart as any, {
|
|
33
|
+
limits: {
|
|
34
|
+
fileSize: 104857600, // 100MB
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## References
|
|
40
|
+
|
|
41
|
+
- [Frontend Integration](./frontend-integration.md): How to connect your frontend app to this service.
|
|
42
|
+
- [Storage Providers](./storage-providers.md): Detailed configuration for GOOGLE, CLOUDFLARE, and LOCAL.
|
|
43
|
+
- [Universal Storage](./universal-storage.md): Guide on the universal multi-provider integration.
|
|
44
|
+
- [Usage Guide](./usage.md): Instructions on how to upload, download, and delete files.
|
|
45
|
+
- [Cloud Models](../src/models/cloud.model.ts): Type definitions for storage objects and interfaces.
|
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "excalidraw",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
|
|
5
|
+
"elements": [
|
|
6
|
+
{
|
|
7
|
+
"id": "zKU82TpmIygY0Mi-qx8h_",
|
|
8
|
+
"type": "rectangle",
|
|
9
|
+
"x": 418.59765625,
|
|
10
|
+
"y": 545.86328125,
|
|
11
|
+
"width": 140.12109375000006,
|
|
12
|
+
"height": 90.33203125000003,
|
|
13
|
+
"angle": 0,
|
|
14
|
+
"strokeColor": "#1e1e1e",
|
|
15
|
+
"backgroundColor": "transparent",
|
|
16
|
+
"fillStyle": "solid",
|
|
17
|
+
"strokeWidth": 2,
|
|
18
|
+
"strokeStyle": "solid",
|
|
19
|
+
"roughness": 1,
|
|
20
|
+
"opacity": 100,
|
|
21
|
+
"groupIds": [],
|
|
22
|
+
"frameId": null,
|
|
23
|
+
"index": "a0",
|
|
24
|
+
"roundness": {
|
|
25
|
+
"type": 3
|
|
26
|
+
},
|
|
27
|
+
"seed": 45896797,
|
|
28
|
+
"version": 232,
|
|
29
|
+
"versionNonce": 972110067,
|
|
30
|
+
"isDeleted": false,
|
|
31
|
+
"boundElements": [
|
|
32
|
+
{
|
|
33
|
+
"type": "text",
|
|
34
|
+
"id": "gS0i9XpVwsd3uMaQ7Wc3B"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "zDX82GFclXJyCi4qv9eyV",
|
|
38
|
+
"type": "arrow"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "IxnxNhbSOmTXq5ja1mSVj",
|
|
42
|
+
"type": "arrow"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"updated": 1754681035334,
|
|
46
|
+
"link": null,
|
|
47
|
+
"locked": false
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "gS0i9XpVwsd3uMaQ7Wc3B",
|
|
51
|
+
"type": "text",
|
|
52
|
+
"x": 440.3182373046875,
|
|
53
|
+
"y": 566.029296875,
|
|
54
|
+
"width": 96.679931640625,
|
|
55
|
+
"height": 50,
|
|
56
|
+
"angle": 0,
|
|
57
|
+
"strokeColor": "#1e1e1e",
|
|
58
|
+
"backgroundColor": "transparent",
|
|
59
|
+
"fillStyle": "solid",
|
|
60
|
+
"strokeWidth": 2,
|
|
61
|
+
"strokeStyle": "solid",
|
|
62
|
+
"roughness": 1,
|
|
63
|
+
"opacity": 100,
|
|
64
|
+
"groupIds": [],
|
|
65
|
+
"frameId": null,
|
|
66
|
+
"index": "a1",
|
|
67
|
+
"roundness": null,
|
|
68
|
+
"seed": 2034064253,
|
|
69
|
+
"version": 231,
|
|
70
|
+
"versionNonce": 736338579,
|
|
71
|
+
"isDeleted": false,
|
|
72
|
+
"boundElements": null,
|
|
73
|
+
"updated": 1754681035334,
|
|
74
|
+
"link": null,
|
|
75
|
+
"locked": false,
|
|
76
|
+
"text": "Cloudflare\nR2",
|
|
77
|
+
"fontSize": 20,
|
|
78
|
+
"fontFamily": 5,
|
|
79
|
+
"textAlign": "center",
|
|
80
|
+
"verticalAlign": "middle",
|
|
81
|
+
"containerId": "zKU82TpmIygY0Mi-qx8h_",
|
|
82
|
+
"originalText": "Cloudflare\nR2",
|
|
83
|
+
"autoResize": true,
|
|
84
|
+
"lineHeight": 1.25
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "hV2YoJhzBqy1AFhCQtS8g",
|
|
88
|
+
"type": "rectangle",
|
|
89
|
+
"x": 824.197265625,
|
|
90
|
+
"y": 542.203125,
|
|
91
|
+
"width": 168.81250000000003,
|
|
92
|
+
"height": 85,
|
|
93
|
+
"angle": 0,
|
|
94
|
+
"strokeColor": "#1e1e1e",
|
|
95
|
+
"backgroundColor": "transparent",
|
|
96
|
+
"fillStyle": "solid",
|
|
97
|
+
"strokeWidth": 2,
|
|
98
|
+
"strokeStyle": "solid",
|
|
99
|
+
"roughness": 1,
|
|
100
|
+
"opacity": 100,
|
|
101
|
+
"groupIds": [],
|
|
102
|
+
"frameId": null,
|
|
103
|
+
"index": "a2",
|
|
104
|
+
"roundness": {
|
|
105
|
+
"type": 3
|
|
106
|
+
},
|
|
107
|
+
"seed": 1485623059,
|
|
108
|
+
"version": 282,
|
|
109
|
+
"versionNonce": 75405715,
|
|
110
|
+
"isDeleted": false,
|
|
111
|
+
"boundElements": [
|
|
112
|
+
{
|
|
113
|
+
"type": "text",
|
|
114
|
+
"id": "_GqJ3NjTZFY2l6jGde73Q"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "d_kxHYkH4IzFEfNCqooVY",
|
|
118
|
+
"type": "arrow"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "U7YLjSVWgy18bJF54ZIgv",
|
|
122
|
+
"type": "arrow"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"updated": 1754681036034,
|
|
126
|
+
"link": null,
|
|
127
|
+
"locked": false
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "_GqJ3NjTZFY2l6jGde73Q",
|
|
131
|
+
"type": "text",
|
|
132
|
+
"x": 870.4535446166992,
|
|
133
|
+
"y": 547.203125,
|
|
134
|
+
"width": 76.29994201660156,
|
|
135
|
+
"height": 75,
|
|
136
|
+
"angle": 0,
|
|
137
|
+
"strokeColor": "#1e1e1e",
|
|
138
|
+
"backgroundColor": "transparent",
|
|
139
|
+
"fillStyle": "solid",
|
|
140
|
+
"strokeWidth": 2,
|
|
141
|
+
"strokeStyle": "solid",
|
|
142
|
+
"roughness": 1,
|
|
143
|
+
"opacity": 100,
|
|
144
|
+
"groupIds": [],
|
|
145
|
+
"frameId": null,
|
|
146
|
+
"index": "a3",
|
|
147
|
+
"roundness": null,
|
|
148
|
+
"seed": 988247219,
|
|
149
|
+
"version": 297,
|
|
150
|
+
"versionNonce": 1262219059,
|
|
151
|
+
"isDeleted": false,
|
|
152
|
+
"boundElements": [],
|
|
153
|
+
"updated": 1754681036034,
|
|
154
|
+
"link": null,
|
|
155
|
+
"locked": false,
|
|
156
|
+
"text": "Google\nCloud\nStorage",
|
|
157
|
+
"fontSize": 20,
|
|
158
|
+
"fontFamily": 5,
|
|
159
|
+
"textAlign": "center",
|
|
160
|
+
"verticalAlign": "middle",
|
|
161
|
+
"containerId": "hV2YoJhzBqy1AFhCQtS8g",
|
|
162
|
+
"originalText": "Google\nCloud\nStorage",
|
|
163
|
+
"autoResize": true,
|
|
164
|
+
"lineHeight": 1.25
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"id": "S7FmZauf9BUumx-eDV2Ui",
|
|
168
|
+
"type": "ellipse",
|
|
169
|
+
"x": 709.12109375,
|
|
170
|
+
"y": 163.42578125,
|
|
171
|
+
"width": 135.79687500000003,
|
|
172
|
+
"height": 118.01953125000001,
|
|
173
|
+
"angle": 0,
|
|
174
|
+
"strokeColor": "#1e1e1e",
|
|
175
|
+
"backgroundColor": "transparent",
|
|
176
|
+
"fillStyle": "solid",
|
|
177
|
+
"strokeWidth": 2,
|
|
178
|
+
"strokeStyle": "solid",
|
|
179
|
+
"roughness": 1,
|
|
180
|
+
"opacity": 100,
|
|
181
|
+
"groupIds": [],
|
|
182
|
+
"frameId": null,
|
|
183
|
+
"index": "a4",
|
|
184
|
+
"roundness": {
|
|
185
|
+
"type": 2
|
|
186
|
+
},
|
|
187
|
+
"seed": 877617245,
|
|
188
|
+
"version": 317,
|
|
189
|
+
"versionNonce": 201369885,
|
|
190
|
+
"isDeleted": false,
|
|
191
|
+
"boundElements": [
|
|
192
|
+
{
|
|
193
|
+
"type": "text",
|
|
194
|
+
"id": "HjJuYtJ9JGYBfzg7LOGUV"
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"updated": 1754681014838,
|
|
198
|
+
"link": null,
|
|
199
|
+
"locked": false
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "HjJuYtJ9JGYBfzg7LOGUV",
|
|
203
|
+
"type": "text",
|
|
204
|
+
"x": 738.0481170948844,
|
|
205
|
+
"y": 209.70934144533368,
|
|
206
|
+
"width": 77.91993713378906,
|
|
207
|
+
"height": 25,
|
|
208
|
+
"angle": 0,
|
|
209
|
+
"strokeColor": "#1e1e1e",
|
|
210
|
+
"backgroundColor": "transparent",
|
|
211
|
+
"fillStyle": "solid",
|
|
212
|
+
"strokeWidth": 2,
|
|
213
|
+
"strokeStyle": "solid",
|
|
214
|
+
"roughness": 1,
|
|
215
|
+
"opacity": 100,
|
|
216
|
+
"groupIds": [],
|
|
217
|
+
"frameId": null,
|
|
218
|
+
"index": "a5",
|
|
219
|
+
"roundness": null,
|
|
220
|
+
"seed": 291142557,
|
|
221
|
+
"version": 231,
|
|
222
|
+
"versionNonce": 2100235485,
|
|
223
|
+
"isDeleted": false,
|
|
224
|
+
"boundElements": null,
|
|
225
|
+
"updated": 1754681001315,
|
|
226
|
+
"link": null,
|
|
227
|
+
"locked": false,
|
|
228
|
+
"text": "Adapter",
|
|
229
|
+
"fontSize": 20,
|
|
230
|
+
"fontFamily": 5,
|
|
231
|
+
"textAlign": "center",
|
|
232
|
+
"verticalAlign": "middle",
|
|
233
|
+
"containerId": "S7FmZauf9BUumx-eDV2Ui",
|
|
234
|
+
"originalText": "Adapter",
|
|
235
|
+
"autoResize": true,
|
|
236
|
+
"lineHeight": 1.25
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"id": "F-P1Bx_9mnTn9U1WE4Qew",
|
|
240
|
+
"type": "ellipse",
|
|
241
|
+
"x": 570.6796875,
|
|
242
|
+
"y": 166.439453125,
|
|
243
|
+
"width": 135.79687500000003,
|
|
244
|
+
"height": 118.01953125000001,
|
|
245
|
+
"angle": 0,
|
|
246
|
+
"strokeColor": "#1e1e1e",
|
|
247
|
+
"backgroundColor": "transparent",
|
|
248
|
+
"fillStyle": "solid",
|
|
249
|
+
"strokeWidth": 2,
|
|
250
|
+
"strokeStyle": "solid",
|
|
251
|
+
"roughness": 1,
|
|
252
|
+
"opacity": 100,
|
|
253
|
+
"groupIds": [],
|
|
254
|
+
"frameId": null,
|
|
255
|
+
"index": "a6",
|
|
256
|
+
"roundness": {
|
|
257
|
+
"type": 2
|
|
258
|
+
},
|
|
259
|
+
"seed": 161735165,
|
|
260
|
+
"version": 302,
|
|
261
|
+
"versionNonce": 1523756957,
|
|
262
|
+
"isDeleted": false,
|
|
263
|
+
"boundElements": [
|
|
264
|
+
{
|
|
265
|
+
"type": "text",
|
|
266
|
+
"id": "U69reRj_gVBHbb2H2COzv"
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
"updated": 1754680999839,
|
|
270
|
+
"link": null,
|
|
271
|
+
"locked": false
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"id": "U69reRj_gVBHbb2H2COzv",
|
|
275
|
+
"type": "text",
|
|
276
|
+
"x": 600.7067093190055,
|
|
277
|
+
"y": 212.72301332033368,
|
|
278
|
+
"width": 75.71994018554688,
|
|
279
|
+
"height": 25,
|
|
280
|
+
"angle": 0,
|
|
281
|
+
"strokeColor": "#1e1e1e",
|
|
282
|
+
"backgroundColor": "transparent",
|
|
283
|
+
"fillStyle": "solid",
|
|
284
|
+
"strokeWidth": 2,
|
|
285
|
+
"strokeStyle": "solid",
|
|
286
|
+
"roughness": 1,
|
|
287
|
+
"opacity": 100,
|
|
288
|
+
"groupIds": [],
|
|
289
|
+
"frameId": null,
|
|
290
|
+
"index": "a7",
|
|
291
|
+
"roundness": null,
|
|
292
|
+
"seed": 238066269,
|
|
293
|
+
"version": 225,
|
|
294
|
+
"versionNonce": 393524221,
|
|
295
|
+
"isDeleted": false,
|
|
296
|
+
"boundElements": [],
|
|
297
|
+
"updated": 1754680999839,
|
|
298
|
+
"link": null,
|
|
299
|
+
"locked": false,
|
|
300
|
+
"text": "Factory",
|
|
301
|
+
"fontSize": 20,
|
|
302
|
+
"fontFamily": 5,
|
|
303
|
+
"textAlign": "center",
|
|
304
|
+
"verticalAlign": "middle",
|
|
305
|
+
"containerId": "F-P1Bx_9mnTn9U1WE4Qew",
|
|
306
|
+
"originalText": "Factory",
|
|
307
|
+
"autoResize": true,
|
|
308
|
+
"lineHeight": 1.25
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"id": "2ElIXzVKadL3xMuB_N3Sq",
|
|
312
|
+
"type": "rectangle",
|
|
313
|
+
"x": 633.78125,
|
|
314
|
+
"y": 459.375,
|
|
315
|
+
"width": 107.33984375,
|
|
316
|
+
"height": 60,
|
|
317
|
+
"angle": 0,
|
|
318
|
+
"strokeColor": "#1e1e1e",
|
|
319
|
+
"backgroundColor": "transparent",
|
|
320
|
+
"fillStyle": "solid",
|
|
321
|
+
"strokeWidth": 2,
|
|
322
|
+
"strokeStyle": "solid",
|
|
323
|
+
"roughness": 1,
|
|
324
|
+
"opacity": 100,
|
|
325
|
+
"groupIds": [],
|
|
326
|
+
"frameId": null,
|
|
327
|
+
"index": "a8",
|
|
328
|
+
"roundness": {
|
|
329
|
+
"type": 3
|
|
330
|
+
},
|
|
331
|
+
"seed": 53723069,
|
|
332
|
+
"version": 85,
|
|
333
|
+
"versionNonce": 1089732157,
|
|
334
|
+
"isDeleted": false,
|
|
335
|
+
"boundElements": [
|
|
336
|
+
{
|
|
337
|
+
"type": "text",
|
|
338
|
+
"id": "Iy5R5J0abW1NbkQIBvZDB"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"id": "U7YLjSVWgy18bJF54ZIgv",
|
|
342
|
+
"type": "arrow"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"id": "IxnxNhbSOmTXq5ja1mSVj",
|
|
346
|
+
"type": "arrow"
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
"updated": 1754681031541,
|
|
350
|
+
"link": null,
|
|
351
|
+
"locked": false
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"id": "Iy5R5J0abW1NbkQIBvZDB",
|
|
355
|
+
"type": "text",
|
|
356
|
+
"x": 640.381217956543,
|
|
357
|
+
"y": 464.375,
|
|
358
|
+
"width": 94.13990783691406,
|
|
359
|
+
"height": 50,
|
|
360
|
+
"angle": 0,
|
|
361
|
+
"strokeColor": "#1e1e1e",
|
|
362
|
+
"backgroundColor": "transparent",
|
|
363
|
+
"fillStyle": "solid",
|
|
364
|
+
"strokeWidth": 2,
|
|
365
|
+
"strokeStyle": "solid",
|
|
366
|
+
"roughness": 1,
|
|
367
|
+
"opacity": 100,
|
|
368
|
+
"groupIds": [],
|
|
369
|
+
"frameId": null,
|
|
370
|
+
"index": "a9",
|
|
371
|
+
"roundness": null,
|
|
372
|
+
"seed": 490620221,
|
|
373
|
+
"version": 41,
|
|
374
|
+
"versionNonce": 1005724093,
|
|
375
|
+
"isDeleted": false,
|
|
376
|
+
"boundElements": null,
|
|
377
|
+
"updated": 1754680992573,
|
|
378
|
+
"link": null,
|
|
379
|
+
"locked": false,
|
|
380
|
+
"text": "Storage\nInterface",
|
|
381
|
+
"fontSize": 20,
|
|
382
|
+
"fontFamily": 5,
|
|
383
|
+
"textAlign": "center",
|
|
384
|
+
"verticalAlign": "middle",
|
|
385
|
+
"containerId": "2ElIXzVKadL3xMuB_N3Sq",
|
|
386
|
+
"originalText": "Storage\nInterface",
|
|
387
|
+
"autoResize": true,
|
|
388
|
+
"lineHeight": 1.25
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"id": "SZ0hGztGLOT86sZs4BldH",
|
|
392
|
+
"type": "ellipse",
|
|
393
|
+
"x": 677.87890625,
|
|
394
|
+
"y": 281.8203125,
|
|
395
|
+
"width": 53.12890625,
|
|
396
|
+
"height": 49.5703125,
|
|
397
|
+
"angle": 0,
|
|
398
|
+
"strokeColor": "#1e1e1e",
|
|
399
|
+
"backgroundColor": "transparent",
|
|
400
|
+
"fillStyle": "solid",
|
|
401
|
+
"strokeWidth": 2,
|
|
402
|
+
"strokeStyle": "solid",
|
|
403
|
+
"roughness": 1,
|
|
404
|
+
"opacity": 100,
|
|
405
|
+
"groupIds": [],
|
|
406
|
+
"frameId": null,
|
|
407
|
+
"index": "aB",
|
|
408
|
+
"roundness": {
|
|
409
|
+
"type": 2
|
|
410
|
+
},
|
|
411
|
+
"seed": 583586419,
|
|
412
|
+
"version": 64,
|
|
413
|
+
"versionNonce": 306634685,
|
|
414
|
+
"isDeleted": false,
|
|
415
|
+
"boundElements": [
|
|
416
|
+
{
|
|
417
|
+
"id": "d_kxHYkH4IzFEfNCqooVY",
|
|
418
|
+
"type": "arrow"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"id": "zDX82GFclXJyCi4qv9eyV",
|
|
422
|
+
"type": "arrow"
|
|
423
|
+
}
|
|
424
|
+
],
|
|
425
|
+
"updated": 1754681023262,
|
|
426
|
+
"link": null,
|
|
427
|
+
"locked": false
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"id": "d_kxHYkH4IzFEfNCqooVY",
|
|
431
|
+
"type": "arrow",
|
|
432
|
+
"x": 729.6631288573552,
|
|
433
|
+
"y": 331.37856879434014,
|
|
434
|
+
"width": 142.08407262383287,
|
|
435
|
+
"height": 204.9973806164336,
|
|
436
|
+
"angle": 0,
|
|
437
|
+
"strokeColor": "#1e1e1e",
|
|
438
|
+
"backgroundColor": "transparent",
|
|
439
|
+
"fillStyle": "solid",
|
|
440
|
+
"strokeWidth": 2,
|
|
441
|
+
"strokeStyle": "solid",
|
|
442
|
+
"roughness": 1,
|
|
443
|
+
"opacity": 100,
|
|
444
|
+
"groupIds": [],
|
|
445
|
+
"frameId": null,
|
|
446
|
+
"index": "aC",
|
|
447
|
+
"roundness": {
|
|
448
|
+
"type": 2
|
|
449
|
+
},
|
|
450
|
+
"seed": 1955501107,
|
|
451
|
+
"version": 73,
|
|
452
|
+
"versionNonce": 654151891,
|
|
453
|
+
"isDeleted": false,
|
|
454
|
+
"boundElements": null,
|
|
455
|
+
"updated": 1754681036034,
|
|
456
|
+
"link": null,
|
|
457
|
+
"locked": false,
|
|
458
|
+
"points": [
|
|
459
|
+
[
|
|
460
|
+
0,
|
|
461
|
+
0
|
|
462
|
+
],
|
|
463
|
+
[
|
|
464
|
+
142.08407262383287,
|
|
465
|
+
204.9973806164336
|
|
466
|
+
]
|
|
467
|
+
],
|
|
468
|
+
"lastCommittedPoint": null,
|
|
469
|
+
"startBinding": {
|
|
470
|
+
"elementId": "SZ0hGztGLOT86sZs4BldH",
|
|
471
|
+
"focus": -0.1841803944506745,
|
|
472
|
+
"gap": 9.694575285123364
|
|
473
|
+
},
|
|
474
|
+
"endBinding": {
|
|
475
|
+
"elementId": "hV2YoJhzBqy1AFhCQtS8g",
|
|
476
|
+
"focus": -0.029487470482841595,
|
|
477
|
+
"gap": 11.8515625
|
|
478
|
+
},
|
|
479
|
+
"startArrowhead": null,
|
|
480
|
+
"endArrowhead": "arrow",
|
|
481
|
+
"elbowed": false
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"id": "zDX82GFclXJyCi4qv9eyV",
|
|
485
|
+
"type": "arrow",
|
|
486
|
+
"x": 678.5735093188252,
|
|
487
|
+
"y": 321.70122012756815,
|
|
488
|
+
"width": 165.0295149975866,
|
|
489
|
+
"height": 216.32742106022715,
|
|
490
|
+
"angle": 0,
|
|
491
|
+
"strokeColor": "#1e1e1e",
|
|
492
|
+
"backgroundColor": "transparent",
|
|
493
|
+
"fillStyle": "solid",
|
|
494
|
+
"strokeWidth": 2,
|
|
495
|
+
"strokeStyle": "solid",
|
|
496
|
+
"roughness": 1,
|
|
497
|
+
"opacity": 100,
|
|
498
|
+
"groupIds": [],
|
|
499
|
+
"frameId": null,
|
|
500
|
+
"index": "aD",
|
|
501
|
+
"roundness": {
|
|
502
|
+
"type": 2
|
|
503
|
+
},
|
|
504
|
+
"seed": 603593309,
|
|
505
|
+
"version": 57,
|
|
506
|
+
"versionNonce": 857200691,
|
|
507
|
+
"isDeleted": false,
|
|
508
|
+
"boundElements": null,
|
|
509
|
+
"updated": 1754681035334,
|
|
510
|
+
"link": null,
|
|
511
|
+
"locked": false,
|
|
512
|
+
"points": [
|
|
513
|
+
[
|
|
514
|
+
0,
|
|
515
|
+
0
|
|
516
|
+
],
|
|
517
|
+
[
|
|
518
|
+
-165.0295149975866,
|
|
519
|
+
216.32742106022715
|
|
520
|
+
]
|
|
521
|
+
],
|
|
522
|
+
"lastCommittedPoint": null,
|
|
523
|
+
"startBinding": {
|
|
524
|
+
"elementId": "SZ0hGztGLOT86sZs4BldH",
|
|
525
|
+
"focus": 0.31546367126519276,
|
|
526
|
+
"gap": 3.8708631779442952
|
|
527
|
+
},
|
|
528
|
+
"endBinding": {
|
|
529
|
+
"elementId": "zKU82TpmIygY0Mi-qx8h_",
|
|
530
|
+
"focus": -0.1487326299751002,
|
|
531
|
+
"gap": 13.62890625
|
|
532
|
+
},
|
|
533
|
+
"startArrowhead": null,
|
|
534
|
+
"endArrowhead": "arrow",
|
|
535
|
+
"elbowed": false
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"id": "U7YLjSVWgy18bJF54ZIgv",
|
|
539
|
+
"type": "arrow",
|
|
540
|
+
"x": 821.6725157020725,
|
|
541
|
+
"y": 560.631867333575,
|
|
542
|
+
"width": 68.79617719586997,
|
|
543
|
+
"height": 49.2424623316391,
|
|
544
|
+
"angle": 0,
|
|
545
|
+
"strokeColor": "#1e1e1e",
|
|
546
|
+
"backgroundColor": "transparent",
|
|
547
|
+
"fillStyle": "solid",
|
|
548
|
+
"strokeWidth": 2,
|
|
549
|
+
"strokeStyle": "solid",
|
|
550
|
+
"roughness": 1,
|
|
551
|
+
"opacity": 100,
|
|
552
|
+
"groupIds": [],
|
|
553
|
+
"frameId": null,
|
|
554
|
+
"index": "aE",
|
|
555
|
+
"roundness": {
|
|
556
|
+
"type": 2
|
|
557
|
+
},
|
|
558
|
+
"seed": 1511279475,
|
|
559
|
+
"version": 52,
|
|
560
|
+
"versionNonce": 156892787,
|
|
561
|
+
"isDeleted": false,
|
|
562
|
+
"boundElements": [
|
|
563
|
+
{
|
|
564
|
+
"type": "text",
|
|
565
|
+
"id": "1ypiZG3E9f79on1xod-iq"
|
|
566
|
+
}
|
|
567
|
+
],
|
|
568
|
+
"updated": 1754681036034,
|
|
569
|
+
"link": null,
|
|
570
|
+
"locked": false,
|
|
571
|
+
"points": [
|
|
572
|
+
[
|
|
573
|
+
0,
|
|
574
|
+
0
|
|
575
|
+
],
|
|
576
|
+
[
|
|
577
|
+
-68.79617719586997,
|
|
578
|
+
-49.2424623316391
|
|
579
|
+
]
|
|
580
|
+
],
|
|
581
|
+
"lastCommittedPoint": null,
|
|
582
|
+
"startBinding": {
|
|
583
|
+
"elementId": "hV2YoJhzBqy1AFhCQtS8g",
|
|
584
|
+
"focus": -0.3709598325944409,
|
|
585
|
+
"gap": 2.8762612304445034
|
|
586
|
+
},
|
|
587
|
+
"endBinding": {
|
|
588
|
+
"elementId": "2ElIXzVKadL3xMuB_N3Sq",
|
|
589
|
+
"focus": -0.3619318362760949,
|
|
590
|
+
"gap": 14.022437059284249
|
|
591
|
+
},
|
|
592
|
+
"startArrowhead": null,
|
|
593
|
+
"endArrowhead": "arrow",
|
|
594
|
+
"elbowed": false
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"id": "1ypiZG3E9f79on1xod-iq",
|
|
598
|
+
"type": "text",
|
|
599
|
+
"x": 784.5248489379883,
|
|
600
|
+
"y": 522.84375,
|
|
601
|
+
"width": 4.8799896240234375,
|
|
602
|
+
"height": 25,
|
|
603
|
+
"angle": 0,
|
|
604
|
+
"strokeColor": "#1e1e1e",
|
|
605
|
+
"backgroundColor": "transparent",
|
|
606
|
+
"fillStyle": "solid",
|
|
607
|
+
"strokeWidth": 2,
|
|
608
|
+
"strokeStyle": "solid",
|
|
609
|
+
"roughness": 1,
|
|
610
|
+
"opacity": 100,
|
|
611
|
+
"groupIds": [],
|
|
612
|
+
"frameId": null,
|
|
613
|
+
"index": "aF",
|
|
614
|
+
"roundness": null,
|
|
615
|
+
"seed": 1437393331,
|
|
616
|
+
"version": 3,
|
|
617
|
+
"versionNonce": 56605331,
|
|
618
|
+
"isDeleted": false,
|
|
619
|
+
"boundElements": null,
|
|
620
|
+
"updated": 1754681027628,
|
|
621
|
+
"link": null,
|
|
622
|
+
"locked": false,
|
|
623
|
+
"text": "i",
|
|
624
|
+
"fontSize": 20,
|
|
625
|
+
"fontFamily": 5,
|
|
626
|
+
"textAlign": "center",
|
|
627
|
+
"verticalAlign": "middle",
|
|
628
|
+
"containerId": "U7YLjSVWgy18bJF54ZIgv",
|
|
629
|
+
"originalText": "i",
|
|
630
|
+
"autoResize": true,
|
|
631
|
+
"lineHeight": 1.25
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
"id": "IxnxNhbSOmTXq5ja1mSVj",
|
|
635
|
+
"type": "arrow",
|
|
636
|
+
"x": 567.2729560630362,
|
|
637
|
+
"y": 561.8194093598581,
|
|
638
|
+
"width": 57.09010671281408,
|
|
639
|
+
"height": 38.68367646340255,
|
|
640
|
+
"angle": 0,
|
|
641
|
+
"strokeColor": "#1e1e1e",
|
|
642
|
+
"backgroundColor": "transparent",
|
|
643
|
+
"fillStyle": "solid",
|
|
644
|
+
"strokeWidth": 2,
|
|
645
|
+
"strokeStyle": "solid",
|
|
646
|
+
"roughness": 1,
|
|
647
|
+
"opacity": 100,
|
|
648
|
+
"groupIds": [],
|
|
649
|
+
"frameId": null,
|
|
650
|
+
"index": "aG",
|
|
651
|
+
"roundness": {
|
|
652
|
+
"type": 2
|
|
653
|
+
},
|
|
654
|
+
"seed": 380405981,
|
|
655
|
+
"version": 55,
|
|
656
|
+
"versionNonce": 1567906259,
|
|
657
|
+
"isDeleted": false,
|
|
658
|
+
"boundElements": [
|
|
659
|
+
{
|
|
660
|
+
"type": "text",
|
|
661
|
+
"id": "QPaCdreTkf52nEzRe73Oz"
|
|
662
|
+
}
|
|
663
|
+
],
|
|
664
|
+
"updated": 1754681035334,
|
|
665
|
+
"link": null,
|
|
666
|
+
"locked": false,
|
|
667
|
+
"points": [
|
|
668
|
+
[
|
|
669
|
+
0,
|
|
670
|
+
0
|
|
671
|
+
],
|
|
672
|
+
[
|
|
673
|
+
57.09010671281408,
|
|
674
|
+
-38.68367646340255
|
|
675
|
+
]
|
|
676
|
+
],
|
|
677
|
+
"lastCommittedPoint": null,
|
|
678
|
+
"startBinding": {
|
|
679
|
+
"elementId": "zKU82TpmIygY0Mi-qx8h_",
|
|
680
|
+
"focus": 0.2606141310890022,
|
|
681
|
+
"gap": 10.402882082131438
|
|
682
|
+
},
|
|
683
|
+
"endBinding": {
|
|
684
|
+
"elementId": "2ElIXzVKadL3xMuB_N3Sq",
|
|
685
|
+
"focus": 0.13333072838251991,
|
|
686
|
+
"gap": 14.97915134738753
|
|
687
|
+
},
|
|
688
|
+
"startArrowhead": null,
|
|
689
|
+
"endArrowhead": "arrow",
|
|
690
|
+
"elbowed": false
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
"id": "QPaCdreTkf52nEzRe73Oz",
|
|
694
|
+
"type": "text",
|
|
695
|
+
"x": 594.4916458129883,
|
|
696
|
+
"y": 528.78515625,
|
|
697
|
+
"width": 4.8799896240234375,
|
|
698
|
+
"height": 25,
|
|
699
|
+
"angle": 0,
|
|
700
|
+
"strokeColor": "#1e1e1e",
|
|
701
|
+
"backgroundColor": "transparent",
|
|
702
|
+
"fillStyle": "solid",
|
|
703
|
+
"strokeWidth": 2,
|
|
704
|
+
"strokeStyle": "solid",
|
|
705
|
+
"roughness": 1,
|
|
706
|
+
"opacity": 100,
|
|
707
|
+
"groupIds": [],
|
|
708
|
+
"frameId": null,
|
|
709
|
+
"index": "aH",
|
|
710
|
+
"roundness": null,
|
|
711
|
+
"seed": 1968330163,
|
|
712
|
+
"version": 3,
|
|
713
|
+
"versionNonce": 1007267475,
|
|
714
|
+
"isDeleted": false,
|
|
715
|
+
"boundElements": null,
|
|
716
|
+
"updated": 1754681033502,
|
|
717
|
+
"link": null,
|
|
718
|
+
"locked": false,
|
|
719
|
+
"text": "i",
|
|
720
|
+
"fontSize": 20,
|
|
721
|
+
"fontFamily": 5,
|
|
722
|
+
"textAlign": "center",
|
|
723
|
+
"verticalAlign": "middle",
|
|
724
|
+
"containerId": "IxnxNhbSOmTXq5ja1mSVj",
|
|
725
|
+
"originalText": "i",
|
|
726
|
+
"autoResize": true,
|
|
727
|
+
"lineHeight": 1.25
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
"id": "lKO56RjY9cHbgbHykBlkJ",
|
|
731
|
+
"type": "line",
|
|
732
|
+
"x": 182.98046875,
|
|
733
|
+
"y": 354.23046875,
|
|
734
|
+
"width": 13.94921875,
|
|
735
|
+
"height": 392.828125,
|
|
736
|
+
"angle": 0,
|
|
737
|
+
"strokeColor": "#1e1e1e",
|
|
738
|
+
"backgroundColor": "transparent",
|
|
739
|
+
"fillStyle": "solid",
|
|
740
|
+
"strokeWidth": 2,
|
|
741
|
+
"strokeStyle": "solid",
|
|
742
|
+
"roughness": 1,
|
|
743
|
+
"opacity": 100,
|
|
744
|
+
"groupIds": [],
|
|
745
|
+
"frameId": null,
|
|
746
|
+
"index": "aJ",
|
|
747
|
+
"roundness": {
|
|
748
|
+
"type": 2
|
|
749
|
+
},
|
|
750
|
+
"seed": 637308787,
|
|
751
|
+
"version": 35,
|
|
752
|
+
"versionNonce": 1915980701,
|
|
753
|
+
"isDeleted": false,
|
|
754
|
+
"boundElements": null,
|
|
755
|
+
"updated": 1754681043943,
|
|
756
|
+
"link": null,
|
|
757
|
+
"locked": false,
|
|
758
|
+
"points": [
|
|
759
|
+
[
|
|
760
|
+
0,
|
|
761
|
+
0
|
|
762
|
+
],
|
|
763
|
+
[
|
|
764
|
+
13.94921875,
|
|
765
|
+
392.828125
|
|
766
|
+
]
|
|
767
|
+
],
|
|
768
|
+
"lastCommittedPoint": null,
|
|
769
|
+
"startBinding": null,
|
|
770
|
+
"endBinding": null,
|
|
771
|
+
"startArrowhead": null,
|
|
772
|
+
"endArrowhead": null
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
"id": "l7mubWsbCqajt3OheF5cK",
|
|
776
|
+
"type": "text",
|
|
777
|
+
"x": -76.84375,
|
|
778
|
+
"y": 375.33203125,
|
|
779
|
+
"width": 208.97984313964844,
|
|
780
|
+
"height": 25,
|
|
781
|
+
"angle": 0,
|
|
782
|
+
"strokeColor": "#1e1e1e",
|
|
783
|
+
"backgroundColor": "transparent",
|
|
784
|
+
"fillStyle": "solid",
|
|
785
|
+
"strokeWidth": 2,
|
|
786
|
+
"strokeStyle": "solid",
|
|
787
|
+
"roughness": 1,
|
|
788
|
+
"opacity": 100,
|
|
789
|
+
"groupIds": [],
|
|
790
|
+
"frameId": null,
|
|
791
|
+
"index": "aK",
|
|
792
|
+
"roundness": null,
|
|
793
|
+
"seed": 1878067293,
|
|
794
|
+
"version": 42,
|
|
795
|
+
"versionNonce": 1521645235,
|
|
796
|
+
"isDeleted": false,
|
|
797
|
+
"boundElements": null,
|
|
798
|
+
"updated": 1754681052002,
|
|
799
|
+
"link": null,
|
|
800
|
+
"locked": false,
|
|
801
|
+
"text": "Environment Variables",
|
|
802
|
+
"fontSize": 20,
|
|
803
|
+
"fontFamily": 5,
|
|
804
|
+
"textAlign": "left",
|
|
805
|
+
"verticalAlign": "top",
|
|
806
|
+
"containerId": null,
|
|
807
|
+
"originalText": "Environment Variables",
|
|
808
|
+
"autoResize": true,
|
|
809
|
+
"lineHeight": 1.25
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
"id": "iv8N0YohYV8J6G8rj-QsC",
|
|
813
|
+
"type": "text",
|
|
814
|
+
"x": -335.53515625,
|
|
815
|
+
"y": 449.0078125,
|
|
816
|
+
"width": 405.93975830078125,
|
|
817
|
+
"height": 150,
|
|
818
|
+
"angle": 0,
|
|
819
|
+
"strokeColor": "#1e1e1e",
|
|
820
|
+
"backgroundColor": "transparent",
|
|
821
|
+
"fillStyle": "solid",
|
|
822
|
+
"strokeWidth": 2,
|
|
823
|
+
"strokeStyle": "solid",
|
|
824
|
+
"roughness": 1,
|
|
825
|
+
"opacity": 100,
|
|
826
|
+
"groupIds": [],
|
|
827
|
+
"frameId": null,
|
|
828
|
+
"index": "aL",
|
|
829
|
+
"roundness": null,
|
|
830
|
+
"seed": 1286792915,
|
|
831
|
+
"version": 97,
|
|
832
|
+
"versionNonce": 647556573,
|
|
833
|
+
"isDeleted": false,
|
|
834
|
+
"boundElements": null,
|
|
835
|
+
"updated": 1754681097280,
|
|
836
|
+
"link": null,
|
|
837
|
+
"locked": false,
|
|
838
|
+
"text": "R2_ACCOUNT_ID=\nR2_ACCESS_KEY_ID=\nR2_SECRET_ACCESS_KEY=\nR2_BUCKET_NAME=polil..\nR2_ENDPOINT=https://.r2..com...\nR2_PUBLIC_DOMAIN=https://pu.r2.dev..",
|
|
839
|
+
"fontSize": 20,
|
|
840
|
+
"fontFamily": 5,
|
|
841
|
+
"textAlign": "left",
|
|
842
|
+
"verticalAlign": "top",
|
|
843
|
+
"containerId": null,
|
|
844
|
+
"originalText": "R2_ACCOUNT_ID=\nR2_ACCESS_KEY_ID=\nR2_SECRET_ACCESS_KEY=\nR2_BUCKET_NAME=polil..\nR2_ENDPOINT=https://.r2..com...\nR2_PUBLIC_DOMAIN=https://pu.r2.dev..",
|
|
845
|
+
"autoResize": true,
|
|
846
|
+
"lineHeight": 1.25
|
|
847
|
+
}
|
|
848
|
+
],
|
|
849
|
+
"appState": {
|
|
850
|
+
"gridSize": 20,
|
|
851
|
+
"gridStep": 5,
|
|
852
|
+
"gridModeEnabled": false,
|
|
853
|
+
"viewBackgroundColor": "#ffffff"
|
|
854
|
+
},
|
|
855
|
+
"files": {}
|
|
856
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Storage Provider Configuration
|
|
2
|
+
|
|
3
|
+
This document explains how to configure the storage provider for the application. The system is designed to be flexible, allowing you to switch between different storage services by changing an environment variable.
|
|
4
|
+
|
|
5
|
+
## Supported Providers
|
|
6
|
+
|
|
7
|
+
- `GOOGLE`: Uses Google Cloud Storage.
|
|
8
|
+
- `CLOUDFLARE`: Uses Cloudflare R2.
|
|
9
|
+
- `LOCAL`: Uses the server's local file system.
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
To select a storage provider, set the `STORAGE_PROVIDER` environment variable in your `.env` file.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
STORAGE_PROVIDER=GOOGLE
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
STORAGE_PROVIDER=CLOUDFLARE
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If `STORAGE_PROVIDER` is not set, the system will default to `GOOGLE`.
|
|
26
|
+
|
|
27
|
+
### Google Cloud Storage
|
|
28
|
+
|
|
29
|
+
No additional environment variables are required if you have the Google Cloud SDK configured on your machine. The service uses Application Default Credentials.
|
|
30
|
+
|
|
31
|
+
### Cloudflare R2
|
|
32
|
+
|
|
33
|
+
To use Cloudflare R2, you must provide the following environment variables:
|
|
34
|
+
|
|
35
|
+
- `CLOUDFLARE_ACCOUNT_ID`: Your Cloudflare account ID.
|
|
36
|
+
- `CLOUDFLARE_R2_ACCESS_KEY_ID`: Your R2 access key ID.
|
|
37
|
+
- `CLOUDFLARE_R2_SECRET_ACCESS_KEY`: Your R2 secret access key.
|
|
38
|
+
- `STORAGE_BUCKET`: The name of your R2 bucket.
|
|
39
|
+
- `R2_PUBLIC_DOMAIN`: The public domain for your R2 bucket, if you have one configured.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
CLOUDFLARE_ACCOUNT_ID=...
|
|
45
|
+
CLOUDFLARE_R2_ACCESS_KEY_ID=...
|
|
46
|
+
CLOUDFLARE_R2_SECRET_ACCESS_KEY=...
|
|
47
|
+
STORAGE_BUCKET=my-r2-bucket
|
|
48
|
+
R2_PUBLIC_DOMAIN=https://my-public-bucket.example.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Local Storage
|
|
52
|
+
|
|
53
|
+
To use local storage, set the following environment variables:
|
|
54
|
+
|
|
55
|
+
- `LOCAL_STORAGE_PATH`: The absolute or relative path to the storage directory (defaults to `./uploads`).
|
|
56
|
+
- `LOCAL_STORAGE_BASE_URL`: The base URL used to construct public links (e.g., `http://localhost:3000/storage/files`).
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
LOCAL_STORAGE_PATH=./uploads
|
|
62
|
+
LOCAL_STORAGE_BASE_URL=http://localhost:3000/storage/files
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How to Use
|
|
66
|
+
|
|
67
|
+
In your services, you can inject the storage service using the `IStorageService` token:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
71
|
+
import { IStorageService } from '@polilan/nest-storage';
|
|
72
|
+
|
|
73
|
+
@Injectable()
|
|
74
|
+
export class MyService {
|
|
75
|
+
constructor(@Inject('IStorageService') private readonly storageService: IStorageService) {}
|
|
76
|
+
|
|
77
|
+
async uploadMyFile(file: Buffer) {
|
|
78
|
+
return this.storageService.uploadFile('my-bucket', 'my-file.txt', file);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Universal Storage Integration
|
|
2
|
+
|
|
3
|
+
The `@polilan/nest-storage` library has been updated to support a "Universal" storage flow, allowing the backend to act as a proxy for file uploads. This is particularly useful for supporting local storage or when you want to centralize storage logic in the backend.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The system uses a `StorageFactory` to return the appropriate `IStorageService` implementation based on the `STORAGE_PROVIDER` environment variable or a per-request override.
|
|
8
|
+
|
|
9
|
+
### Supported Providers
|
|
10
|
+
- `GOOGLE`: Google Cloud Storage
|
|
11
|
+
- `CLOUDFLARE`: Cloudflare R2
|
|
12
|
+
- `LOCAL`: Local File System (New)
|
|
13
|
+
|
|
14
|
+
## API Endpoints
|
|
15
|
+
|
|
16
|
+
The library now includes a `StorageController` that provides the following endpoints:
|
|
17
|
+
|
|
18
|
+
### 1. Upload File
|
|
19
|
+
`POST /storage/upload`
|
|
20
|
+
|
|
21
|
+
**Body (Multipart Form Data):**
|
|
22
|
+
- `file`: The file to upload.
|
|
23
|
+
- `path` (optional): The target storage path (e.g., `avatars/user-1`).
|
|
24
|
+
- `provider` (optional): Override the default provider (e.g., `LOCAL`).
|
|
25
|
+
|
|
26
|
+
**Behavior:**
|
|
27
|
+
- If the file is an image, it is automatically converted to **WebP** format using Sharp.
|
|
28
|
+
- Returns a `CloudFileStorage` object containing the file URL and metadata.
|
|
29
|
+
|
|
30
|
+
### 2. Serve Local Files
|
|
31
|
+
`GET /storage/files/:filename(*)`
|
|
32
|
+
|
|
33
|
+
**Behavior:**
|
|
34
|
+
- Serves files stored locally in the `LOCAL_STORAGE_PATH` directory.
|
|
35
|
+
- This endpoint is used when `STORAGE_PROVIDER=LOCAL` is active.
|
|
36
|
+
|
|
37
|
+
## Local Storage Configuration
|
|
38
|
+
|
|
39
|
+
To use local storage, add the following to your `.env`:
|
|
40
|
+
|
|
41
|
+
```env
|
|
42
|
+
STORAGE_PROVIDER=LOCAL
|
|
43
|
+
LOCAL_STORAGE_PATH=./uploads
|
|
44
|
+
LOCAL_STORAGE_BASE_URL=http://localhost:3000/storage/files
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `LOCAL_STORAGE_PATH`: The directory where files will be saved on the server.
|
|
48
|
+
- `LOCAL_STORAGE_BASE_URL`: The URL used to access these files from the frontend (should point to the `/storage/files` endpoint).
|
package/docs/usage.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Usage Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to use the `@polilan/nest-storage` library to manage files in your application.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
To use the storage service, you first need to configure the provider as described in the [Storage Providers](./storage-providers.md) documentation.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Then, you can inject the `IStorageService` into your controllers or services:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
14
|
+
import { IStorageService } from '@polilan/nest-storage';
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class MyService {
|
|
18
|
+
constructor(
|
|
19
|
+
@Inject('IStorageService') private readonly storageService: IStorageService
|
|
20
|
+
) {}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Saving Files to Storage
|
|
25
|
+
|
|
26
|
+
The library provides several methods to save files, depending on your needs.
|
|
27
|
+
|
|
28
|
+
### 1. General File Upload
|
|
29
|
+
|
|
30
|
+
Use `uploadFile` for standard file uploads. This method requires an `auditable` object for tracking.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const fileBuffer = Buffer.from('Hello World');
|
|
34
|
+
const fileName = 'documents/hello.txt';
|
|
35
|
+
const auditable = { createdBy: 'user-id', updatedBy: 'user-id' };
|
|
36
|
+
|
|
37
|
+
const result = await this.storageService.uploadFile(
|
|
38
|
+
fileName,
|
|
39
|
+
fileBuffer,
|
|
40
|
+
auditable,
|
|
41
|
+
'text/plain'
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
console.log('File URL:', result.url);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Public File Upload
|
|
48
|
+
|
|
49
|
+
Use `uploadPublicFile` when you want to upload a file and ensure it's publicly accessible.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const result = await this.storageService.uploadPublicFile({
|
|
53
|
+
fileName: 'public/avatar.png',
|
|
54
|
+
fileBuffer: imageBuffer,
|
|
55
|
+
contentType: 'image/png',
|
|
56
|
+
metadata: { userId: '123' }
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. WebP Image Upload
|
|
61
|
+
|
|
62
|
+
Use `uploadWebpImage` to automatically convert an image to WebP and resize it before uploading. This is recommended for web assets.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const result = await this.storageService.uploadWebpImage({
|
|
66
|
+
fileName: 'gallery/photo.jpg',
|
|
67
|
+
imageBuffer: largeImageBuffer,
|
|
68
|
+
resizeWidth: 800, // Optional, defaults to 900
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Deleting From Storage
|
|
73
|
+
|
|
74
|
+
### 1. Delete a Single File
|
|
75
|
+
|
|
76
|
+
Use `deleteStorageFile` by providing the bucket and the path (file name).
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
await this.storageService.deleteStorageFile('my-bucket', 'documents/hello.txt');
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Delete All Files in an Object
|
|
83
|
+
|
|
84
|
+
If you have a complex object that contains paths to several files, you can remove them all at once.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const myEntity = {
|
|
88
|
+
name: 'Project Alpha',
|
|
89
|
+
files: [
|
|
90
|
+
{ path: 'file1.pdf', bucket: 'my-bucket' },
|
|
91
|
+
{ path: 'file2.jpg', bucket: 'my-bucket' }
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await this.storageService.removeAllStorageFilesPresentInObject(myEntity);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Delete by URL
|
|
99
|
+
|
|
100
|
+
If you only have the public URLs of the files, use `removeAllStorageFilesByUrl`.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const urls = [
|
|
104
|
+
'https://storage.googleapis.com/my-bucket/file1.pdf',
|
|
105
|
+
'https://storage.googleapis.com/my-bucket/file2.jpg'
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
await this.storageService.removeAllStorageFilesByUrl({ urls });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## HTTP API Endpoints
|
|
112
|
+
|
|
113
|
+
The library includes a `StorageController` that exposes endpoints for file management via HTTP.
|
|
114
|
+
|
|
115
|
+
### 1. Upload a Single File
|
|
116
|
+
|
|
117
|
+
- **Endpoint**: `POST /storage/upload`
|
|
118
|
+
- **Body**: `multipart/form-data` with a `file` field.
|
|
119
|
+
- **Query Params**: `path`, `provider`, `keepOriginalName`.
|
|
120
|
+
|
|
121
|
+
**Example (cURL):**
|
|
122
|
+
```bash
|
|
123
|
+
curl -X POST "http://localhost:8091/storage/upload?path=my-folder&keepOriginalName=true" \
|
|
124
|
+
-F "file=@/path/to/your/image.png"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2. Upload Multiple Files
|
|
128
|
+
|
|
129
|
+
- **Endpoint**: `POST /storage/upload-multiple`
|
|
130
|
+
- **Body**: `multipart/form-data` with multiple `file` fields (or multiple fields named `file`).
|
|
131
|
+
- **Query Params**: `path`, `provider`, `keepOriginalName`.
|
|
132
|
+
|
|
133
|
+
**Example (cURL):**
|
|
134
|
+
```bash
|
|
135
|
+
curl -X POST "http://localhost:8091/storage/upload-multiple?path=batch-upload" \
|
|
136
|
+
-F "file=@image1.png" \
|
|
137
|
+
-F "file=@image2.png"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Other Utility Methods
|
|
141
|
+
|
|
142
|
+
- **`getFile(bucket, path)`**: Downloads a file and returns it as a Buffer.
|
|
143
|
+
- **`fileExists(bucket, path)`**: Checks if a file exists.
|
|
144
|
+
- **`generateSignedUrl(bucket, path, expires)`**: Generates a temporary URL for private files.
|
|
145
|
+
- **`listFiles(bucket, prefix)`**: Lists files in a bucket, optionally filtered by prefix.
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataclouder/nest-storage",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "NestJS library for storage services",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"**/*.js",
|
|
9
9
|
"**/*.d.ts",
|
|
10
|
-
"**/*.js.map"
|
|
10
|
+
"**/*.js.map",
|
|
11
|
+
"README.md",
|
|
12
|
+
"docs"
|
|
11
13
|
],
|
|
12
14
|
"scripts": {
|
|
13
15
|
"build": "tsc -p tsconfig.lib.json",
|