@lenne.tech/nest-server 11.7.3 → 11.8.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/dist/core/common/interfaces/server-options.interface.d.ts +22 -0
- package/dist/core/modules/file/core-file.controller.d.ts +1 -0
- package/dist/core/modules/file/core-file.controller.js +22 -0
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.controller.d.ts +9 -0
- package/dist/core/modules/tus/core-tus.controller.js +85 -0
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -0
- package/dist/core/modules/tus/core-tus.service.d.ts +30 -0
- package/dist/core/modules/tus/core-tus.service.js +284 -0
- package/dist/core/modules/tus/core-tus.service.js.map +1 -0
- package/dist/core/modules/tus/index.d.ts +4 -0
- package/dist/core/modules/tus/index.js +21 -0
- package/dist/core/modules/tus/index.js.map +1 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.d.ts +10 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +59 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +21 -0
- package/dist/core/modules/tus/tus.module.js +99 -0
- package/dist/core/modules/tus/tus.module.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/file/file.controller.d.ts +5 -7
- package/dist/server/modules/file/file.controller.js +3 -31
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/server.module.js +3 -1
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -1
- package/src/core/common/interfaces/server-options.interface.ts +154 -0
- package/src/core/modules/file/README.md +165 -0
- package/src/core/modules/file/core-file.controller.ts +27 -1
- package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +176 -0
- package/src/core/modules/tus/README.md +439 -0
- package/src/core/modules/tus/core-tus.controller.ts +88 -0
- package/src/core/modules/tus/core-tus.service.ts +424 -0
- package/src/core/modules/tus/index.ts +5 -0
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +107 -0
- package/src/core/modules/tus/tus.module.ts +187 -0
- package/src/index.ts +6 -0
- package/src/server/modules/file/file.controller.ts +14 -34
- package/src/server/server.module.ts +5 -1
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { FileStore } from '@tus/file-store';
|
|
3
|
+
import { Server, Upload } from '@tus/server';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { Connection, mongo } from 'mongoose';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
import { GridFSHelper } from '../../common/helpers/gridfs.helper';
|
|
9
|
+
import { ITusConfig } from '../../common/interfaces/server-options.interface';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_TUS_ALLOWED_HEADERS,
|
|
12
|
+
DEFAULT_TUS_CONFIG,
|
|
13
|
+
normalizeTusConfig,
|
|
14
|
+
parseExpirationTime,
|
|
15
|
+
} from './interfaces/tus-config.interface';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Core TUS Service
|
|
19
|
+
*
|
|
20
|
+
* Provides integration with @tus/server for resumable file uploads.
|
|
21
|
+
* After upload completion, files are migrated to GridFS and a File entity is created.
|
|
22
|
+
*
|
|
23
|
+
* This service follows the Module Inheritance Pattern and can be extended in projects.
|
|
24
|
+
*/
|
|
25
|
+
@Injectable()
|
|
26
|
+
export class CoreTusService implements OnModuleDestroy, OnModuleInit {
|
|
27
|
+
private readonly logger = new Logger(CoreTusService.name);
|
|
28
|
+
private tusServer: null | Server = null;
|
|
29
|
+
private config: ITusConfig;
|
|
30
|
+
private files: mongo.GridFSBucket;
|
|
31
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
32
|
+
|
|
33
|
+
constructor(private readonly connection: Connection) {
|
|
34
|
+
// Initialize with defaults - will be configured in onModuleInit or via configure()
|
|
35
|
+
this.config = { ...DEFAULT_TUS_CONFIG };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configure the TUS service
|
|
40
|
+
* Called by TusModule.forRoot() with the resolved configuration
|
|
41
|
+
*/
|
|
42
|
+
configure(config: boolean | ITusConfig | undefined): void {
|
|
43
|
+
const normalizedConfig = normalizeTusConfig(config);
|
|
44
|
+
if (normalizedConfig) {
|
|
45
|
+
this.config = normalizedConfig;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async onModuleInit(): Promise<void> {
|
|
50
|
+
if (!this.config.enabled) {
|
|
51
|
+
this.logger.debug('TUS uploads disabled');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Initialize GridFS bucket
|
|
56
|
+
this.files = new mongo.GridFSBucket(this.connection.db, { bucketName: 'fs' });
|
|
57
|
+
|
|
58
|
+
// Ensure upload directory exists
|
|
59
|
+
const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
|
|
60
|
+
await this.ensureUploadDir(uploadDir);
|
|
61
|
+
|
|
62
|
+
// Create TUS server instance
|
|
63
|
+
this.tusServer = this.createTusServer(uploadDir);
|
|
64
|
+
|
|
65
|
+
// Setup expiration cleanup if enabled
|
|
66
|
+
this.setupExpirationCleanup();
|
|
67
|
+
|
|
68
|
+
this.logger.log(`TUS server initialized at ${this.config.path}`);
|
|
69
|
+
this.logEnabledFeatures();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async onModuleDestroy(): Promise<void> {
|
|
73
|
+
if (this.cleanupInterval) {
|
|
74
|
+
clearInterval(this.cleanupInterval);
|
|
75
|
+
this.cleanupInterval = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the TUS server instance
|
|
81
|
+
*/
|
|
82
|
+
getServer(): null | Server {
|
|
83
|
+
return this.tusServer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the current configuration
|
|
88
|
+
*/
|
|
89
|
+
getConfig(): ITusConfig {
|
|
90
|
+
return this.config;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if TUS is enabled
|
|
95
|
+
*/
|
|
96
|
+
isEnabled(): boolean {
|
|
97
|
+
return this.config.enabled !== false && this.tusServer !== null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the configured path
|
|
102
|
+
*/
|
|
103
|
+
getPath(): string {
|
|
104
|
+
return this.config.path || DEFAULT_TUS_CONFIG.path;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle upload completion - migrate to GridFS
|
|
109
|
+
*
|
|
110
|
+
* This method can be overridden in extending services to customize
|
|
111
|
+
* what happens after an upload completes.
|
|
112
|
+
*/
|
|
113
|
+
protected async onUploadComplete(upload: Upload): Promise<void> {
|
|
114
|
+
const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
|
|
115
|
+
const filePath = path.join(uploadDir, upload.id);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Extract metadata
|
|
119
|
+
const metadata = this.parseMetadata(upload.metadata);
|
|
120
|
+
const filename = metadata.filename || upload.id;
|
|
121
|
+
const contentType = metadata.filetype || 'application/octet-stream';
|
|
122
|
+
|
|
123
|
+
// Check if file exists
|
|
124
|
+
const fileExists = await fs.promises
|
|
125
|
+
.access(filePath)
|
|
126
|
+
.then(() => true)
|
|
127
|
+
.catch(() => false);
|
|
128
|
+
if (!fileExists) {
|
|
129
|
+
this.logger.warn(`Upload file not found at ${filePath}, skipping GridFS migration`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Read the completed file and upload to GridFS
|
|
134
|
+
const readStream = fs.createReadStream(filePath);
|
|
135
|
+
const fileInfo = await GridFSHelper.writeFileFromStream(this.files, readStream, {
|
|
136
|
+
contentType,
|
|
137
|
+
filename,
|
|
138
|
+
metadata: {
|
|
139
|
+
originalMetadata: metadata,
|
|
140
|
+
tusUploadId: upload.id,
|
|
141
|
+
uploadedAt: new Date(),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.logger.debug(`Upload ${upload.id} migrated to GridFS as ${fileInfo._id} (filename: ${filename})`);
|
|
146
|
+
|
|
147
|
+
// Clean up the temporary file
|
|
148
|
+
await this.deleteTemporaryFile(upload.id);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
this.logger.error(`Failed to migrate upload ${upload.id} to GridFS: ${error.message}`);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handle upload termination (deletion)
|
|
157
|
+
*/
|
|
158
|
+
protected async onUploadTerminate(upload: Upload): Promise<void> {
|
|
159
|
+
this.logger.debug(`Upload ${upload.id} terminated`);
|
|
160
|
+
await this.deleteTemporaryFile(upload.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validate file type against allowedTypes configuration
|
|
165
|
+
*
|
|
166
|
+
* This method can be overridden in extending services to customize
|
|
167
|
+
* file type validation logic.
|
|
168
|
+
*
|
|
169
|
+
* @param filetype - The MIME type from upload metadata
|
|
170
|
+
* @returns true if allowed, false if rejected
|
|
171
|
+
*/
|
|
172
|
+
protected validateFileType(filetype: string | undefined): boolean {
|
|
173
|
+
const allowedTypes = this.config.allowedTypes;
|
|
174
|
+
|
|
175
|
+
// If no restrictions configured, allow all types
|
|
176
|
+
if (!allowedTypes || allowedTypes.length === 0) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If no filetype provided in metadata, reject when restrictions exist
|
|
181
|
+
if (!filetype) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check exact match
|
|
186
|
+
if (allowedTypes.includes(filetype)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check wildcard patterns (e.g., 'image/*')
|
|
191
|
+
for (const allowed of allowedTypes) {
|
|
192
|
+
if (allowed.endsWith('/*')) {
|
|
193
|
+
const prefix = allowed.slice(0, -1); // 'image/*' -> 'image/'
|
|
194
|
+
if (filetype.startsWith(prefix)) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create the TUS server instance with configured extensions
|
|
205
|
+
*/
|
|
206
|
+
private createTusServer(uploadDir: string): Server {
|
|
207
|
+
const datastore = new FileStore({ directory: uploadDir });
|
|
208
|
+
|
|
209
|
+
const server = new Server({
|
|
210
|
+
allowedHeaders: this.config.allowedHeaders || DEFAULT_TUS_ALLOWED_HEADERS,
|
|
211
|
+
datastore,
|
|
212
|
+
maxSize: this.config.maxSize,
|
|
213
|
+
onUploadCreate: async (_req, upload) => {
|
|
214
|
+
// Validate file type if allowedTypes is configured
|
|
215
|
+
if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
|
|
216
|
+
const metadata = this.parseMetadata(upload.metadata);
|
|
217
|
+
const filetype = metadata.filetype;
|
|
218
|
+
|
|
219
|
+
if (!this.validateFileType(filetype)) {
|
|
220
|
+
const allowedList = this.config.allowedTypes.join(', ');
|
|
221
|
+
this.logger.warn(
|
|
222
|
+
`Upload rejected: file type '${filetype || 'unknown'}' not allowed. Allowed types: ${allowedList}`,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Throw error to reject the upload
|
|
226
|
+
// @tus/server v2 expects throwing an error with status_code
|
|
227
|
+
const error = new Error(
|
|
228
|
+
`File type '${filetype || 'unknown'}' is not allowed. Allowed types: ${allowedList}`,
|
|
229
|
+
);
|
|
230
|
+
(error as any).status_code = 415; // Unsupported Media Type
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Return empty object to proceed with upload
|
|
236
|
+
return {};
|
|
237
|
+
},
|
|
238
|
+
onUploadFinish: async (_req, upload) => {
|
|
239
|
+
try {
|
|
240
|
+
await this.onUploadComplete(upload);
|
|
241
|
+
return {};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.logger.error(`Upload finish error: ${error.message}`);
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
path: this.config.path || DEFAULT_TUS_CONFIG.path,
|
|
248
|
+
respectForwardedHeaders: true,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return server;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parse TUS metadata into object
|
|
256
|
+
* @tus/server v2 already parses metadata into an object
|
|
257
|
+
*/
|
|
258
|
+
private parseMetadata(metadata: Record<string, string> | string | undefined): Record<string, string> {
|
|
259
|
+
if (!metadata) {
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// @tus/server v2 returns metadata as an object
|
|
264
|
+
if (typeof metadata === 'object') {
|
|
265
|
+
return metadata;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fallback for string format (legacy or raw)
|
|
269
|
+
const result: Record<string, string> = {};
|
|
270
|
+
const pairs = metadata.split(',');
|
|
271
|
+
|
|
272
|
+
for (const pair of pairs) {
|
|
273
|
+
const [key, value] = pair.trim().split(' ');
|
|
274
|
+
if (key) {
|
|
275
|
+
// Decode base64 value if present
|
|
276
|
+
result[key] = value ? Buffer.from(value, 'base64').toString('utf-8') : '';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Ensure the upload directory exists
|
|
285
|
+
*/
|
|
286
|
+
private async ensureUploadDir(uploadDir: string): Promise<void> {
|
|
287
|
+
try {
|
|
288
|
+
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (error.code !== 'EEXIST') {
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Delete a temporary upload file
|
|
298
|
+
*/
|
|
299
|
+
private async deleteTemporaryFile(uploadId: string): Promise<void> {
|
|
300
|
+
const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
|
|
301
|
+
const filePath = path.join(uploadDir, uploadId);
|
|
302
|
+
const infoPath = `${filePath}.json`;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await fs.promises.unlink(filePath);
|
|
306
|
+
} catch {
|
|
307
|
+
// Ignore if file doesn't exist
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
await fs.promises.unlink(infoPath);
|
|
312
|
+
} catch {
|
|
313
|
+
// Ignore if info file doesn't exist
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Setup periodic cleanup of expired uploads
|
|
319
|
+
*/
|
|
320
|
+
private setupExpirationCleanup(): void {
|
|
321
|
+
const expirationConfig = this.config.expiration;
|
|
322
|
+
// Handle boolean | ITusExpirationConfig type
|
|
323
|
+
if (!expirationConfig) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let expiresIn = '24h';
|
|
328
|
+
|
|
329
|
+
// If explicitly set to false, skip
|
|
330
|
+
if (typeof expirationConfig === 'boolean') {
|
|
331
|
+
if (!expirationConfig) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// If true, use defaults (expiresIn already set to '24h')
|
|
335
|
+
} else {
|
|
336
|
+
// ITusExpirationConfig object
|
|
337
|
+
if (expirationConfig.enabled === false) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
expiresIn = expirationConfig.expiresIn || '24h';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const expirationMs = parseExpirationTime(expiresIn);
|
|
344
|
+
|
|
345
|
+
// Run cleanup every hour
|
|
346
|
+
this.cleanupInterval = setInterval(
|
|
347
|
+
async () => {
|
|
348
|
+
await this.cleanupExpiredUploads(expirationMs);
|
|
349
|
+
},
|
|
350
|
+
60 * 60 * 1000,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
this.logger.debug(`Expiration cleanup scheduled (expire after ${expiresIn})`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Clean up expired incomplete uploads
|
|
358
|
+
*/
|
|
359
|
+
private async cleanupExpiredUploads(maxAgeMs: number): Promise<void> {
|
|
360
|
+
const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const files = await fs.promises.readdir(uploadDir);
|
|
365
|
+
let cleanedCount = 0;
|
|
366
|
+
|
|
367
|
+
for (const file of files) {
|
|
368
|
+
if (file.endsWith('.json')) {
|
|
369
|
+
continue; // Skip info files
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const filePath = path.join(uploadDir, file);
|
|
373
|
+
const stats = await fs.promises.stat(filePath);
|
|
374
|
+
const age = now - stats.mtimeMs;
|
|
375
|
+
|
|
376
|
+
if (age > maxAgeMs) {
|
|
377
|
+
await this.deleteTemporaryFile(file);
|
|
378
|
+
cleanedCount++;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (cleanedCount > 0) {
|
|
383
|
+
this.logger.debug(`Cleaned up ${cleanedCount} expired uploads`);
|
|
384
|
+
}
|
|
385
|
+
} catch (error) {
|
|
386
|
+
this.logger.warn(`Failed to cleanup expired uploads: ${error.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Log which features are enabled
|
|
392
|
+
*/
|
|
393
|
+
private logEnabledFeatures(): void {
|
|
394
|
+
const features: string[] = [];
|
|
395
|
+
|
|
396
|
+
if (this.config.creation !== false) {
|
|
397
|
+
features.push('creation');
|
|
398
|
+
}
|
|
399
|
+
if (this.config.creationWithUpload !== false) {
|
|
400
|
+
features.push('creation-with-upload');
|
|
401
|
+
}
|
|
402
|
+
if (this.config.termination !== false) {
|
|
403
|
+
features.push('termination');
|
|
404
|
+
}
|
|
405
|
+
if (this.config.expiration !== false) {
|
|
406
|
+
features.push('expiration');
|
|
407
|
+
}
|
|
408
|
+
if (this.config.checksum !== false) {
|
|
409
|
+
features.push('checksum');
|
|
410
|
+
}
|
|
411
|
+
if (this.config.concatenation !== false) {
|
|
412
|
+
features.push('concatenation');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (features.length > 0) {
|
|
416
|
+
this.logger.log(`TUS extensions: ${features.join(', ')}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Log file type restrictions if configured
|
|
420
|
+
if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
|
|
421
|
+
this.logger.log(`TUS allowed types: ${this.config.allowedTypes.join(', ')}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUS Configuration Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* The interfaces are defined in server-options.interface.ts to avoid circular imports.
|
|
5
|
+
* This file contains helper functions and defaults.
|
|
6
|
+
*/
|
|
7
|
+
import { ITusConfig, ITusExpirationConfig } from '../../../common/interfaces/server-options.interface';
|
|
8
|
+
|
|
9
|
+
// Re-export for convenience
|
|
10
|
+
export {
|
|
11
|
+
ITusConfig,
|
|
12
|
+
ITusCreationConfig,
|
|
13
|
+
ITusExpirationConfig,
|
|
14
|
+
} from '../../../common/interfaces/server-options.interface';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Additional allowed headers for TUS requests (beyond @tus/server defaults).
|
|
18
|
+
*
|
|
19
|
+
* Note: @tus/server already includes all TUS protocol headers by default:
|
|
20
|
+
* Authorization, Content-Type, Location, Tus-Extension, Tus-Max-Size,
|
|
21
|
+
* Tus-Resumable, Tus-Version, Upload-Concat, Upload-Defer-Length,
|
|
22
|
+
* Upload-Length, Upload-Metadata, Upload-Offset, X-HTTP-Method-Override,
|
|
23
|
+
* X-Requested-With, X-Forwarded-Host, X-Forwarded-Proto, Forwarded
|
|
24
|
+
*
|
|
25
|
+
* This array is only for PROJECT-SPECIFIC additional headers.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_TUS_ALLOWED_HEADERS: string[] = [];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default TUS configuration
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_TUS_CONFIG: Required<
|
|
33
|
+
Omit<ITusConfig, 'allowedTypes' | 'creation' | 'expiration'> & {
|
|
34
|
+
allowedTypes: undefined;
|
|
35
|
+
creation: boolean;
|
|
36
|
+
expiration: ITusExpirationConfig;
|
|
37
|
+
}
|
|
38
|
+
> = {
|
|
39
|
+
allowedHeaders: DEFAULT_TUS_ALLOWED_HEADERS,
|
|
40
|
+
allowedTypes: undefined,
|
|
41
|
+
checksum: true,
|
|
42
|
+
concatenation: true,
|
|
43
|
+
creation: true,
|
|
44
|
+
creationWithUpload: true,
|
|
45
|
+
enabled: true,
|
|
46
|
+
expiration: { enabled: true, expiresIn: '24h' },
|
|
47
|
+
maxSize: 50 * 1024 * 1024 * 1024, // 50 GB
|
|
48
|
+
path: '/tus',
|
|
49
|
+
termination: true,
|
|
50
|
+
uploadDir: 'uploads/tus',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalizes tus config from various input formats
|
|
55
|
+
* - `undefined` → enabled with defaults (new behavior: enabled by default)
|
|
56
|
+
* - `true` → enabled with defaults
|
|
57
|
+
* - `false` → disabled
|
|
58
|
+
* - `{ ... }` → enabled with custom config
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeTusConfig(config: boolean | ITusConfig | undefined): ITusConfig | null {
|
|
61
|
+
// Enabled by default if not specified
|
|
62
|
+
if (config === undefined) {
|
|
63
|
+
return { ...DEFAULT_TUS_CONFIG };
|
|
64
|
+
}
|
|
65
|
+
if (config === true) {
|
|
66
|
+
return { ...DEFAULT_TUS_CONFIG };
|
|
67
|
+
}
|
|
68
|
+
if (config === false) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Check explicit enabled: false
|
|
72
|
+
if (config.enabled === false) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
// Merge with defaults
|
|
76
|
+
return {
|
|
77
|
+
...DEFAULT_TUS_CONFIG,
|
|
78
|
+
...config,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse expiration time string to milliseconds
|
|
84
|
+
* Supports: '24h', '1d', '12h', '30m', etc.
|
|
85
|
+
*/
|
|
86
|
+
export function parseExpirationTime(expiresIn: string): number {
|
|
87
|
+
const match = expiresIn.match(/^(\d+)([hdms])$/);
|
|
88
|
+
if (!match) {
|
|
89
|
+
return 24 * 60 * 60 * 1000; // Default: 24 hours
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const value = parseInt(match[1], 10);
|
|
93
|
+
const unit = match[2];
|
|
94
|
+
|
|
95
|
+
switch (unit) {
|
|
96
|
+
case 'd':
|
|
97
|
+
return value * 24 * 60 * 60 * 1000;
|
|
98
|
+
case 'h':
|
|
99
|
+
return value * 60 * 60 * 1000;
|
|
100
|
+
case 'm':
|
|
101
|
+
return value * 60 * 1000;
|
|
102
|
+
case 's':
|
|
103
|
+
return value * 1000;
|
|
104
|
+
default:
|
|
105
|
+
return 24 * 60 * 60 * 1000;
|
|
106
|
+
}
|
|
107
|
+
}
|