2016-09-15 11:29:45 +02:00
'use strict' ;
exports = module . exports = {
2020-06-05 13:27:18 +02:00
getBackupPath : getBackupPath ,
2020-06-08 16:25:00 +02:00
checkPreconditions : checkPreconditions ,
2020-06-05 13:27:18 +02:00
2017-09-17 18:50:29 -07:00
upload : upload ,
download : download ,
2017-09-23 14:27:35 -07:00
2017-09-17 18:50:29 -07:00
copy : copy ,
2018-07-27 14:29:07 -07:00
listDir : listDir ,
2017-09-23 11:09:36 -07:00
remove : remove ,
2017-09-27 17:34:49 -07:00
removeDir : removeDir ,
2016-09-16 11:21:08 +02:00
2019-02-09 18:08:10 -08:00
testConfig : testConfig ,
removePrivateFields : removePrivateFields ,
injectPrivateFields : injectPrivateFields
2016-09-15 11:29:45 +02:00
} ;
2020-06-08 17:08:26 +02:00
const PROVIDER _FILESYSTEM = 'filesystem' ;
const PROVIDER _SSHFS = 'sshfs' ;
const PROVIDER _CIFS = 'cifs' ;
2017-04-20 15:15:49 +02:00
var assert = require ( 'assert' ) ,
2019-10-22 20:36:20 -07:00
BoxError = require ( '../boxerror.js' ) ,
2020-06-08 16:25:00 +02:00
DataLayout = require ( '../datalayout.js' ) ,
2017-04-11 11:00:55 +02:00
debug = require ( 'debug' ) ( 'box:storage/filesystem' ) ,
2020-06-08 16:25:00 +02:00
df = require ( '@sindresorhus/df' ) ,
2017-10-04 11:00:30 -07:00
EventEmitter = require ( 'events' ) ,
2016-09-16 11:55:59 +02:00
fs = require ( 'fs' ) ,
2017-04-18 15:32:59 +02:00
path = require ( 'path' ) ,
2020-06-08 16:25:00 +02:00
prettyBytes = require ( 'pretty-bytes' ) ,
2018-07-27 13:55:54 -07:00
readdirp = require ( 'readdirp' ) ,
2017-04-20 16:59:17 -07:00
safe = require ( 'safetydance' ) ,
2017-09-20 09:57:16 -07:00
shell = require ( '../shell.js' ) ;
2016-09-16 11:55:59 +02:00
2017-04-18 15:32:59 +02:00
// storage api
2020-06-05 13:27:18 +02:00
function getBackupPath ( apiConfig ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2020-06-08 17:08:26 +02:00
if ( apiConfig . provider === PROVIDER _SSHFS ) return path . join ( apiConfig . mountPoint , apiConfig . prefix ) ;
if ( apiConfig . provider === PROVIDER _CIFS ) return path . join ( apiConfig . mountPoint , apiConfig . prefix ) ;
2020-06-05 13:27:18 +02:00
return apiConfig . backupFolder ;
}
2020-06-08 16:25:00 +02:00
// the du call in the function below requires root
function checkPreconditions ( apiConfig , dataLayout , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-06-08 17:08:26 +02:00
// TODO check filesystem is mounted for sshfs and cifs so we don't write into the actual folder on disk
2020-06-08 17:46:52 +02:00
if ( apiConfig . provider === PROVIDER _SSHFS || apiConfig . provider === PROVIDER _CIFS ) {
const mounts = safe . fs . readFileSync ( '/proc/mounts' , 'utf8' ) ;
const mountInfo = mounts . split ( '\n' ) . filter ( function ( l ) { return l . indexOf ( apiConfig . mountPoint ) !== - 1 ; } ) [ 0 ] ;
if ( ! mountInfo ) return callback ( new BoxError ( BoxError . FS _ERROR , ` ${ apiConfig . mountPoint } is not mounted ` ) ) ;
}
2020-06-08 17:08:26 +02:00
2020-06-08 16:25:00 +02:00
let used = 0 ;
for ( let localPath of dataLayout . localPaths ( ) ) {
debug ( ` checkPreconditions: getting disk usage of ${ localPath } ` ) ;
let result = safe . child _process . execSync ( ` du -Dsb ${ localPath } ` , { encoding : 'utf8' } ) ;
if ( ! result ) return callback ( new BoxError ( BoxError . FS _ERROR , safe . error ) ) ;
used += parseInt ( result , 10 ) ;
}
debug ( ` checkPreconditions: ${ used } bytes ` ) ;
2020-06-08 17:08:26 +02:00
df . file ( getBackupPath ( apiConfig ) ) . then ( function ( diskUsage ) {
2020-06-08 16:25:00 +02:00
const needed = used + ( 1024 * 1024 * 1024 ) ; // check if there is atleast 1GB left afterwards
if ( diskUsage . available <= needed ) return callback ( new BoxError ( BoxError . FS _ERROR , ` Not enough disk space for backup. Needed: ${ prettyBytes ( needed ) } Available: ${ prettyBytes ( diskUsage . available ) } ` ) ) ;
callback ( null ) ;
} ) . catch ( function ( error ) {
callback ( new BoxError ( BoxError . FS _ERROR , error ) ) ;
} ) ;
}
2017-09-20 09:57:16 -07:00
function upload ( apiConfig , backupFilePath , sourceStream , callback ) {
2016-09-15 11:29:45 +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-15 11:29:45 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-06-11 08:27:48 -07:00
fs . mkdir ( path . dirname ( backupFilePath ) , { recursive : true } , function ( error ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2016-09-16 11:21:08 +02:00
2017-09-22 14:40:37 -07:00
safe . fs . unlinkSync ( backupFilePath ) ; // remove any hardlink
2017-04-20 16:59:17 -07:00
var fileStream = fs . createWriteStream ( backupFilePath ) ;
2016-09-16 11:21:08 +02:00
2017-09-27 19:31:07 -07:00
// this pattern is required to ensure that the file got created before 'finish'
fileStream . on ( 'open' , function ( ) {
sourceStream . pipe ( fileStream ) ;
} ) ;
2017-04-20 15:35:52 +02:00
fileStream . on ( 'error' , function ( error ) {
2017-09-20 09:57:16 -07:00
debug ( '[%s] upload: out stream error.' , backupFilePath , error ) ;
2019-10-22 20:36:20 -07:00
callback ( new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:35:52 +02:00
} ) ;
2017-09-26 16:42:54 -07:00
fileStream . on ( 'finish' , function ( ) {
2017-09-27 14:44:48 -07:00
// in test, upload() may or may not be called via sudo script
const BACKUP _UID = parseInt ( process . env . SUDO _UID , 10 ) || process . getuid ( ) ;
2020-06-08 17:08:26 +02:00
// sshfs and cifs handle ownership through the mount args
if ( apiConfig . provider === PROVIDER _FILESYSTEM && ! safe . fs . chownSync ( backupFilePath , BACKUP _UID , BACKUP _UID ) ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Unable to chown:' + safe . error . message ) ) ;
if ( apiConfig . provider === PROVIDER _FILESYSTEM && ! safe . fs . chownSync ( path . dirname ( backupFilePath ) , BACKUP _UID , BACKUP _UID ) ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Unable to chown:' + safe . error . message ) ) ;
2017-04-20 16:59:17 -07:00
2017-09-27 14:44:48 -07:00
debug ( 'upload %s: done.' , backupFilePath ) ;
2017-04-20 16:59:17 -07:00
2017-09-27 14:44:48 -07:00
callback ( null ) ;
2017-04-11 11:00:55 +02:00
} ) ;
} ) ;
2016-09-16 11:21:08 +02:00
}
2017-09-20 09:57:16 -07:00
function download ( apiConfig , sourceFilePath , callback ) {
2016-09-15 11:29:45 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof sourceFilePath , 'string' ) ;
2016-09-15 11:29:45 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-07-30 07:39:34 -07:00
debug ( ` download: ${ sourceFilePath } ` ) ;
2017-04-11 11:00:55 +02:00
2019-10-22 20:36:20 -07:00
if ( ! safe . fs . existsSync ( sourceFilePath ) ) return callback ( new BoxError ( BoxError . NOT _FOUND , ` File not found: ${ sourceFilePath } ` ) ) ;
2017-09-28 14:26:39 -07:00
2017-04-21 15:28:25 -07:00
var fileStream = fs . createReadStream ( sourceFilePath ) ;
2017-09-28 14:26:39 -07:00
callback ( null , fileStream ) ;
2017-04-17 14:46:19 +02:00
}
2018-07-27 13:55:54 -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' ) ;
var entries = [ ] ;
2019-04-24 10:40:33 -07:00
var entryStream = readdirp ( dir , { type : 'files' , alwaysStat : true , lstat : true } ) ;
entryStream . on ( 'data' , function ( entryInfo ) {
if ( entryInfo . stats . isSymbolicLink ( ) ) return ;
2018-08-02 14:59:50 -07:00
2019-04-24 10:40:33 -07:00
entries . push ( { fullPath : entryInfo . fullPath } ) ;
2018-07-27 13:55:54 -07:00
if ( entries . length < batchSize ) return ;
entryStream . pause ( ) ;
iteratorCallback ( entries , function ( error ) {
if ( error ) return callback ( error ) ;
entries = [ ] ;
entryStream . resume ( ) ;
} ) ;
} ) ;
entryStream . on ( 'warn' , function ( error ) {
debug ( 'listDir: warning ' , error ) ;
} ) ;
entryStream . on ( 'end' , function ( ) {
iteratorCallback ( entries , callback ) ;
} ) ;
}
2017-10-04 11:00:30 -07:00
function copy ( apiConfig , oldFilePath , newFilePath ) {
2017-04-17 14:46:19 +02: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-04-17 14:46:19 +02:00
2017-10-04 11:00:30 -07:00
var events = new EventEmitter ( ) ;
2020-06-11 08:27:48 -07:00
fs . mkdir ( path . dirname ( newFilePath ) , { recursive : true } , function ( error ) {
2019-10-22 20:36:20 -07:00
if ( error ) return events . emit ( 'done' , new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-17 14:46:19 +02:00
2019-04-03 11:54:46 -07:00
events . emit ( 'progress' , ` Copying ${ oldFilePath } to ${ newFilePath } ` ) ;
2020-06-08 17:08:26 +02:00
// sshfs and cifs do not allow preserving attributes
var cpOptions = apiConfig . provider === PROVIDER _FILESYSTEM ? '-a' : '-dR' ;
2020-06-05 13:45:25 +02:00
2017-09-18 12:42:42 -07:00
// this will hardlink backups saving space
2020-06-05 13:45:25 +02:00
cpOptions += apiConfig . noHardlinks ? '' : 'l' ;
2018-11-17 19:26:19 -08:00
shell . spawn ( 'copy' , '/bin/cp' , [ cpOptions , oldFilePath , newFilePath ] , { } , function ( error ) {
2019-10-22 20:36:20 -07:00
if ( error ) return events . emit ( 'done' , new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 16:59:17 -07:00
2017-10-04 11:00:30 -07:00
events . emit ( 'done' , null ) ;
2017-04-20 16:59:17 -07:00
} ) ;
2017-04-20 15:41:25 +02:00
} ) ;
2017-10-04 11:00:30 -07:00
return events ;
2016-09-15 11:29:45 +02:00
}
2017-09-27 17:34:49 -07:00
function remove ( apiConfig , filename , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof filename , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-28 20:55:39 -07:00
var stat = safe . fs . statSync ( filename ) ;
if ( ! stat ) return callback ( ) ;
2017-09-27 17:34:49 -07:00
2017-09-28 20:55:39 -07:00
if ( stat . isFile ( ) ) {
2019-10-22 20:36:20 -07:00
if ( ! safe . fs . unlinkSync ( filename ) ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , safe . error . message ) ) ;
2017-09-28 20:55:39 -07:00
} else if ( stat . isDirectory ( ) ) {
2019-10-22 20:36:20 -07:00
if ( ! safe . fs . rmdirSync ( filename ) ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , safe . error . message ) ) ;
2017-09-28 20:55:39 -07:00
}
2017-09-27 17:34:49 -07:00
2017-10-02 20:08:00 -07:00
callback ( null ) ;
2017-09-27 17:34:49 -07:00
}
2017-10-10 20:23:04 -07:00
function removeDir ( apiConfig , pathPrefix ) {
2017-04-17 15:45:58 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-23 11:09:36 -07:00
assert . strictEqual ( typeof pathPrefix , 'string' ) ;
2017-10-10 20:23:04 -07:00
var events = new EventEmitter ( ) ;
2019-04-03 11:54:46 -07:00
process . nextTick ( ( ) => events . emit ( 'progress' , ` Removing directory ${ pathPrefix } ` ) ) ;
2017-04-17 15:45:58 +02:00
2018-11-17 19:26:19 -08:00
shell . spawn ( 'removeDir' , '/bin/rm' , [ '-rf' , pathPrefix ] , { } , function ( error ) {
2019-10-22 20:36:20 -07:00
if ( error ) return events . emit ( 'done' , new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-23 11:34:46 -07:00
2017-10-10 20:23:04 -07:00
events . emit ( 'done' , null ) ;
2017-09-23 11:09:36 -07:00
} ) ;
2017-10-10 20:23:04 -07:00
return events ;
2017-04-17 15:45:58 +02:00
}
2016-10-11 11:36:25 +02:00
function testConfig ( apiConfig , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-06-08 17:08:26 +02:00
if ( apiConfig . provider === PROVIDER _FILESYSTEM ) {
if ( ! apiConfig . backupFolder || typeof apiConfig . backupFolder !== 'string' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'backupFolder must be non-empty string' , { field : 'backupFolder' } ) ) ;
if ( 'externalDisk' in apiConfig && typeof apiConfig . externalDisk !== 'boolean' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'externalDisk must be boolean' , { field : 'externalDisk' } ) ) ;
}
2016-10-11 11:36:25 +02:00
2020-06-08 17:08:26 +02:00
if ( apiConfig . provider === PROVIDER _SSHFS || apiConfig . provider === PROVIDER _CIFS ) {
if ( ! apiConfig . mountPoint || typeof apiConfig . mountPoint !== 'string' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'mountPoint must be non-empty string' , { field : 'mountPoint' } ) ) ;
if ( typeof apiConfig . prefix !== 'string' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'prefix must be a string' , { field : 'prefix' } ) ) ;
2017-04-21 15:37:57 +02:00
2020-06-08 17:46:52 +02:00
const mounts = safe . fs . readFileSync ( '/proc/mounts' , 'utf8' ) ;
const mountInfo = mounts . split ( '\n' ) . filter ( function ( l ) { return l . indexOf ( apiConfig . mountPoint ) !== - 1 ; } ) [ 0 ] ;
if ( ! mountInfo ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'mountPoint is not mounted' , { field : 'mountPoint' } ) ) ;
if ( apiConfig . provider === PROVIDER _SSHFS && ! mountInfo . split ( ' ' ) . find ( i => i === 'fuse.sshfs' ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'mountPoint must be a "fuse.sshfs" filesystem' , { field : 'mountPoint' } ) ) ;
if ( apiConfig . provider === PROVIDER _CIFS && ! mountInfo . split ( ' ' ) . find ( i => i === 'cifs' ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'mountPoint must be a "cifs" filesystem' , { field : 'mountPoint' } ) ) ;
2020-06-08 17:08:26 +02:00
}
2017-10-16 15:15:15 -07:00
2020-06-08 17:08:26 +02:00
// common checks
const backupPath = getBackupPath ( apiConfig ) ;
const field = apiConfig . provider === PROVIDER _FILESYSTEM ? 'backupFolder' : 'prefix' ;
2018-06-07 11:13:57 -07:00
2020-06-08 17:08:26 +02:00
const stat = safe . fs . statSync ( backupPath ) ;
if ( ! stat ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Directory does not exist or cannot be accessed: ' + safe . error . message ) , { field } ) ;
if ( ! stat . isDirectory ( ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Backup location is not a directory' , { field } ) ) ;
2017-04-21 15:37:57 +02:00
2020-06-08 17:08:26 +02:00
if ( ! safe . fs . mkdirSync ( path . join ( backupPath , 'snapshot' ) ) && safe . error . code !== 'EEXIST' ) {
if ( safe . error && safe . error . code === 'EACCES' ) return callback ( new BoxError ( BoxError . BAD _FIELD , ` Access denied. Run "chown yellowtent:yellowtent ${ backupPath } " on the server ` , { field } ) ) ;
return callback ( new BoxError ( BoxError . BAD _FIELD , safe . error . message , { field } ) ) ;
2020-05-26 14:57:20 -07:00
}
2017-10-26 11:27:36 -07:00
2020-06-08 17:08:26 +02:00
if ( ! safe . fs . writeFileSync ( path . join ( backupPath , 'cloudron-testfile' ) , 'testcontent' ) ) {
return callback ( new BoxError ( BoxError . BAD _FIELD , ` Unable to create test file as 'yellowtent' user in ${ backupPath } : ${ safe . error . message } . Check dir/mount permissions ` , { field } ) ) ;
2020-05-26 14:57:20 -07:00
}
2020-06-08 17:08:26 +02:00
if ( ! safe . fs . unlinkSync ( path . join ( backupPath , 'cloudron-testfile' ) ) ) {
return callback ( new BoxError ( BoxError . BAD _FIELD , ` Unable to remove test file as 'yellowtent' user in ${ backupPath } : ${ safe . error . message } . Check dir/mount permissions ` , { field } ) ) ;
2020-05-26 14:57:20 -07:00
}
2020-06-08 17:08:26 +02:00
if ( 'noHardlinks' in apiConfig && typeof apiConfig . noHardlinks !== 'boolean' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'noHardlinks must be boolean' , { field : 'noHardLinks' } ) ) ;
2020-05-26 14:57:20 -07:00
callback ( null ) ;
2016-10-11 11:36:25 +02:00
}
2017-01-04 16:22:58 -08:00
2019-02-09 18:08:10 -08:00
function removePrivateFields ( apiConfig ) {
return apiConfig ;
}
function injectPrivateFields ( /* newConfig, currentConfig */ ) {
}