2017-09-17 17:51:00 +02:00
'use strict' ;
exports = module . exports = {
2017-10-29 11:10:50 +01:00
upload : upload ,
download : download ,
copy : copy ,
2018-07-27 17:08:53 -07:00
listDir : listDir ,
2017-10-29 11:10:50 +01:00
remove : remove ,
removeDir : removeDir ,
2017-09-17 17:51:00 +02:00
testConfig : testConfig ,
2019-02-09 18:08:10 -08:00
removePrivateFields : removePrivateFields ,
injectPrivateFields : injectPrivateFields ,
2017-09-17 17:51:00 +02:00
// Used to mock GCS
_mockInject : mockInject ,
_mockRestore : mockRestore
} ;
var assert = require ( 'assert' ) ,
2017-12-15 17:28:45 +05:30
async = require ( 'async' ) ,
2019-02-09 18:08:10 -08:00
backups = require ( '../backups.js' ) ,
2017-09-17 17:51:00 +02:00
BackupsError = require ( '../backups.js' ) . BackupsError ,
debug = require ( 'debug' ) ( 'box:storage/gcs' ) ,
2017-10-29 11:10:50 +01:00
EventEmitter = require ( 'events' ) ,
2017-12-15 17:28:45 +05:30
GCS = require ( '@google-cloud/storage' ) ,
PassThrough = require ( 'stream' ) . PassThrough ,
path = require ( 'path' ) ;
2017-09-17 17:51:00 +02:00
// test only
var originalGCS ;
function mockInject ( mock ) {
originalGCS = GCS ;
GCS = mock ;
}
function mockRestore ( ) {
GCS = originalGCS ;
}
// internal only
2017-12-16 07:09:15 +05:30
function getBucket ( apiConfig ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-17 17:51:00 +02:00
2017-12-16 07:09:15 +05:30
var gcsConfig = {
projectId : apiConfig . projectId ,
credentials : {
client _email : apiConfig . credentials . client _email ,
private _key : apiConfig . credentials . private _key
}
2017-09-17 17:51:00 +02:00
} ;
2017-12-16 07:09:15 +05:30
return GCS ( gcsConfig ) . bucket ( apiConfig . bucket ) ;
2017-09-17 17:51:00 +02:00
}
2017-10-29 11:10:50 +01:00
// storage api
function upload ( apiConfig , backupFilePath , sourceStream , callback ) {
2017-09-17 17:51:00 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-10-29 11:10:50 +01:00
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
assert . strictEqual ( typeof sourceStream , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-10-31 11:40:00 +01:00
debug ( ` Uploading to ${ backupFilePath } ` ) ;
2017-10-29 11:10:50 +01:00
function done ( error ) {
if ( error ) {
debug ( '[%s] upload: gcp upload error.' , backupFilePath , error ) ;
2017-10-31 11:40:00 +01:00
return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , ` Error uploading ${ backupFilePath } . Message: ${ error . message } HTTP Code: ${ error . code } ` ) ) ;
2017-10-29 11:10:50 +01:00
}
2017-09-17 17:51:00 +02:00
2017-10-29 11:10:50 +01:00
callback ( null ) ;
}
2017-09-17 17:51:00 +02:00
2017-12-16 07:09:15 +05:30
var uploadStream = getBucket ( apiConfig ) . file ( backupFilePath )
. createWriteStream ( { resumable : false } )
. on ( 'finish' , done )
. on ( 'error' , done ) ;
sourceStream . pipe ( uploadStream ) ;
2017-09-17 17:51:00 +02:00
}
2017-10-29 11:10:50 +01:00
function download ( apiConfig , backupFilePath , callback ) {
2017-09-17 17:51:00 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-10-29 11:10:50 +01:00
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
2017-09-17 17:51:00 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-10-31 11:40:00 +01:00
debug ( ` Download ${ backupFilePath } starting ` ) ;
2017-10-29 11:10:50 +01:00
var file = getBucket ( apiConfig ) . file ( backupFilePath ) ;
2017-09-17 17:51:00 +02:00
2017-10-29 11:10:50 +01:00
var ps = new PassThrough ( ) ;
var readStream = file . createReadStream ( )
2017-12-15 17:33:24 +05:30
. on ( 'error' , function ( error ) {
2017-10-29 11:10:50 +01:00
if ( error && error . code == 404 ) {
ps . emit ( 'error' , new BackupsError ( BackupsError . NOT _FOUND ) ) ;
} else {
debug ( '[%s] download: gcp stream error.' , backupFilePath , error ) ;
ps . emit ( 'error' , new BackupsError ( BackupsError . EXTERNAL _ERROR , error ) ) ;
}
} )
;
readStream . pipe ( ps ) ;
2017-09-17 17:51:00 +02:00
2017-10-29 11:10:50 +01:00
callback ( null , ps ) ;
}
2017-09-17 17:51:00 +02:00
2017-12-15 17:33:24 +05:30
function listDir ( apiConfig , backupFilePath , batchSize , iteratorCallback , callback ) {
2017-09-17 17:51:00 +02:00
var bucket = getBucket ( apiConfig ) ;
2017-12-16 07:09:15 +05:30
var query = { prefix : backupFilePath , autoPaginate : batchSize === - 1 } ;
2017-10-31 11:40:00 +01:00
if ( batchSize > 0 ) {
query . maxResults = batchSize ;
}
2017-10-29 11:10:50 +01:00
async . forever ( function listAndDownload ( foreverCallback ) {
bucket . getFiles ( query , function ( error , files , nextQuery ) {
2018-07-27 17:08:53 -07:00
if ( error ) return foreverCallback ( error ) ;
2017-10-29 11:10:50 +01:00
2017-10-31 11:40:00 +01:00
if ( files . length === 0 ) return foreverCallback ( new Error ( 'Done' ) ) ;
2017-10-29 11:10:50 +01:00
2018-07-27 17:08:53 -07:00
const entries = files . map ( function ( f ) { return { fullPath : f . name } ; } ) ;
iteratorCallback ( entries , function ( error ) {
if ( error ) return foreverCallback ( error ) ;
2017-10-31 11:40:00 +01:00
if ( ! nextQuery ) return foreverCallback ( new Error ( 'Done' ) ) ;
2017-10-29 11:10:50 +01:00
query = nextQuery ;
2018-07-27 17:08:53 -07:00
2017-10-29 11:10:50 +01:00
foreverCallback ( ) ;
} ) ;
} ) ;
} , function ( error ) {
if ( error . message === 'Done' ) return callback ( null ) ;
callback ( error ) ;
} ) ;
2017-09-17 17:51:00 +02:00
}
2017-10-29 11:10:50 +01:00
function copy ( apiConfig , oldFilePath , newFilePath ) {
2017-09-17 17:51:00 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-10-29 11:10:50 +01:00
assert . strictEqual ( typeof oldFilePath , 'string' ) ;
assert . strictEqual ( typeof newFilePath , 'string' ) ;
2017-09-17 17:51:00 +02:00
2017-10-29 11:10:50 +01:00
var events = new EventEmitter ( ) , retryCount = 0 ;
2018-07-27 17:08:53 -07:00
function copyFile ( entry , iteratorCallback ) {
var relativePath = path . relative ( oldFilePath , entry . fullPath ) ;
2017-09-17 17:51:00 +02:00
2018-07-27 17:08:53 -07:00
getBucket ( apiConfig ) . file ( entry . fullPath ) . copy ( path . join ( newFilePath , relativePath ) , function ( error ) {
if ( error ) debug ( 'copyBackup: gcs copy error' , error ) ;
2017-10-29 11:10:50 +01:00
2018-02-22 12:24:16 -08:00
if ( error && error . code === 404 ) return iteratorCallback ( new BackupsError ( BackupsError . NOT _FOUND , 'Old backup not found' ) ) ;
2018-07-27 17:08:53 -07:00
if ( error ) return iteratorCallback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-10-29 11:10:50 +01:00
iteratorCallback ( null ) ;
} ) ;
2017-10-31 11:40:20 +01:00
events . emit ( 'progress' , ` Copying ${ relativePath } ... ` ) ;
2017-10-29 11:10:50 +01:00
}
2017-10-31 11:40:00 +01:00
const batchSize = - 1 ;
2017-10-29 11:10:50 +01:00
var total = 0 , concurrency = 4 ;
2018-07-27 17:08:53 -07:00
listDir ( apiConfig , oldFilePath , batchSize , function ( entries , done ) {
total += entries . length ;
2017-09-17 17:51:00 +02:00
2017-10-29 11:10:50 +01:00
if ( retryCount === 0 ) concurrency = Math . min ( concurrency + 1 , 10 ) ; else concurrency = Math . max ( concurrency - 1 , 5 ) ;
2017-10-31 11:40:00 +01:00
events . emit ( 'progress' , ` ${ retryCount } errors. concurrency set to ${ concurrency } ` ) ;
2017-10-29 11:10:50 +01:00
retryCount = 0 ;
2018-07-27 17:08:53 -07:00
async . eachLimit ( entries , concurrency , copyFile , done ) ;
2017-10-29 11:10:50 +01:00
} , function ( error ) {
2017-10-31 11:40:00 +01:00
events . emit ( 'progress' , ` Copied ${ total } files ` ) ;
2017-10-29 11:10:50 +01:00
events . emit ( 'done' , error ) ;
} ) ;
return events ;
}
function remove ( apiConfig , filename , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof filename , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getBucket ( apiConfig )
. file ( filename )
2018-07-27 17:08:53 -07:00
. delete ( function ( error ) {
if ( error ) debug ( 'removeBackups: Unable to remove %s (%s). Not fatal.' , filename , error . message ) ;
2017-09-17 17:51:00 +02:00
callback ( null ) ;
} ) ;
}
2017-10-29 11:10:50 +01:00
function removeDir ( apiConfig , pathPrefix ) {
2017-09-17 17:51:00 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-10-29 11:10:50 +01:00
assert . strictEqual ( typeof pathPrefix , 'string' ) ;
2017-09-17 17:51:00 +02:00
2017-10-29 11:10:50 +01:00
var events = new EventEmitter ( ) , retryCount = 0 ;
2017-09-17 17:51:00 +02:00
2017-10-29 11:10:50 +01:00
const batchSize = 1 ;
var total = 0 , concurrency = 4 ;
2018-07-27 17:08:53 -07:00
listDir ( apiConfig , pathPrefix , batchSize , function ( entries , done ) {
total += entries . length ;
2017-10-29 11:10:50 +01:00
if ( retryCount === 0 ) concurrency = Math . min ( concurrency + 1 , 10 ) ; else concurrency = Math . max ( concurrency - 1 , 5 ) ;
2017-10-31 11:40:00 +01:00
events . emit ( 'progress' , ` ${ retryCount } errors. concurrency set to ${ concurrency } ` ) ;
2017-10-29 11:10:50 +01:00
retryCount = 0 ;
2018-07-27 17:08:53 -07:00
async . eachLimit ( entries , concurrency , function ( entry , iteratorCallback ) {
remove ( apiConfig , entry . fullPath , iteratorCallback ) ;
} , done ) ;
2017-10-29 11:10:50 +01:00
} , function ( error ) {
2017-10-31 11:40:00 +01:00
events . emit ( 'progress' , ` Deleted ${ total } files ` ) ;
2017-10-29 11:10:50 +01:00
events . emit ( 'done' , error ) ;
2017-09-17 17:51:00 +02:00
} ) ;
2017-10-29 11:10:50 +01:00
return events ;
2017-09-17 17:51:00 +02:00
}
function testConfig ( apiConfig , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( typeof apiConfig . projectId !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'projectId must be a string' ) ) ;
2017-12-16 07:09:15 +05:30
if ( ! apiConfig . credentials || typeof apiConfig . credentials !== 'object' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'credentials must be an object' ) ) ;
if ( typeof apiConfig . credentials . client _email !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'credentials.client_email must be a string' ) ) ;
if ( typeof apiConfig . credentials . private _key !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'credentials.private_key must be a string' ) ) ;
2017-09-17 17:51:00 +02:00
if ( typeof apiConfig . bucket !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'bucket must be a string' ) ) ;
if ( typeof apiConfig . prefix !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'prefix must be a string' ) ) ;
// attempt to upload and delete a file with new credentials
var bucket = getBucket ( apiConfig ) ;
2017-10-31 11:40:00 +01:00
2017-09-17 17:51:00 +02:00
var testFile = bucket . file ( path . join ( apiConfig . prefix , 'cloudron-testfile' ) ) ;
2017-10-31 11:40:00 +01:00
2017-12-16 21:24:37 +05:30
var uploadStream = testFile . createWriteStream ( { resumable : false } ) ;
uploadStream . write ( 'testfilecontents' ) ;
uploadStream . end ( ) ;
uploadStream . on ( 'error' , function ( error ) {
debug ( 'testConfig: failed uploading cloudron-testfile' , error ) ;
if ( error && error . code && ( error . code == 403 || error . code == 404 ) ) {
return callback ( new BackupsError ( BackupsError . BAD _FIELD , error . message ) ) ;
}
2017-10-31 11:40:00 +01:00
2017-12-16 21:24:37 +05:30
return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
} ) ;
2017-09-17 17:51:00 +02:00
2017-12-16 21:24:37 +05:30
uploadStream . on ( 'finish' , function ( ) {
debug ( 'testConfig: uploaded cloudron-testfile ' + JSON . stringify ( arguments ) ) ;
bucket . file ( path . join ( apiConfig . prefix , 'cloudron-testfile' ) ) . delete ( function ( error ) {
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
debug ( 'testConfig: deleted cloudron-testfile' ) ;
callback ( ) ;
} ) ;
} ) ;
2017-09-17 17:51:00 +02:00
}
2019-02-09 18:08:10 -08:00
function removePrivateFields ( apiConfig ) {
apiConfig . credentials . private _key = backups . SECRET _PLACEHOLDER ;
return apiConfig ;
}
function injectPrivateFields ( newConfig , currentConfig ) {
if ( newConfig . credentials . private _key === backups . SECRET _PLACEHOLDER && currentConfig . credentials ) newConfig . credentials . private _key = currentConfig . credentials . private _key ;
}