Skip to content

Commit ffaab4e

Browse files
authored
feat(storage): support for proper disposition headers to maintain true filenames when using aliases (#1358)
feat(storage): support for attachment disposition header to request immediate downloads
1 parent 97f7304 commit ffaab4e

File tree

11 files changed

+64
-22
lines changed

11 files changed

+64
-22
lines changed

modules/storage/src/Storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export default class Storage extends ManagedModule<Config> {
224224
const request = createParsedRouterRequest(
225225
call.request,
226226
undefined,
227-
{ scope: call.request.scope },
227+
{ scope: call.request.scope, download: call.request.download },
228228
undefined,
229229
undefined,
230230
undefined,

modules/storage/src/admin/adminFile.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ConfigController } from '@conduitplatform/module-tools';
99
import { status } from '@grpc/grpc-js';
1010
import { isNil, isString } from 'lodash-es';
1111
import { _StorageContainer, _StorageFolder, File } from '../models/index.js';
12-
import { IStorageProvider } from '../interfaces/index.js';
12+
import { IStorageProvider, UrlOptions } from '../interfaces/index.js';
1313
import {
1414
_createFileUploadUrl,
1515
_updateFile,
@@ -208,9 +208,13 @@ export class AdminFileHandlers {
208208
}
209209
return { redirect: found.url };
210210
}
211+
let options: UrlOptions = {
212+
download: call.request.params.download ?? false,
213+
fileName: found.alias ?? found.name,
214+
};
211215
const url = await this.storageProvider
212216
.container(found.container)
213-
.getSignedUrl((found.folder === '/' ? '' : found.folder) + found.name);
217+
.getSignedUrl((found.folder === '/' ? '' : found.folder) + found.name, options);
214218

215219
if (!call.request.params.redirect) {
216220
return { result: url };

modules/storage/src/handlers/file.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ConfigController } from '@conduitplatform/module-tools';
1010
import { status } from '@grpc/grpc-js';
1111
import { isNil, isString } from 'lodash-es';
1212
import { _StorageContainer, _StorageFolder, File } from '../models/index.js';
13-
import { IStorageProvider } from '../interfaces/index.js';
13+
import { IStorageProvider, UrlOptions } from '../interfaces/index.js';
1414
import {
1515
_createFileUploadUrl,
1616
_updateFile,
@@ -275,9 +275,13 @@ export class FileHandlers {
275275
return { redirect: found.url };
276276
}
277277
await this.fileAccessCheck('read', call.request, found);
278+
const options: UrlOptions = {
279+
download: call.request.params.download ?? false,
280+
fileName: found.alias ?? found.name,
281+
};
278282
const url = await this.storageProvider
279283
.container(found.container)
280-
.getSignedUrl((found.folder === '/' ? '' : found.folder) + found.name);
284+
.getSignedUrl((found.folder === '/' ? '' : found.folder) + found.name, options);
281285

282286
if (!call.request.params.redirect) {
283287
return { result: url };

modules/storage/src/interfaces/IStorageProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export type UrlOptions = {
2+
download?: boolean;
3+
fileName?: string;
4+
};
5+
16
export interface IStorageProvider {
27
store(fileName: string, data: any, isPublic?: boolean): Promise<boolean | Error>;
38

@@ -34,7 +39,7 @@ export interface IStorageProvider {
3439

3540
get(fileName: string, downloadPath?: string): Promise<Buffer | Error>;
3641

37-
getSignedUrl(fileName: string): Promise<any | Error>;
42+
getSignedUrl(fileName: string, options?: UrlOptions): Promise<any | Error>;
3843

3944
getPublicUrl(fileName: string): Promise<any | Error>;
4045

modules/storage/src/providers/aliyun/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { IStorageProvider, StorageConfig } from '../../interfaces/index.js';
1+
import { IStorageProvider, StorageConfig, UrlOptions } from '../../interfaces/index.js';
22
import OSS from 'ali-oss';
33
import fs from 'fs';
44
import { ConduitGrpcSdk } from '@conduitplatform/grpc-sdk';
55
import { SIGNED_URL_EXPIRY_SECONDS } from '../../constants/expiry.js';
6+
import { constructDispositionHeader } from '../../utils/index.js';
67

78
export class AliyunStorage implements IStorageProvider {
89
private _activeContainer: string = '';
@@ -135,13 +136,14 @@ export class AliyunStorage implements IStorageProvider {
135136
return content;
136137
}
137138

138-
async getSignedUrl(fileName: string): Promise<any | Error> {
139-
const url = this._ossClient.signatureUrl(fileName, {
139+
async getSignedUrl(fileName: string, options?: UrlOptions): Promise<any | Error> {
140+
return this._ossClient.signatureUrl(fileName, {
140141
expires: SIGNED_URL_EXPIRY_SECONDS,
141142
method: 'GET',
143+
response: {
144+
'content-disposition': constructDispositionHeader(fileName, options),
145+
},
142146
});
143-
144-
return url;
145147
}
146148

147149
async getPublicUrl(fileName: string): Promise<any | Error> {

modules/storage/src/providers/aws/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IStorageProvider, StorageConfig } from '../../interfaces/index.js';
1+
import { IStorageProvider, StorageConfig, UrlOptions } from '../../interfaces/index.js';
22
import {
33
CreateBucketCommand,
44
DeleteBucketCommand,
@@ -12,7 +12,7 @@ import {
1212
S3ClientConfig,
1313
} from '@aws-sdk/client-s3';
1414
import { Readable } from 'stream';
15-
import { streamToBuffer } from '../../utils/index.js';
15+
import { constructDispositionHeader, streamToBuffer } from '../../utils/index.js';
1616
import fs from 'fs';
1717
import { getSignedUrl as awsGetSignedUrl } from '@aws-sdk/s3-request-presigner';
1818
import { ConduitGrpcSdk } from '@conduitplatform/grpc-sdk';
@@ -202,10 +202,11 @@ export class AWSS3Storage implements IStorageProvider {
202202
}
203203
}
204204

205-
async getSignedUrl(fileName: string) {
205+
async getSignedUrl(fileName: string, options?: UrlOptions) {
206206
const command = new GetObjectCommand({
207207
Bucket: this._activeContainer,
208208
Key: fileName,
209+
ResponseContentDisposition: constructDispositionHeader(fileName, options),
209210
});
210211
return awsGetSignedUrl(this._storage, command, {
211212
expiresIn: SIGNED_URL_EXPIRY_SECONDS,

modules/storage/src/providers/azure/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IStorageProvider, StorageConfig } from '../../interfaces/index.js';
1+
import { IStorageProvider, StorageConfig, UrlOptions } from '../../interfaces/index.js';
22
import {
33
BlobClient,
44
BlobSASPermissions,
@@ -8,7 +8,7 @@ import {
88
} from '@azure/storage-blob';
99
import fs from 'fs';
1010
import { ConduitGrpcSdk } from '@conduitplatform/grpc-sdk';
11-
import { streamToBuffer } from '../../utils/index.js';
11+
import { constructDispositionHeader, streamToBuffer } from '../../utils/index.js';
1212
import { SIGNED_URL_EXPIRY_DATE } from '../../constants/expiry.js';
1313

1414
export class AzureStorage implements IStorageProvider {
@@ -122,14 +122,17 @@ export class AzureStorage implements IStorageProvider {
122122
return data;
123123
}
124124

125-
async getSignedUrl(fileName: string): Promise<any | Error> {
125+
async getSignedUrl(fileName: string, options: UrlOptions): Promise<any | Error> {
126126
const containerClient = this._storage.getContainerClient(this._activeContainer);
127127
const sasOptions: BlobSASSignatureValues = {
128128
containerName: containerClient.containerName,
129129
blobName: fileName,
130130
expiresOn: new Date(SIGNED_URL_EXPIRY_DATE()),
131131
permissions: BlobSASPermissions.parse('r'),
132132
};
133+
if (options) {
134+
sasOptions.contentDisposition = constructDispositionHeader(fileName, options);
135+
}
133136
return this.blobClient(fileName).generateSasUrl(sasOptions);
134137
}
135138

modules/storage/src/providers/google/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { IStorageProvider, StorageConfig } from '../../interfaces/index.js';
1+
import { IStorageProvider, StorageConfig, UrlOptions } from '../../interfaces/index.js';
22
import { Storage } from '@google-cloud/storage';
33
import { ConduitGrpcSdk } from '@conduitplatform/grpc-sdk';
44
import { SIGNED_URL_EXPIRY_DATE } from '../../constants/expiry.js';
5+
import { constructDispositionHeader } from '../../utils/index.js';
56

67
/**
78
* WARNING: DO NOT USE THIS, IT NEEDS A REWRITE
@@ -103,13 +104,14 @@ export class GoogleCloudStorage implements IStorageProvider {
103104
});
104105
}
105106

106-
async getSignedUrl(fileName: string): Promise<any | Error> {
107+
async getSignedUrl(fileName: string, options?: UrlOptions): Promise<any | Error> {
107108
this._storage
108109
.bucket(this._activeBucket)
109110
.file(fileName)
110111
.getSignedUrl({
111112
action: 'read',
112113
expires: SIGNED_URL_EXPIRY_DATE(),
114+
responseDisposition: constructDispositionHeader(fileName, options),
113115
})
114116
.then((r: any) => {
115117
if (r.data && r.data[0]) {
@@ -120,8 +122,7 @@ export class GoogleCloudStorage implements IStorageProvider {
120122
}
121123

122124
async getPublicUrl(fileName: string): Promise<any | Error> {
123-
await this._storage.bucket(this._activeBucket).file(fileName).isPublic();
124-
return this._storage.bucket(this._activeBucket).file(fileName).baseUrl;
125+
return this._storage.bucket(this._activeBucket).file(fileName).publicUrl();
125126
}
126127

127128
async store(

modules/storage/src/routes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export class StorageRoutes {
5353
},
5454
queryParams: {
5555
redirect: { type: TYPE.Boolean, required: false },
56+
download: { type: TYPE.Boolean, required: false },
5657
...(authzEnabled && { scope: { type: TYPE.String, required: false } }),
5758
},
5859
middlewares: ['authMiddleware?'],

modules/storage/src/storage.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ message GetFileUrlRequest {
1111
string id = 1;
1212
optional string userId = 2;
1313
optional string scope = 3;
14+
optional bool download = 4;
1415

1516
}
1617
message GetFileUrlResponse {

0 commit comments

Comments
 (0)