2015-08-24 11:13:21 -07:00
'use strict' ;
exports = module . exports = {
2022-04-04 14:13:27 -07:00
getRootPath ,
2021-02-01 14:23:15 -08:00
checkPreconditions ,
2020-06-05 13:27:18 +02:00
2021-02-01 14:23:15 -08:00
upload ,
2021-02-18 16:51:43 -08:00
exists ,
2021-02-01 14:23:15 -08:00
download ,
copy ,
2017-09-17 18:50:29 -07:00
2021-02-01 14:23:15 -08:00
listDir ,
2018-07-28 09:05:44 -07:00
2021-02-01 14:23:15 -08:00
remove ,
removeDir ,
2016-10-11 11:36:25 +02:00
2021-10-11 17:45:35 +02:00
remount ,
2021-02-01 14:23:15 -08:00
testConfig ,
removePrivateFields ,
injectPrivateFields ,
2017-04-18 19:15:56 +02:00
// Used to mock AWS
_mockInject : mockInject ,
_mockRestore : mockRestore
2015-08-24 11:13:21 -07:00
} ;
2021-06-16 22:36:01 -07:00
const assert = require ( 'assert' ) ,
2017-09-23 11:09:36 -07:00
async = require ( 'async' ) ,
2021-06-23 14:30:00 -07:00
AwsSdk = require ( 'aws-sdk' ) ,
2019-10-22 20:36:20 -07:00
BoxError = require ( '../boxerror.js' ) ,
2017-10-05 10:01:09 -07:00
chunk = require ( 'lodash.chunk' ) ,
2020-05-14 23:01:44 +02:00
constants = require ( '../constants.js' ) ,
2020-06-08 16:25:00 +02:00
DataLayout = require ( '../datalayout.js' ) ,
2017-04-18 15:33:06 +02:00
debug = require ( 'debug' ) ( 'box:storage/s3' ) ,
2017-10-04 11:00:30 -07:00
EventEmitter = require ( 'events' ) ,
2017-10-05 10:01:09 -07:00
https = require ( 'https' ) ,
2017-04-21 15:28:25 -07:00
PassThrough = require ( 'stream' ) . PassThrough ,
2017-04-18 15:33:06 +02:00
path = require ( 'path' ) ,
2020-05-12 22:45:01 -07:00
S3BlockReadStream = require ( 's3-block-read-stream' ) ,
2022-04-14 07:59:50 -05:00
safe = require ( 'safetydance' ) ,
2022-04-14 16:07:01 -05:00
util = require ( 'util' ) ,
2020-05-12 22:45:01 -07:00
_ = require ( 'underscore' ) ;
2015-08-24 11:13:21 -07:00
2021-06-23 14:30:00 -07:00
let aws = AwsSdk ;
2017-04-18 19:15:56 +02:00
// test only
var originalAWS ;
function mockInject ( mock ) {
2021-06-23 14:30:00 -07:00
originalAWS = aws ;
aws = mock ;
2017-04-18 19:15:56 +02:00
}
function mockRestore ( ) {
2021-06-23 14:30:00 -07:00
aws = originalAWS ;
2017-04-18 19:15:56 +02:00
}
2018-07-30 07:39:34 -07:00
function S3 _NOT _FOUND ( error ) {
return error . code === 'NoSuchKey' || error . code === 'NotFound' || error . code === 'ENOENT' ;
}
2022-04-14 07:35:41 -05:00
function getS3Config ( apiConfig ) {
2016-03-31 09:48:01 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2015-08-24 11:13:21 -07:00
2022-04-14 07:35:41 -05:00
const credentials = {
2017-04-20 19:56:06 -07:00
signatureVersion : apiConfig . signatureVersion || 'v4' ,
2020-05-27 17:33:59 -07:00
s3ForcePathStyle : false , // Use vhost style instead of path style - https://forums.aws.amazon.com/ann.jspa?annID=6776
2016-03-31 09:48:01 -07:00
accessKeyId : apiConfig . accessKeyId ,
secretAccessKey : apiConfig . secretAccessKey ,
2017-10-10 23:42:24 -07:00
region : apiConfig . region || 'us-east-1' ,
2020-11-06 14:47:03 -08:00
maxRetries : 10 ,
2017-10-11 13:57:05 -07:00
retryDelayOptions : {
2021-03-04 23:40:23 -08:00
customBackoff : ( /* retryCount, error */ ) => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
2018-02-22 12:19:23 -08:00
} ,
httpOptions : {
2020-12-15 14:37:18 -08:00
connectTimeout : 60000 , // https://github.com/aws/aws-sdk-js/pull/1446
2020-11-06 14:47:03 -08:00
timeout : 0 // https://github.com/aws/aws-sdk-js/issues/1704 (allow unlimited time for chunk upload)
2017-10-11 13:57:05 -07:00
}
2015-11-06 18:22:29 -08:00
} ;
2015-09-09 11:43:50 -07:00
2016-12-07 10:47:06 +01:00
if ( apiConfig . endpoint ) credentials . endpoint = apiConfig . endpoint ;
2015-09-09 11:43:50 -07:00
2020-05-27 17:33:59 -07:00
if ( apiConfig . s3ForcePathStyle === true ) credentials . s3ForcePathStyle = true ;
// s3 endpoint names come from the SDK
const isHttps = ( credentials . endpoint && credentials . endpoint . startsWith ( 'https://' ) ) || apiConfig . provider === 's3' ;
if ( isHttps ) { // only set agent for https calls. otherwise, it crashes
if ( apiConfig . acceptSelfSignedCerts || apiConfig . bucket . includes ( '.' ) ) {
credentials . httpOptions . agent = new https . Agent ( { rejectUnauthorized : false } ) ;
}
2017-10-05 10:01:09 -07:00
}
2022-04-14 07:35:41 -05:00
return credentials ;
2015-08-24 11:13:21 -07:00
}
2015-08-25 10:01:04 -07:00
2017-04-18 15:33:06 +02:00
// storage api
2022-04-04 14:13:27 -07:00
function getRootPath ( apiConfig ) {
2020-06-05 13:27:18 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
return apiConfig . prefix ;
}
2022-04-14 07:40:19 -05:00
async function checkPreconditions ( apiConfig , dataLayout ) {
2020-06-08 16:25:00 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
}
2017-09-20 09:57:16 -07:00
function upload ( apiConfig , backupFilePath , sourceStream , callback ) {
2016-09-16 11:21:08 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
2017-09-20 09:57:16 -07:00
assert . strictEqual ( typeof sourceStream , 'object' ) ;
2016-09-16 11:21:08 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
2015-08-25 10:01:04 -07:00
2022-04-14 07:35:41 -05:00
const params = {
Bucket : apiConfig . bucket ,
Key : backupFilePath ,
Body : sourceStream
} ;
2015-08-25 10:01:04 -07:00
2022-04-14 07:35:41 -05:00
const s3 = new aws . S3 ( credentials ) ;
2017-10-10 10:47:41 -07:00
2022-04-14 07:35:41 -05:00
// s3.upload automatically does a multi-part upload. we set queueSize to 3 to reduce memory usage
// uploader will buffer at most queueSize * partSize bytes into memory at any given time.
// scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/)
// s3: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html (max 10k parts and no size limit on the last part!)
const partSize = apiConfig . uploadPartSize || ( apiConfig . provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024 ) ;
2019-09-30 20:42:37 -07:00
2022-04-14 07:35:41 -05:00
s3 . upload ( params , { partSize , queueSize : 3 } , function ( error , data ) {
if ( error ) {
debug ( 'Error uploading [%s]: s3 upload error.' , backupFilePath , error ) ;
return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , ` Error uploading ${ backupFilePath } . Message: ${ error . message } HTTP Code: ${ error . code } ` ) ) ;
}
2018-02-22 11:06:28 -08:00
2022-04-14 07:35:41 -05:00
debug ( ` Uploaded ${ backupFilePath } with partSize ${ partSize } : ${ JSON . stringify ( data ) } ` ) ;
2018-03-20 18:19:14 -07:00
2022-04-14 07:35:41 -05:00
callback ( null ) ;
2015-08-25 10:01:04 -07:00
} ) ;
}
2015-08-26 16:14:51 -07:00
2022-04-14 08:07:03 -05:00
async function exists ( apiConfig , backupFilePath ) {
2021-02-18 16:51:43 -08:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
2021-02-18 16:51:43 -08:00
2022-04-14 07:35:41 -05:00
const s3 = new aws . S3 ( _ . omit ( credentials , 'retryDelayOptions' , 'maxRetries' ) ) ;
2021-02-18 16:51:43 -08:00
2022-04-14 07:35:41 -05:00
if ( ! backupFilePath . endsWith ( '/' ) ) { // check for file
const params = {
Bucket : apiConfig . bucket ,
Key : backupFilePath
} ;
2021-02-18 16:51:43 -08:00
2022-04-14 08:07:03 -05:00
const [ error ] = await safe ( s3 . headObject ( params ) . promise ( ) ) ;
if ( ! Object . keys ( this . httpResponse . headers ) . some ( h => h . startsWith ( 'x-amz' ) ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'not a s3 endpoint' ) ;
if ( error && S3 _NOT _FOUND ( error ) ) return false ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Error headObject ${ backupFilePath } . Message: ${ error . message } HTTP Code: ${ error . code } ` ) ;
2021-02-18 16:51:43 -08:00
2022-04-14 08:07:03 -05:00
return true ;
2022-04-14 07:35:41 -05:00
} else { // list dir contents
const listParams = {
Bucket : apiConfig . bucket ,
Prefix : backupFilePath ,
MaxKeys : 1
} ;
2021-02-18 16:51:43 -08:00
2022-04-14 08:07:03 -05:00
const [ error , listData ] = await safe ( s3 . listObjects ( listParams ) . promise ( ) ) ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Error listing objects ${ backupFilePath } . Message: ${ error . message } HTTP Code: ${ error . code } ` ) ;
2021-02-18 16:51:43 -08:00
2022-04-14 08:07:03 -05:00
return listData . Contents . length !== 0 ;
2022-04-14 07:35:41 -05:00
}
2021-02-18 16:51:43 -08:00
}
2017-09-20 09:57:16 -07:00
function download ( apiConfig , backupFilePath , callback ) {
2016-09-19 15:03:38 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
2016-09-19 15:03:38 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
2016-09-19 15:03:38 +02:00
2022-04-14 07:35:41 -05:00
const params = {
Bucket : apiConfig . bucket ,
Key : backupFilePath
} ;
2017-04-18 15:33:06 +02:00
2022-04-14 07:35:41 -05:00
const s3 = new aws . S3 ( credentials ) ;
2017-04-18 16:44:49 +02:00
2022-04-14 07:35:41 -05:00
const ps = new PassThrough ( ) ;
const multipartDownload = new S3BlockReadStream ( s3 , params , { blockSize : 64 * 1024 * 1024 /*, logCallback: debug */ } ) ;
2017-04-21 15:06:54 -07:00
2022-04-14 07:35:41 -05:00
multipartDownload . on ( 'error' , function ( error ) {
if ( S3 _NOT _FOUND ( error ) ) {
ps . emit ( 'error' , new BoxError ( BoxError . NOT _FOUND , ` Backup not found: ${ backupFilePath } ` ) ) ;
} else {
debug ( ` download: ${ apiConfig . bucket } : ${ backupFilePath } s3 stream error. ` , error ) ;
ps . emit ( 'error' , new BoxError ( BoxError . EXTERNAL _ERROR , ` Error multipartDownload ${ backupFilePath } . Message: ${ error . message } HTTP Code: ${ error . code } ` ) ) ;
}
} ) ;
2017-04-21 15:28:25 -07:00
2022-04-14 07:35:41 -05:00
multipartDownload . pipe ( ps ) ;
2017-09-20 09:57:16 -07:00
2022-04-14 07:35:41 -05:00
callback ( null , ps ) ;
2016-09-16 18:14:36 +02:00
}
2018-07-28 09:05:44 -07:00
function listDir ( apiConfig , dir , batchSize , iteratorCallback , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof dir , 'string' ) ;
assert . strictEqual ( typeof batchSize , 'number' ) ;
assert . strictEqual ( typeof iteratorCallback , 'function' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
2017-09-23 14:27:35 -07:00
2022-04-14 07:35:41 -05:00
const s3 = new aws . S3 ( credentials ) ;
const listParams = {
Bucket : apiConfig . bucket ,
Prefix : dir ,
MaxKeys : batchSize
} ;
2017-09-23 14:27:35 -07:00
2022-04-14 07:35:41 -05:00
let done = false ;
2019-12-04 11:17:42 -08:00
2022-04-14 07:35:41 -05:00
async . whilst ( ( testDone ) => testDone ( null , ! done ) , function listAndDownload ( whilstCallback ) {
s3 . listObjects ( listParams , function ( error , listData ) {
if ( error ) return whilstCallback ( new BoxError ( BoxError . EXTERNAL _ERROR , ` Error listing objects in ${ dir } . Message: ${ error . message } HTTP Code: ${ error . code } ` ) ) ;
2017-09-23 14:27:35 -07:00
2022-04-14 07:35:41 -05:00
if ( listData . Contents . length === 0 ) { done = true ; return whilstCallback ( ) ; }
2017-09-27 21:46:24 -07:00
2022-04-14 07:35:41 -05:00
const entries = listData . Contents . map ( function ( c ) { return { fullPath : c . Key , size : c . Size } ; } ) ;
2018-07-28 09:05:44 -07:00
2022-04-14 07:35:41 -05:00
iteratorCallback ( entries , function ( error ) {
if ( error ) return whilstCallback ( error ) ;
2017-09-23 14:27:35 -07:00
2022-04-14 07:35:41 -05:00
if ( ! listData . IsTruncated ) { done = true ; return whilstCallback ( ) ; }
2017-09-27 21:46:24 -07:00
2022-04-14 07:35:41 -05:00
listParams . Marker = listData . Contents [ listData . Contents . length - 1 ] . Key ; // NextMarker is returned only with delimiter
2017-09-23 14:27:35 -07:00
2022-04-14 07:35:41 -05:00
whilstCallback ( ) ;
2017-09-23 14:27:35 -07:00
} ) ;
2022-04-14 07:35:41 -05:00
} ) ;
} , callback ) ;
2017-09-23 14:27:35 -07:00
}
2018-02-03 22:00:33 -08:00
// https://github.com/aws/aws-sdk-js/blob/2b6bcbdec1f274fe931640c1b61ece999aae7a19/lib/util.js#L41
// https://github.com/GeorgePhillips/node-s3-url-encode/blob/master/index.js
// See aws-sdk-js/issues/1302
function encodeCopySource ( bucket , path ) {
// AWS percent-encodes some extra non-standard characters in a URI
2022-04-14 07:35:41 -05:00
const output = encodeURI ( path ) . replace ( /[+!"#$@&'()*+,:;=?@]/g , function ( ch ) {
2018-02-03 22:00:33 -08:00
return '%' + ch . charCodeAt ( 0 ) . toString ( 16 ) . toUpperCase ( ) ;
} ) ;
// the slash at the beginning is optional
return ` / ${ bucket } / ${ output } ` ;
}
2017-10-04 11:00:30 -07:00
function copy ( apiConfig , oldFilePath , newFilePath ) {
2016-03-31 09:48:01 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof oldFilePath , 'string' ) ;
assert . strictEqual ( typeof newFilePath , 'string' ) ;
2017-10-04 11:00:30 -07:00
2022-04-14 07:35:41 -05:00
const events = new EventEmitter ( ) ;
2015-09-21 14:02:00 -07:00
2018-07-28 09:05:44 -07:00
function copyFile ( entry , iteratorCallback ) {
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
2018-03-21 14:58:36 -07:00
2022-04-14 07:35:41 -05:00
const s3 = new aws . S3 ( credentials ) ;
const relativePath = path . relative ( oldFilePath , entry . fullPath ) ;
2017-09-22 14:40:37 -07:00
2022-04-14 07:35:41 -05:00
function done ( error ) {
if ( error ) debug ( ` copy: s3 copy error when copying ${ entry . fullPath } : ${ error } ` ) ;
2018-02-22 10:31:56 -08:00
2022-04-14 07:35:41 -05:00
if ( error && S3 _NOT _FOUND ( error ) ) return iteratorCallback ( new BoxError ( BoxError . NOT _FOUND , ` Old backup not found: ${ entry . fullPath } ` ) ) ;
if ( error ) return iteratorCallback ( new BoxError ( BoxError . EXTERNAL _ERROR , ` Error copying ${ entry . fullPath } ( ${ entry . size } bytes): ${ error . code || '' } ${ error } ` ) ) ;
2017-10-11 13:57:05 -07:00
2022-04-14 07:35:41 -05:00
iteratorCallback ( null ) ;
}
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
const copyParams = {
Bucket : apiConfig . bucket ,
Key : path . join ( newFilePath , relativePath )
} ;
2017-10-04 11:00:30 -07:00
2022-04-14 07:35:41 -05:00
// S3 copyObject has a file size limit of 5GB so if we have larger files, we do a multipart copy
// Exoscale and B2 take too long to copy 5GB
const largeFileLimit = ( apiConfig . provider === 'exoscale-sos' || apiConfig . provider === 'backblaze-b2' || apiConfig . provider === 'digitalocean-spaces' ) ? 1024 * 1024 * 1024 : 5 * 1024 * 1024 * 1024 ;
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
if ( entry . size < largeFileLimit ) {
events . emit ( 'progress' , ` Copying ${ relativePath || oldFilePath } ` ) ;
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
copyParams . CopySource = encodeCopySource ( apiConfig . bucket , entry . fullPath ) ;
s3 . copyObject ( copyParams , done ) . on ( 'retry' , function ( response ) {
events . emit ( 'progress' , ` Retrying ( ${ response . retryCount + 1 } ) copy of ${ relativePath || oldFilePath } . Error: ${ response . error } ${ response . httpResponse . statusCode } ` ) ;
// on DO, we get a random 408. these are not retried by the SDK
if ( response . error ) response . error . retryable = true ; // https://github.com/aws/aws-sdk-js/issues/412
} ) ;
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
return ;
}
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
events . emit ( 'progress' , ` Copying (multipart) ${ relativePath || oldFilePath } ` ) ;
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
s3 . createMultipartUpload ( copyParams , function ( error , multipart ) {
if ( error ) return done ( error ) ;
2020-09-02 22:32:42 -07:00
2022-04-14 07:35:41 -05:00
// Exoscale (96M) was suggested by exoscale. 1GB - rather random size for others
const chunkSize = apiConfig . provider === 'exoscale-sos' ? 96 * 1024 * 1024 : 1024 * 1024 * 1024 ;
const uploadId = multipart . UploadId ;
let uploadedParts = [ ] , ranges = [ ] ;
2020-09-02 22:32:42 -07:00
2022-04-14 07:35:41 -05:00
let cur = 0 ;
while ( cur + chunkSize < entry . size ) {
ranges . push ( { startBytes : cur , endBytes : cur + chunkSize - 1 } ) ;
cur += chunkSize ;
}
ranges . push ( { startBytes : cur , endBytes : entry . size - 1 } ) ;
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
async . eachOfLimit ( ranges , 3 , function copyChunk ( range , index , iteratorDone ) {
const partCopyParams = {
Bucket : apiConfig . bucket ,
Key : path . join ( newFilePath , relativePath ) ,
CopySource : encodeCopySource ( apiConfig . bucket , entry . fullPath ) , // See aws-sdk-js/issues/1302
CopySourceRange : 'bytes=' + range . startBytes + '-' + range . endBytes ,
PartNumber : index + 1 ,
UploadId : uploadId
} ;
2019-03-26 11:58:32 -07:00
2022-04-14 07:35:41 -05:00
events . emit ( 'progress' , ` Copying part ${ partCopyParams . PartNumber } - ${ partCopyParams . CopySource } ${ partCopyParams . CopySourceRange } ` ) ;
2019-03-26 11:58:32 -07:00
2022-04-14 07:35:41 -05:00
s3 . uploadPartCopy ( partCopyParams , function ( error , part ) {
if ( error ) return iteratorDone ( error ) ;
2019-11-20 12:43:08 -08:00
2022-04-14 07:35:41 -05:00
events . emit ( 'progress' , ` Copying part ${ partCopyParams . PartNumber } - Etag: ${ part . CopyPartResult . ETag } ` ) ;
2018-07-29 09:00:57 -07:00
2022-04-14 07:35:41 -05:00
if ( ! part . CopyPartResult . ETag ) return iteratorDone ( new Error ( 'Multi-part copy is broken or not implemented by the S3 storage provider' ) ) ;
2018-07-29 09:00:57 -07:00
2022-04-14 07:35:41 -05:00
uploadedParts [ index ] = { ETag : part . CopyPartResult . ETag , PartNumber : partCopyParams . PartNumber } ;
2018-07-29 09:00:57 -07:00
2022-04-14 07:35:41 -05:00
iteratorDone ( ) ;
} ) . on ( 'retry' , function ( response ) {
events . emit ( 'progress' , ` Retrying ( ${ response . retryCount + 1 } ) multipart copy of ${ relativePath || oldFilePath } . Error: ${ response . error } ${ response . httpResponse . statusCode } ` ) ;
} ) ;
} , function chunksCopied ( error ) {
if ( error ) { // we must still recommend the user to set a AbortIncompleteMultipartUpload lifecycle rule
const abortParams = {
2020-09-02 22:32:42 -07:00
Bucket : apiConfig . bucket ,
Key : path . join ( newFilePath , relativePath ) ,
UploadId : uploadId
} ;
2022-04-14 07:35:41 -05:00
events . emit ( 'progress' , ` Aborting multipart copy of ${ relativePath || oldFilePath } ` ) ;
return s3 . abortMultipartUpload ( abortParams , ( ) => done ( error ) ) ; // ignore any abort errors
}
2019-03-26 11:58:32 -07:00
2022-04-14 07:35:41 -05:00
const completeMultipartParams = {
Bucket : apiConfig . bucket ,
Key : path . join ( newFilePath , relativePath ) ,
MultipartUpload : { Parts : uploadedParts } ,
UploadId : uploadId
} ;
2017-10-04 16:54:56 +02:00
2022-04-14 07:35:41 -05:00
events . emit ( 'progress' , ` Finishing multipart copy - ${ completeMultipartParams . Key } ` ) ;
s3 . completeMultipartUpload ( completeMultipartParams , done ) ;
2018-07-29 09:00:57 -07:00
} ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2017-10-11 13:57:05 -07:00
}
2020-09-02 22:32:42 -07:00
let total = 0 ;
2019-01-14 13:17:51 -08:00
const concurrency = apiConfig . copyConcurrency || ( apiConfig . provider === 's3' ? 500 : 10 ) ;
2020-09-02 22:32:42 -07:00
events . emit ( 'progress' , ` Copying with concurrency of ${ concurrency } ` ) ;
2017-10-11 13:57:05 -07:00
2018-07-28 09:05:44 -07:00
listDir ( apiConfig , oldFilePath , 1000 , function listDirIterator ( entries , done ) {
total += entries . length ;
2017-10-11 13:57:05 -07:00
2020-09-02 22:32:42 -07:00
events . emit ( 'progress' , ` Copying files from ${ total - entries . length } - ${ total } ` ) ;
2017-10-11 13:57:05 -07:00
2018-07-28 09:05:44 -07:00
async . eachLimit ( entries , concurrency , copyFile , done ) ;
2017-10-04 11:00:30 -07:00
} , function ( error ) {
2018-03-21 14:22:41 -07:00
events . emit ( 'progress' , ` Copied ${ total } files with error: ${ error } ` ) ;
2017-10-10 20:23:04 -07:00
2020-02-11 11:48:46 -08:00
process . nextTick ( ( ) => events . emit ( 'done' , error ) ) ;
2017-10-04 11:00:30 -07:00
} ) ;
return events ;
2015-09-21 14:02:00 -07:00
}
2016-10-10 15:04:28 +02:00
2022-04-14 16:07:01 -05:00
async function remove ( apiConfig , filename ) {
2017-09-27 17:34:49 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof filename , 'string' ) ;
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
2017-09-27 17:34:49 -07:00
2022-04-14 07:35:41 -05:00
const s3 = new aws . S3 ( credentials ) ;
2017-09-27 17:34:49 -07:00
2022-04-14 07:35:41 -05:00
const deleteParams = {
Bucket : apiConfig . bucket ,
Delete : {
Objects : [ { Key : filename } ]
}
} ;
2017-09-27 17:34:49 -07:00
2022-04-14 07:35:41 -05:00
// deleteObjects does not return error if key is not found
2022-04-14 16:07:01 -05:00
const [ error ] = await safe ( s3 . deleteObjects ( deleteParams ) . promise ( ) ) ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unable to remove ${ deleteParams . Key } . error: ${ error . message } ` ) ;
2017-09-27 17:34:49 -07:00
}
2022-04-14 16:07:01 -05:00
async function removeDir ( apiConfig , pathPrefix , progressCallback ) {
2016-10-10 15:04:28 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-23 11:09:36 -07:00
assert . strictEqual ( typeof pathPrefix , 'string' ) ;
2022-04-14 16:07:01 -05:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-10-10 15:04:28 +02:00
2017-10-10 20:23:04 -07:00
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
const s3 = new aws . S3 ( credentials ) ;
2022-04-14 16:07:01 -05:00
const listDirAsync = util . promisify ( listDir ) ;
let total = 0 ;
2017-10-11 13:57:05 -07:00
2022-04-14 16:07:01 -05:00
await listDirAsync ( apiConfig , pathPrefix , 1000 , function listDirIterator ( entries , done ) {
2022-04-14 07:35:41 -05:00
total += entries . length ;
2018-02-22 12:14:13 -08:00
2022-04-14 07:35:41 -05:00
const chunkSize = apiConfig . deleteConcurrency || ( apiConfig . provider !== 'digitalocean-spaces' ? 1000 : 100 ) ; // throttle objects in each request
2022-04-14 16:07:01 -05:00
const chunks = chunk ( entries , chunkSize ) ;
2018-02-22 12:14:13 -08:00
2022-04-14 16:07:01 -05:00
async . eachSeries ( chunks , async function deleteFiles ( objects ) {
const deleteParams = {
2022-04-14 07:35:41 -05:00
Bucket : apiConfig . bucket ,
Delete : {
Objects : objects . map ( function ( o ) { return { Key : o . fullPath } ; } )
}
} ;
2018-02-22 12:14:13 -08:00
2022-04-14 16:07:01 -05:00
progressCallback ( { message : ` Removing ${ objects . length } files from ${ objects [ 0 ] . fullPath } to ${ objects [ objects . length - 1 ] . fullPath } ` } ) ;
2017-10-10 20:23:04 -07:00
2022-04-14 07:35:41 -05:00
// deleteObjects does not return error if key is not found
2022-04-14 16:07:01 -05:00
const [ error ] = await safe ( s3 . deleteObjects ( deleteParams ) . promise ( ) ) ;
if ( error ) {
progressCallback ( { message : ` Unable to remove ${ deleteParams . Key } ${ error . message || error . code } ` } ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unable to remove ${ deleteParams . Key } . error: ${ error . message } ` ) ;
}
2022-04-14 07:35:41 -05:00
} , done ) ;
2017-10-10 20:23:04 -07:00
} ) ;
2022-04-14 16:07:01 -05:00
progressCallback ( { message : ` Removed ${ total } files ` } ) ;
2016-10-10 15:04:28 +02:00
}
2016-10-11 11:36:25 +02:00
2022-04-14 07:43:43 -05:00
async function remount ( apiConfig ) {
2021-10-11 17:45:35 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
}
2022-04-14 07:59:50 -05:00
async function testConfig ( apiConfig ) {
2016-10-11 11:36:25 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2022-04-14 07:59:50 -05:00
if ( typeof apiConfig . accessKeyId !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'accessKeyId must be a string' ) ;
if ( typeof apiConfig . secretAccessKey !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'secretAccessKey must be a string' ) ;
2017-09-27 10:25:36 -07:00
2022-04-14 07:59:50 -05:00
if ( typeof apiConfig . bucket !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'bucket must be a string' ) ;
2020-02-11 11:14:38 -08:00
// the node module seems to incorrectly accept bucket name with '/'
2022-04-14 07:59:50 -05:00
if ( apiConfig . bucket . includes ( '/' ) ) throw new BoxError ( BoxError . BAD _FIELD , 'bucket name cannot contain "/"' ) ;
2020-02-11 11:14:38 -08:00
2020-05-27 16:58:27 -07:00
// names must be lowercase and start with a letter or number. can contain dashes
2022-04-14 07:59:50 -05:00
if ( apiConfig . bucket . includes ( '_' ) || apiConfig . bucket . match ( /[A-Z]/ ) ) throw new BoxError ( BoxError . BAD _FIELD , 'bucket name cannot contain "_" or capitals' ) ;
2020-05-27 16:58:27 -07:00
2022-04-14 07:59:50 -05:00
if ( typeof apiConfig . prefix !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'prefix must be a string' ) ;
if ( 'signatureVersion' in apiConfig && typeof apiConfig . signatureVersion !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'signatureVersion must be a string' ) ;
if ( 'endpoint' in apiConfig && typeof apiConfig . endpoint !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'endpoint must be a string' ) ;
2016-10-11 11:36:25 +02:00
2022-04-14 07:59:50 -05:00
if ( 'acceptSelfSignedCerts' in apiConfig && typeof apiConfig . acceptSelfSignedCerts !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'acceptSelfSignedCerts must be a boolean' ) ;
if ( 's3ForcePathStyle' in apiConfig && typeof apiConfig . s3ForcePathStyle !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 's3ForcePathStyle must be a boolean' ) ;
2020-05-27 17:33:59 -07:00
2016-10-11 11:46:28 +02:00
// attempt to upload and delete a file with new credentials
2022-04-14 07:35:41 -05:00
const credentials = getS3Config ( apiConfig ) ;
2016-10-11 11:46:28 +02:00
2022-04-14 07:59:50 -05:00
const putParams = {
2022-04-14 07:35:41 -05:00
Bucket : apiConfig . bucket ,
Key : path . join ( apiConfig . prefix , 'cloudron-testfile' ) ,
Body : 'testcontent'
} ;
2016-10-11 11:46:28 +02:00
2022-04-14 07:35:41 -05:00
const s3 = new aws . S3 ( _ . omit ( credentials , 'retryDelayOptions' , 'maxRetries' ) ) ;
2022-04-14 07:59:50 -05:00
const [ putError ] = await safe ( s3 . putObject ( putParams ) . promise ( ) ) ;
if ( putError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Error put object cloudron-testfile. Message: ${ putError . message } HTTP Code: ${ putError . code } ` ) ;
2016-10-11 11:46:28 +02:00
2022-04-14 07:59:50 -05:00
const delParams = {
Bucket : apiConfig . bucket ,
Key : path . join ( apiConfig . prefix , 'cloudron-testfile' )
} ;
2016-10-11 11:46:28 +02:00
2022-04-14 07:59:50 -05:00
const [ delError ] = await safe ( s3 . deleteObject ( delParams ) . promise ( ) ) ;
if ( delError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Error del object cloudron-testfile. Message: ${ delError . message } HTTP Code: ${ delError . code } ` ) ;
2016-10-11 11:36:25 +02:00
}
2019-02-09 18:08:10 -08:00
function removePrivateFields ( apiConfig ) {
2020-05-14 23:01:44 +02:00
apiConfig . secretAccessKey = constants . SECRET _PLACEHOLDER ;
2019-02-09 18:08:10 -08:00
return apiConfig ;
}
function injectPrivateFields ( newConfig , currentConfig ) {
2020-05-14 23:01:44 +02:00
if ( newConfig . secretAccessKey === constants . SECRET _PLACEHOLDER ) newConfig . secretAccessKey = currentConfig . secretAccessKey ;
2019-02-09 18:08:10 -08:00
}