2016-09-15 11:29:45 +02:00
'use strict' ;
exports = module . exports = {
2023-07-15 08:59:58 +05:30
getProviderStatus ,
2022-10-02 17:22:44 +02:00
getAvailableSize ,
2020-06-05 13:27:18 +02:00
2021-02-18 16:51:43 -08:00
upload ,
download ,
2017-09-23 14:27:35 -07:00
2021-02-18 16:51:43 -08:00
copy ,
2017-09-17 18:50:29 -07:00
2021-02-18 16:51:43 -08:00
exists ,
listDir ,
2018-07-27 14:29:07 -07:00
2021-02-18 16:51:43 -08:00
remove ,
removeDir ,
2016-09-16 11:21:08 +02:00
2021-02-18 16:51:43 -08:00
testConfig ,
removePrivateFields ,
injectPrivateFields
2016-09-15 11:29:45 +02:00
} ;
2020-06-08 17:08:26 +02:00
const PROVIDER _FILESYSTEM = 'filesystem' ;
2021-05-17 12:51:23 -07:00
const PROVIDER _MOUNTPOINT = 'mountpoint' ;
2020-06-08 17:08:26 +02:00
const PROVIDER _SSHFS = 'sshfs' ;
const PROVIDER _CIFS = 'cifs' ;
2022-06-08 10:32:25 -07:00
const PROVIDER _XFS = 'xfs' ;
2023-08-08 13:21:56 +02:00
const PROVIDER _DISK = 'disk' ; // replaces xfs and ext4
2020-06-22 15:51:05 +02:00
const PROVIDER _NFS = 'nfs' ;
2021-05-17 15:58:38 -07:00
const PROVIDER _EXT4 = 'ext4' ;
2020-06-08 17:08:26 +02:00
2021-05-17 15:58:38 -07:00
const assert = require ( 'assert' ) ,
2019-10-22 20:36:20 -07:00
BoxError = require ( '../boxerror.js' ) ,
2021-05-17 15:58:38 -07:00
constants = require ( '../constants.js' ) ,
2017-04-11 11:00:55 +02:00
debug = require ( 'debug' ) ( 'box:storage/filesystem' ) ,
2022-10-18 19:32:07 +02:00
df = require ( '../df.js' ) ,
2016-09-16 11:55:59 +02:00
fs = require ( 'fs' ) ,
2021-10-11 17:45:35 +02:00
mounts = require ( '../mounts.js' ) ,
2017-04-18 15:32:59 +02:00
path = require ( 'path' ) ,
2020-12-01 12:11:55 -08:00
paths = require ( '../paths.js' ) ,
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
2023-07-15 08:59:58 +05:30
async function getProviderStatus ( apiConfig ) {
2020-06-08 16:25:00 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2022-10-02 16:51:03 +02:00
// Check filesystem is mounted so we don't write into the actual folder on disk
2022-11-05 08:43:02 +01:00
if ( ! mounts . isManagedProvider ( apiConfig . provider ) && apiConfig . provider !== 'mountpoint' ) return await mounts . getStatus ( apiConfig . provider , apiConfig . backupFolder ) ;
2022-10-02 16:33:04 +02:00
2022-11-05 08:43:02 +01:00
const hostPath = mounts . isManagedProvider ( apiConfig . provider ) ? paths . MANAGED _BACKUP _MOUNT _DIR : apiConfig . mountPoint ;
return await mounts . getStatus ( apiConfig . provider , hostPath ) ; // { state, message }
2022-10-02 17:22:44 +02:00
}
2022-10-02 16:16:30 +02:00
2022-10-02 17:22:44 +02:00
// the du call in the function below requires root
async function getAvailableSize ( apiConfig ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2020-06-08 16:25:00 +02:00
2023-08-15 20:24:54 +05:30
const [ error , dfResult ] = await safe ( df . file ( apiConfig . rootPath ) ) ;
2022-10-02 17:22:44 +02:00
if ( error ) throw new BoxError ( BoxError . FS _ERROR , ` Error when checking for disk space: ${ error . message } ` ) ;
2020-06-08 16:25:00 +02:00
2022-10-02 17:22:44 +02:00
return dfResult . available ;
2020-06-08 16:25:00 +02:00
}
2021-07-10 08:36:30 -07:00
function hasChownSupportSync ( apiConfig ) {
switch ( apiConfig . provider ) {
case PROVIDER _NFS :
case PROVIDER _EXT4 :
2022-06-08 10:32:25 -07:00
case PROVIDER _XFS :
2023-08-08 13:21:56 +02:00
case PROVIDER _DISK :
2021-07-10 08:36:30 -07:00
case PROVIDER _FILESYSTEM :
return true ;
case PROVIDER _SSHFS :
// sshfs can be mounted as root or normal user. when mounted as root, we have to chown since we remove backups as the yellowtent user
// when mounted as non-root user, files are created as yellowtent user but they are still owned by the non-root user (thus del also works)
2021-07-10 11:13:12 -07:00
return apiConfig . mountOptions . user === 'root' ;
2021-07-10 08:36:30 -07:00
case PROVIDER _CIFS :
return true ;
case PROVIDER _MOUNTPOINT :
return apiConfig . chown ;
}
}
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 ) {
2023-04-16 10:49:59 +02:00
debug ( ` upload: [ ${ backupFilePath } ] out stream error. %o ` , 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 ( ) {
2021-06-22 09:27:11 -07:00
const backupUid = parseInt ( process . env . SUDO _UID , 10 ) || process . getuid ( ) ; // in test, upload() may or may not be called via sudo script
2017-09-27 14:44:48 -07:00
2021-07-10 08:36:30 -07:00
if ( hasChownSupportSync ( apiConfig ) ) {
2021-06-22 09:27:11 -07:00
if ( ! safe . fs . chownSync ( backupFilePath , backupUid , backupUid ) ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Unable to chown:' + safe . error . message ) ) ;
if ( ! safe . fs . chownSync ( path . dirname ( backupFilePath ) , backupUid , backupUid ) ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Unable to chown:' + safe . error . message ) ) ;
2020-12-18 17:14:31 -08:00
}
2017-04-20 16:59:17 -07:00
2023-04-16 10:49:59 +02:00
debug ( ` upload ${ backupFilePath } : done ` ) ;
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
}
2023-07-24 22:25:06 +05:30
async function download ( apiConfig , sourceFilePath ) {
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
2018-07-30 07:39:34 -07:00
debug ( ` download: ${ sourceFilePath } ` ) ;
2017-04-11 11:00:55 +02:00
2023-07-24 22:25:06 +05:30
if ( ! safe . fs . existsSync ( sourceFilePath ) ) throw new BoxError ( BoxError . NOT _FOUND , ` File not found: ${ sourceFilePath } ` ) ;
2017-09-28 14:26:39 -07:00
2023-07-24 22:25:06 +05:30
return fs . createReadStream ( sourceFilePath ) ;
2017-04-17 14:46:19 +02:00
}
2022-04-14 08:07:03 -05:00
async function exists ( apiConfig , sourceFilePath ) {
2021-02-18 16:51:43 -08:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof sourceFilePath , 'string' ) ;
// do not use existsSync because it does not return EPERM etc
if ( ! safe . fs . statSync ( sourceFilePath ) ) {
2022-04-14 08:07:03 -05:00
if ( safe . error && safe . error . code === 'ENOENT' ) return false ;
if ( safe . error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Exists ${ sourceFilePath } : ${ safe . error . message } ` ) ;
2021-02-18 16:51:43 -08:00
}
2022-04-14 08:07:03 -05:00
return true ;
2021-02-18 16:51:43 -08: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 ) {
2023-04-16 10:49:59 +02:00
debug ( 'listDir: warning. %o' , error ) ;
2018-07-27 13:55:54 -07:00
} ) ;
entryStream . on ( 'end' , function ( ) {
iteratorCallback ( entries , callback ) ;
} ) ;
}
2022-04-30 16:01:42 -07:00
async function copy ( apiConfig , oldFilePath , newFilePath , progressCallback ) {
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' ) ;
2022-04-30 16:01:42 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-04-17 14:46:19 +02:00
2022-04-30 16:01:42 -07:00
const [ mkdirError ] = await safe ( fs . promises . mkdir ( path . dirname ( newFilePath ) , { recursive : true } ) ) ;
if ( mkdirError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , mkdirError . message ) ;
2017-10-04 11:00:30 -07:00
2022-04-30 16:01:42 -07:00
progressCallback ( { message : ` Copying ${ oldFilePath } to ${ newFilePath } ` } ) ;
2017-04-17 14:46:19 +02:00
2022-04-30 16:01:42 -07:00
let cpOptions = ( ( apiConfig . provider !== PROVIDER _MOUNTPOINT && apiConfig . provider !== PROVIDER _CIFS ) || apiConfig . preserveAttributes ) ? '-a' : '-dR' ;
cpOptions += apiConfig . noHardlinks ? '' : 'l' ; // this will hardlink backups saving space
2019-04-03 11:54:46 -07:00
2022-04-30 16:01:42 -07:00
const [ copyError ] = await safe ( shell . promises . spawn ( 'copy' , '/bin/cp' , [ cpOptions , oldFilePath , newFilePath ] , { } ) ) ;
if ( copyError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , copyError . message ) ;
2016-09-15 11:29:45 +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 16:07:01 -05:00
const stat = safe . fs . statSync ( filename ) ;
if ( ! stat ) return ;
2017-09-27 17:34:49 -07:00
2017-09-28 20:55:39 -07:00
if ( stat . isFile ( ) ) {
2022-04-14 16:07:01 -05:00
if ( ! safe . fs . unlinkSync ( filename ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , safe . error . message ) ;
2017-09-28 20:55:39 -07:00
} else if ( stat . isDirectory ( ) ) {
2022-04-14 16:07:01 -05:00
if ( ! safe . fs . rmSync ( filename , { recursive : true } ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , safe . error . message ) ;
2017-09-28 20:55:39 -07:00
}
2017-09-27 17:34:49 -07:00
}
2022-04-14 16:07:01 -05:00
async function removeDir ( apiConfig , pathPrefix , progressCallback ) {
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' ) ;
2022-04-14 16:07:01 -05:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-10-10 20:23:04 -07:00
2022-04-14 16:07:01 -05:00
progressCallback ( { message : ` Removing directory ${ pathPrefix } ` } ) ;
2017-10-10 20:23:04 -07:00
2022-04-14 16:07:01 -05:00
const [ error ] = await safe ( shell . promises . spawn ( 'removeDir' , '/bin/rm' , [ '-rf' , pathPrefix ] , { } ) ) ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ;
2017-04-17 15:45:58 +02:00
}
2020-12-01 12:11:55 -08:00
function validateBackupTarget ( folder ) {
assert . strictEqual ( typeof folder , 'string' ) ;
2022-02-07 13:19:59 -08:00
if ( path . normalize ( folder ) !== folder ) return new BoxError ( BoxError . BAD _FIELD , 'backupFolder/mountpoint must contain a normalized path' ) ;
if ( ! path . isAbsolute ( folder ) ) return new BoxError ( BoxError . BAD _FIELD , 'backupFolder/mountpoint must be an absolute path' ) ;
2020-12-01 12:11:55 -08:00
2022-02-07 13:19:59 -08:00
if ( folder === '/' ) return new BoxError ( BoxError . BAD _FIELD , 'backupFolder/mountpoint cannot be /' ) ;
2020-12-01 12:11:55 -08:00
if ( ! folder . endsWith ( '/' ) ) folder = folder + '/' ; // ensure trailing slash for the prefix matching to work
const PROTECTED _PREFIXES = [ '/boot/' , '/usr/' , '/bin/' , '/lib/' , '/root/' , '/var/lib/' , paths . baseDir ( ) ] ;
2022-02-07 13:19:59 -08:00
if ( PROTECTED _PREFIXES . some ( p => folder . startsWith ( p ) ) ) return new BoxError ( BoxError . BAD _FIELD , 'backupFolder path is protected' ) ;
2020-12-01 12:11:55 -08:00
return null ;
}
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 ( 'noHardlinks' in apiConfig && typeof apiConfig . noHardlinks !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'noHardlinks must be boolean' ) ;
if ( 'chown' in apiConfig && typeof apiConfig . chown !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'chown must be boolean' ) ;
2021-05-17 15:58:38 -07:00
2020-06-08 17:08:26 +02:00
if ( apiConfig . provider === PROVIDER _FILESYSTEM ) {
2022-04-14 07:59:50 -05:00
if ( ! apiConfig . backupFolder || typeof apiConfig . backupFolder !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'backupFolder must be non-empty string' ) ;
const error = validateBackupTarget ( apiConfig . backupFolder ) ;
if ( error ) throw error ;
2022-06-08 10:32:25 -07:00
} else { // xfs/cifs/ext4/nfs/mountpoint/sshfs
2022-01-26 12:59:34 -08:00
if ( apiConfig . provider === PROVIDER _MOUNTPOINT ) {
2022-04-14 07:59:50 -05:00
if ( ! apiConfig . mountPoint || typeof apiConfig . mountPoint !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'mountPoint must be non-empty string' ) ;
const error = validateBackupTarget ( apiConfig . mountPoint ) ;
if ( error ) throw error ;
2022-01-26 12:59:34 -08:00
}
2020-12-01 12:11:55 -08: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' ) ;
2020-12-18 14:41:59 -08:00
if ( apiConfig . prefix !== '' ) {
2022-04-14 07:59:50 -05:00
if ( path . isAbsolute ( apiConfig . prefix ) ) throw new BoxError ( BoxError . BAD _FIELD , 'prefix must be a relative path' ) ;
if ( path . normalize ( apiConfig . prefix ) !== apiConfig . prefix ) throw new BoxError ( BoxError . BAD _FIELD , 'prefix must contain a normalized relative path' ) ;
2020-12-18 14:41:59 -08:00
}
2020-06-08 17:08:26 +02:00
}
2017-10-16 15:15:15 -07:00
2021-06-22 15:28:48 -07:00
if ( apiConfig . provider === PROVIDER _MOUNTPOINT ) {
2022-04-14 07:59:50 -05:00
if ( ! safe . child _process . execSync ( ` mountpoint -q -- ${ apiConfig . mountPoint } ` ) ) throw new BoxError ( BoxError . BAD _FIELD , ` ${ apiConfig . mountPoint } is not mounted ` ) ;
2021-06-22 15:28:48 -07:00
}
2023-08-15 20:24:54 +05:30
const rootPath = apiConfig . rootPath ;
2021-05-26 23:01:05 -07:00
const field = apiConfig . provider === PROVIDER _FILESYSTEM ? 'backupFolder' : 'mountPoint' ;
2017-04-21 15:37:57 +02:00
2023-08-15 20:24:54 +05:30
if ( ! safe . fs . mkdirSync ( path . join ( rootPath , 'snapshot' ) , { recursive : true } ) && safe . error . code !== 'EEXIST' ) {
if ( safe . error && safe . error . code === 'EACCES' ) throw new BoxError ( BoxError . BAD _FIELD , ` Access denied. Create the directory and run "chown yellowtent:yellowtent ${ rootPath } " on the server ` , { field } ) ;
2022-04-14 07:59:50 -05:00
throw new BoxError ( BoxError . BAD _FIELD , safe . error . message , { field } ) ;
2021-05-26 23:01:05 -07:00
}
2017-10-26 11:27:36 -07:00
2023-08-15 20:24:54 +05:30
if ( ! safe . fs . writeFileSync ( path . join ( rootPath , 'cloudron-testfile' ) , 'testcontent' ) ) {
throw new BoxError ( BoxError . BAD _FIELD , ` Unable to create test file as 'yellowtent' user in ${ rootPath } : ${ safe . error . message } . Check dir/mount permissions ` , { field } ) ;
2021-05-26 23:01:05 -07:00
}
2020-05-26 14:57:20 -07:00
2023-08-15 20:24:54 +05:30
if ( ! safe . fs . unlinkSync ( path . join ( rootPath , 'cloudron-testfile' ) ) ) {
throw new BoxError ( BoxError . BAD _FIELD , ` Unable to remove test file as 'yellowtent' user in ${ rootPath } : ${ safe . error . message } . Check dir/mount permissions ` , { field } ) ;
2020-05-26 14:57:20 -07:00
}
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 ) {
2021-05-17 15:58:38 -07:00
if ( apiConfig . mountOptions && apiConfig . mountOptions . password ) apiConfig . mountOptions . password = constants . SECRET _PLACEHOLDER ;
2021-07-09 15:29:29 -07:00
if ( apiConfig . mountOptions && apiConfig . mountOptions . privateKey ) apiConfig . mountOptions . privateKey = constants . SECRET _PLACEHOLDER ;
2019-02-09 18:08:10 -08:00
return apiConfig ;
}
2021-05-17 15:58:38 -07:00
function injectPrivateFields ( newConfig , currentConfig ) {
if ( newConfig . mountOptions && currentConfig . mountOptions && newConfig . mountOptions . password === constants . SECRET _PLACEHOLDER ) newConfig . mountOptions . password = currentConfig . mountOptions . password ;
2021-07-09 15:29:29 -07:00
if ( newConfig . mountOptions && currentConfig . mountOptions && newConfig . mountOptions . privateKey === constants . SECRET _PLACEHOLDER ) newConfig . mountOptions . privateKey = currentConfig . mountOptions . privateKey ;
2019-02-09 18:08:10 -08:00
}