2023-04-13 08:45:44 +02:00
import type { ReadStream } from 'node:fs' ;
import { getOr } from 'lodash/fp' ;
2023-11-06 16:43:35 +01:00
import {
S3Client ,
GetObjectCommand ,
DeleteObjectCommand ,
DeleteObjectCommandOutput ,
PutObjectCommandInput ,
CompleteMultipartUploadCommandOutput ,
AbortMultipartUploadCommandOutput ,
S3ClientConfig ,
ObjectCannedACL ,
} from '@aws-sdk/client-s3' ;
import type { AwsCredentialIdentity } from '@aws-sdk/types' ;
import { getSignedUrl } from '@aws-sdk/s3-request-presigner' ;
import { Upload } from '@aws-sdk/lib-storage' ;
import { extractCredentials , isUrlFromBucket } from './utils' ;
export interface File {
2023-04-13 08:45:44 +02:00
name : string ;
alternativeText? : string ;
caption? : string ;
width? : number ;
height? : number ;
formats? : Record < string , unknown > ;
hash : string ;
ext? : string ;
mime : string ;
size : number ;
2024-02-23 11:37:26 +01:00
sizeInBytes : number ;
2023-04-13 08:45:44 +02:00
url : string ;
previewUrl? : string ;
path? : string ;
provider? : string ;
provider_metadata? : Record < string , unknown > ;
stream? : ReadStream ;
buffer? : Buffer ;
}
2018-02-20 15:57:34 +01:00
2023-11-06 16:43:35 +01:00
export type UploadCommandOutput = (
| CompleteMultipartUploadCommandOutput
| AbortMultipartUploadCommandOutput
) & {
Location : string ;
} ;
2018-02-20 15:57:34 +01:00
2023-11-06 16:43:35 +01:00
export interface AWSParams {
Bucket : string ; // making it required
ACL? : ObjectCannedACL ;
signedUrlExpires? : number ;
}
export interface DefaultOptions extends S3ClientConfig {
// TODO Remove this in V5
accessKeyId? : AwsCredentialIdentity [ 'accessKeyId' ] ;
secretAccessKey? : AwsCredentialIdentity [ 'secretAccessKey' ] ;
// Keep this for V5
credentials? : AwsCredentialIdentity ;
params? : AWSParams ;
[ k : string ] : any ;
2022-10-18 10:56:19 +02:00
}
2023-11-06 16:43:35 +01:00
export type InitOptions = ( DefaultOptions | { s3Options : DefaultOptions } ) & {
2023-04-13 08:45:44 +02:00
baseUrl? : string ;
rootPath? : string ;
2023-11-06 16:43:35 +01:00
[ k : string ] : any ;
} ;
2023-04-13 08:45:44 +02:00
2023-11-06 16:43:35 +01:00
const assertUrlProtocol = ( url : string ) = > {
// Regex to test protocol like "http://", "https://"
return /^\w*:\/\// . test ( url ) ;
} ;
const getConfig = ( { baseUrl , rootPath , s3Options , . . . legacyS3Options } : InitOptions ) = > {
if ( Object . keys ( legacyS3Options ) . length > 0 ) {
process . emitWarning (
"S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property."
) ;
}
2023-11-13 19:54:47 +01:00
const credentials = extractCredentials ( { s3Options , . . . legacyS3Options } ) ;
2023-11-06 16:43:35 +01:00
const config = {
. . . s3Options ,
. . . legacyS3Options ,
2023-11-14 10:03:47 +01:00
. . . ( credentials ? { credentials } : { } ) ,
2023-11-06 16:43:35 +01:00
} ;
2022-04-01 18:40:22 +03:00
2023-11-06 16:43:35 +01:00
config . params . ACL = getOr ( ObjectCannedACL . public_read , [ 'params' , 'ACL' ] , config ) ;
2023-03-21 14:59:26 +01:00
2023-11-06 16:43:35 +01:00
return config ;
} ;
2018-02-21 14:06:57 +01:00
2023-11-06 16:43:35 +01:00
export default {
init ( { baseUrl , rootPath , s3Options , . . . legacyS3Options } : InitOptions ) {
// TODO V5 change config structure to avoid having to do this
const config = getConfig ( { baseUrl , rootPath , s3Options , . . . legacyS3Options } ) ;
const s3Client = new S3Client ( config ) ;
2022-10-25 14:45:50 +02:00
const filePrefix = rootPath ? ` ${ rootPath . replace ( /\/+$/ , '' ) } / ` : '' ;
2023-04-13 08:45:44 +02:00
const getFileKey = ( file : File ) = > {
2022-10-25 14:47:26 +02:00
const path = file . path ? ` ${ file . path } / ` : '' ;
return ` ${ filePrefix } ${ path } ${ file . hash } ${ file . ext } ` ;
2023-03-20 12:27:33 +01:00
} ;
2022-10-25 14:47:26 +02:00
2023-11-06 16:43:35 +01:00
const upload = async ( file : File , customParams : Partial < PutObjectCommandInput > = { } ) = > {
const fileKey = getFileKey ( file ) ;
const uploadObj = new Upload ( {
client : s3Client ,
params : {
2023-04-13 08:45:44 +02:00
Bucket : config.params.Bucket ,
2023-11-06 16:43:35 +01:00
Key : fileKey ,
Body : file.stream || Buffer . from ( file . buffer as any , 'binary' ) ,
ACL : config.params.ACL ,
2023-04-13 08:45:44 +02:00
ContentType : file.mime ,
. . . customParams ,
2023-11-06 16:43:35 +01:00
} ,
} ) ;
2023-04-13 08:45:44 +02:00
2023-11-06 16:43:35 +01:00
const upload = ( await uploadObj . done ( ) ) as UploadCommandOutput ;
2023-04-13 08:45:44 +02:00
2023-11-06 16:43:35 +01:00
if ( assertUrlProtocol ( upload . Location ) ) {
file . url = baseUrl ? ` ${ baseUrl } / ${ fileKey } ` : upload . Location ;
} else {
// Default protocol to https protocol
file . url = ` https:// ${ upload . Location } ` ;
}
} ;
2022-01-05 19:02:04 +01:00
return {
2023-02-09 15:02:15 +01:00
isPrivate() {
2023-11-06 16:43:35 +01:00
return config . params . ACL === 'private' ;
2023-02-09 15:02:15 +01:00
} ,
2023-11-06 16:43:35 +01:00
async getSignedUrl ( file : File , customParams : any ) : Promise < { url : string } > {
2023-02-20 12:18:37 +01:00
// Do not sign the url if it does not come from the same bucket.
2023-05-31 11:26:44 +02:00
if ( ! isUrlFromBucket ( file . url , config . params . Bucket , baseUrl ) ) {
2023-02-20 12:18:37 +01:00
return { url : file.url } ;
}
2023-11-06 16:43:35 +01:00
const fileKey = getFileKey ( file ) ;
2023-02-20 12:18:37 +01:00
2023-11-06 16:43:35 +01:00
const url = await getSignedUrl (
s3Client ,
new GetObjectCommand ( {
Bucket : config.params.Bucket ,
Key : fileKey ,
. . . customParams ,
} ) ,
{
expiresIn : getOr ( 15 * 60 , [ 'params' , 'signedUrlExpires' ] , config ) ,
}
) ;
return { url } ;
2023-02-09 15:02:15 +01:00
} ,
2023-04-13 08:45:44 +02:00
uploadStream ( file : File , customParams = { } ) {
2022-01-05 19:02:04 +01:00
return upload ( file , customParams ) ;
} ,
2023-04-13 08:45:44 +02:00
upload ( file : File , customParams = { } ) {
2022-01-05 19:02:04 +01:00
return upload ( file , customParams ) ;
2018-02-20 15:57:34 +01:00
} ,
2023-11-06 16:43:35 +01:00
delete ( file : File , customParams = { } ) : Promise < DeleteObjectCommandOutput > {
const command = new DeleteObjectCommand ( {
Bucket : config.params.Bucket ,
Key : getFileKey ( file ) ,
. . . customParams ,
2018-02-22 14:43:10 +01:00
} ) ;
2023-11-06 16:43:35 +01:00
return s3Client . send ( command ) ;
2019-07-18 19:28:52 +02:00
} ,
2018-02-20 15:57:34 +01:00
} ;
2019-07-18 19:28:52 +02:00
} ,
2018-02-20 15:57:34 +01:00
} ;