Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions apps/meteor/app/file-upload/server/config/AmazonS3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const configure = _.debounce(() => {
const AWSSecretAccessKey = settings.get<string>('FileUpload_S3_AWSSecretAccessKey');
const URLExpiryTimeSpan = settings.get<number>('FileUpload_S3_URLExpiryTimeSpan');
const Region = settings.get<string>('FileUpload_S3_Region');
const SignatureVersion = settings.get<string>('FileUpload_S3_SignatureVersion');
// const SignatureVersion = settings.get<string>('FileUpload_S3_SignatureVersion');
const ForcePathStyle = settings.get<boolean>('FileUpload_S3_ForcePathStyle');
// const CDN = RocketChat.settings.get('FileUpload_S3_CDN');
const BucketURL = settings.get<string>('FileUpload_S3_BucketURL');
Expand All @@ -81,23 +81,23 @@ const configure = _.debounce(() => {

const config: Omit<S3Options, 'name' | 'getPath'> = {
connection: {
signatureVersion: SignatureVersion,
s3ForcePathStyle: ForcePathStyle,
params: {
Bucket,
ACL: Acl,
},
// signatureVersion: SignatureVersion,
forcePathStyle: ForcePathStyle,
region: Region,
followRegionRedirects: true,
},
params: {
Bucket,
ACL: Acl,
},
URLExpiryTimeSpan,
};

if (AWSAccessKeyId) {
config.connection.accessKeyId = AWSAccessKeyId;
}

if (AWSSecretAccessKey) {
config.connection.secretAccessKey = AWSSecretAccessKey;
if (AWSAccessKeyId && AWSSecretAccessKey) {
config.connection.credentials = {
accessKeyId: AWSAccessKeyId,
secretAccessKey: AWSSecretAccessKey,
};
}

if (BucketURL) {
Expand Down
139 changes: 73 additions & 66 deletions apps/meteor/app/file-upload/ufs/AmazonS3/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import stream from 'stream';

import {
DeleteObjectCommand,
GetObjectCommand,
S3Client,
type GetObjectCommandInput,
type PutObjectCommandInput,
type S3ClientConfig,
} from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import type { IUpload } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import S3 from 'aws-sdk/clients/s3';
import { check } from 'meteor/check';
import type { OptionalId } from 'mongodb';
import _ from 'underscore';
Expand All @@ -12,17 +21,10 @@ import { UploadFS } from '../../../../server/ufs';
import type { StoreOptions } from '../../../../server/ufs/ufs-store';

export type S3Options = StoreOptions & {
connection: {
accessKeyId?: string;
secretAccessKey?: string;
endpoint?: string;
signatureVersion: string;
s3ForcePathStyle?: boolean;
params: {
Bucket: string;
ACL: string;
};
region: string;
connection: S3ClientConfig;
params: {
Bucket: string;
ACL: string;
};
URLExpiryTimeSpan: number;
getPath: (file: OptionalId<IUpload>) => string;
Expand Down Expand Up @@ -54,9 +56,9 @@ class AmazonS3Store extends UploadFS.Store {

const customUserAgent = process.env.FILE_STORAGE_CUSTOM_USER_AGENT?.trim();

const s3 = new S3({
...(customUserAgent && { customUserAgent }),
const s3 = new S3Client({
...options.connection,
...(customUserAgent && { customUserAgent }),
});

options.getPath =
Expand All @@ -79,20 +81,21 @@ class AmazonS3Store extends UploadFS.Store {
};

this.getRedirectURL = async (file, forceDownload = false) => {
const params = {
Key: this.getPath(file),
Expires: classOptions.URLExpiryTimeSpan,
ResponseContentDisposition: `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(file.name || '')}"`,
};

return s3.getSignedUrlPromise('getObject', params);
return getSignedUrl(
s3,
new GetObjectCommand({
Key: this.getPath(file),
ResponseContentDisposition: `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(file.name || '')}"`,
Bucket: classOptions.params.Bucket,
}),
{
expiresIn: classOptions.URLExpiryTimeSpan, // seconds
},
);
};

/**
* Creates the file in the collection
* @param file
* @param callback
* @return {string}
*/
this.create = async (file) => {
check(file, Object);
Expand All @@ -112,61 +115,57 @@ class AmazonS3Store extends UploadFS.Store {

/**
* Removes the file
* @param fileId
* @param callback
*/
this.delete = async function (fileId) {
const file = await this.getCollection().findOne({ _id: fileId });
if (!file) {
throw new Error('File not found');
}
const params = {
Key: this.getPath(file),
Bucket: classOptions.connection.params.Bucket,
};

try {
return s3.deleteObject(params).promise();
} catch (err: any) {
SystemLogger.error({ err });
return await s3.send(
new DeleteObjectCommand({
Key: this.getPath(file),
Bucket: classOptions.params.Bucket,
}),
);
} catch (error) {
SystemLogger.error({ error, key: this.getPath(file), bucket: classOptions.params.Bucket });
throw error;
}
};

/**
* Returns the file read stream
* @param fileId
* @param file
* @param options
* @return {*}
*/
this.getReadStream = async function (_fileId, file, options = {}) {
const params: {
Key: string;
Bucket: string;
Range?: string;
} = {
const params: GetObjectCommandInput = {
Key: this.getPath(file),
Bucket: classOptions.connection.params.Bucket,
Bucket: classOptions.params.Bucket,
};

if (options.start && options.end) {
params.Range = `${options.start} - ${options.end}`;
if (options.start != null && options.end != null) {
params.Range = `bytes=${options.start}-${options.end}`;
}

return s3.getObject(params).createReadStream();
const response = await s3.send(new GetObjectCommand(params));

if (!response.Body) {
throw new Error('File not found');
}

if (!('readable' in response.Body)) {
throw new Error('Response body is not a readable stream');
}

return response.Body;
};

/**
* Returns the file write stream
* @param fileId
* @param file
* @param options
* @return {*}
*/
this.getWriteStream = async function (_fileId, file /* , options*/) {
const writeStream = new stream.PassThrough();
// TS does not allow but S3 requires a length property;
(writeStream as unknown as any).length = file.size;

writeStream.on('newListener', (event, listener) => {
if (event === 'finish') {
Expand All @@ -177,27 +176,35 @@ class AmazonS3Store extends UploadFS.Store {
}
});

s3.putObject(
{
Key: this.getPath(file),
Body: writeStream,
ContentType: file.type,
Bucket: classOptions.connection.params.Bucket,
},
(err) => {
if (err) {
SystemLogger.error({ err });
}
const uploadParams: PutObjectCommandInput = {
Key: this.getPath(file),
Body: writeStream,
Bucket: classOptions.params.Bucket,
...(file.type && { ContentType: file.type }),
...(file.size != null && { ContentLength: file.size }),
...(classOptions.params.ACL && { ACL: classOptions.params.ACL as PutObjectCommandInput['ACL'] }),
};

const upload = new Upload({
client: s3,
params: uploadParams,
});

upload
.done()
.then(() => {
writeStream.emit('real_finish');
},
);
})
.catch((error) => {
SystemLogger.error({ err: error });
writeStream.emit('error', error);
});

return writeStream;
};

this.getUrlExpiryTimeSpan = async () => {
return options.URLExpiryTimeSpan || null;
return classOptions.URLExpiryTimeSpan || null;
};
}
}
Expand Down
4 changes: 3 additions & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
"Firefox ESR"
],
"dependencies": {
"@aws-sdk/client-s3": "^3.862.0",
"@aws-sdk/lib-storage": "^3.862.0",
"@aws-sdk/s3-request-presigner": "^3.862.0",
"@babel/runtime": "~7.28.6",
"@bugsnag/js": "~7.20.2",
"@bugsnag/plugin-react": "~7.19.0",
Expand Down Expand Up @@ -166,7 +169,6 @@
"archiver": "^7.0.1",
"asterisk-manager": "^0.2.0",
"atlassian-crowd-patched": "^0.5.1",
"aws-sdk": "^2.1691.0",
"bad-words": "^3.0.4",
"bcrypt": "^5.1.1",
"body-parser": "1.20.4",
Expand Down
Loading
Loading