@devstroupe/devkit-nest 1.1.14 → 1.1.16

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.
@@ -1,5 +1,5 @@
1
1
  import { StorageService } from '../storage.service';
2
- import { StorageFile, UploadOptions, UploadResult, LocalStorageOptions } from '../storage.types';
2
+ import { StorageFile, UploadOptions, UploadResult, LocalStorageOptions, StorageDownloadResult } from '../storage.types';
3
3
  export declare class LocalStorageService implements StorageService {
4
4
  private readonly storagePath;
5
5
  private readonly baseUrl;
@@ -8,4 +8,5 @@ export declare class LocalStorageService implements StorageService {
8
8
  delete(key: string): Promise<void>;
9
9
  getUrl(key: string): Promise<string>;
10
10
  getSignedUrl(key: string, expiresIn?: number): Promise<string>;
11
+ download(key: string): Promise<StorageDownloadResult>;
11
12
  }
@@ -106,6 +106,24 @@ let LocalStorageService = class LocalStorageService {
106
106
  async getSignedUrl(key, expiresIn) {
107
107
  return this.getUrl(key);
108
108
  }
109
+ async download(key) {
110
+ const targetFile = path.resolve(this.storagePath, key);
111
+ const rootPrefix = this.storagePath.endsWith(path.sep) ? this.storagePath : this.storagePath + path.sep;
112
+ // Mitigação Directory Traversal
113
+ if (!targetFile.startsWith(rootPrefix)) {
114
+ throw new Error('Directory traversal attempt detected');
115
+ }
116
+ try {
117
+ await fs.stat(targetFile);
118
+ }
119
+ catch {
120
+ throw new Error('File not found');
121
+ }
122
+ return {
123
+ type: 'file',
124
+ filePath: targetFile,
125
+ };
126
+ }
109
127
  };
110
128
  exports.LocalStorageService = LocalStorageService;
111
129
  exports.LocalStorageService = LocalStorageService = __decorate([
@@ -103,4 +103,28 @@ const local_storage_1 = require("./local.storage");
103
103
  await service.delete('../../../etc/passwd');
104
104
  }, /Directory traversal attempt detected/);
105
105
  });
106
+ (0, node_test_1.test)('deve realizar download de arquivo com sucesso', async () => {
107
+ const file = {
108
+ originalname: 'download-test.txt',
109
+ mimetype: 'text/plain',
110
+ buffer: Buffer.from('download content'),
111
+ size: 16,
112
+ };
113
+ const uploadResult = await service.upload(file);
114
+ const downloadResult = await service.download(uploadResult.key);
115
+ assert.strictEqual(downloadResult.type, 'file');
116
+ assert.ok(downloadResult.filePath);
117
+ const content = await fs.readFile(downloadResult.filePath);
118
+ assert.strictEqual(content.toString(), 'download content');
119
+ });
120
+ (0, node_test_1.test)('deve rejeitar download com Directory Traversal em key', async () => {
121
+ await assert.rejects(async () => {
122
+ await service.download('../../../etc/passwd');
123
+ }, /Directory traversal attempt detected/);
124
+ });
125
+ (0, node_test_1.test)('deve lançar erro ao tentar baixar arquivo inexistente', async () => {
126
+ await assert.rejects(async () => {
127
+ await service.download('non-existent-file.txt');
128
+ }, /File not found/);
129
+ });
106
130
  });
@@ -1,5 +1,5 @@
1
1
  import { StorageService } from '../storage.service';
2
- import { StorageFile, UploadOptions, UploadResult, S3StorageOptions, R2StorageOptions } from '../storage.types';
2
+ import { StorageFile, UploadOptions, UploadResult, S3StorageOptions, R2StorageOptions, StorageDownloadResult } from '../storage.types';
3
3
  export declare class S3StorageService implements StorageService {
4
4
  private readonly client;
5
5
  private readonly bucket;
@@ -9,4 +9,5 @@ export declare class S3StorageService implements StorageService {
9
9
  delete(key: string): Promise<void>;
10
10
  getUrl(key: string): Promise<string>;
11
11
  getSignedUrl(key: string, expiresIn?: number): Promise<string>;
12
+ download(key: string): Promise<StorageDownloadResult>;
12
13
  }
@@ -130,6 +130,13 @@ let S3StorageService = class S3StorageService {
130
130
  });
131
131
  return (0, s3_request_presigner_1.getSignedUrl)(this.client, command, { expiresIn });
132
132
  }
133
+ async download(key) {
134
+ const url = await this.getSignedUrl(key, 300);
135
+ return {
136
+ type: 'redirect',
137
+ url,
138
+ };
139
+ }
133
140
  };
134
141
  exports.S3StorageService = S3StorageService;
135
142
  exports.S3StorageService = S3StorageService = __decorate([
@@ -1,7 +1,8 @@
1
- import { StorageFile, UploadOptions, UploadResult } from './storage.types';
1
+ import { StorageFile, UploadOptions, UploadResult, StorageDownloadResult } from './storage.types';
2
2
  export declare abstract class StorageService {
3
3
  abstract upload(file: StorageFile, options?: UploadOptions): Promise<UploadResult>;
4
4
  abstract delete(key: string): Promise<void>;
5
5
  abstract getUrl(key: string): Promise<string>;
6
6
  abstract getSignedUrl(key: string, expiresIn?: number): Promise<string>;
7
+ abstract download(key: string): Promise<StorageDownloadResult>;
7
8
  }
@@ -40,3 +40,8 @@ export interface UploadResult {
40
40
  size: number;
41
41
  mimetype: string;
42
42
  }
43
+ export interface StorageDownloadResult {
44
+ type: 'file' | 'redirect';
45
+ filePath?: string;
46
+ url?: string;
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devstroupe/devkit-nest",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "DevsTroupe Development Kit — NestJS base classes: BaseRepository, BaseCrudController and query parsers",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "@aws-sdk/client-s3": "^3.500.0",
33
33
  "@aws-sdk/s3-request-presigner": "^3.500.0",
34
34
  "jsonwebtoken": "^9.0.2",
35
- "@devstroupe/devkit-core": "1.1.14"
35
+ "@devstroupe/devkit-core": "1.1.16"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@nestjs/common": "^10.0.0 || ^11.0.0",