2016-09-15 11:29:45 +02:00
'use strict' ;
exports = module . exports = {
2025-08-01 14:54:32 +02:00
setup ,
teardown ,
cleanup ,
2025-08-01 18:55:04 +02:00
verifyConfig ,
2025-08-01 14:54:32 +02:00
removePrivateFields ,
injectPrivateFields ,
2022-10-02 17:22:44 +02:00
getAvailableSize ,
2025-08-04 10:47:00 +02:00
getStatus ,
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-15 11:29:45 +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' ) ,
2025-08-01 14:54:32 +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' ) ,
2017-04-20 16:59:17 -07:00
safe = require ( 'safetydance' ) ,
2025-08-01 18:55:04 +02:00
shell = require ( '../shell.js' ) ( 'filesystem' ) ,
_ = require ( '../underscore.js' ) ;
2016-09-16 11:55:59 +02:00
2025-08-02 01:46:29 +02:00
function getRootPath ( config ) {
assert . strictEqual ( typeof config , 'object' ) ;
const prefix = config . prefix ? ? '' ; // can be missing for plain filesystem
if ( mounts . isManagedProvider ( config . _provider ) ) {
return path . join ( config . _managedMountPath , prefix ) ;
} else if ( config . _provider === mounts . MOUNT _TYPE _MOUNTPOINT ) {
return path . join ( config . mountPoint , prefix ) ;
} else if ( config . _provider === mounts . MOUNT _TYPE _FILESYSTEM ) {
2025-08-02 10:37:37 +02:00
return path . join ( config . backupDir , prefix ) ;
2025-08-02 01:46:29 +02:00
}
throw new BoxError ( BoxError . INTERNAL _ERROR , ` Unhandled provider: ${ config . _provider } ` ) ;
}
2025-08-01 18:55:04 +02:00
async function getAvailableSize ( config ) {
assert . strictEqual ( typeof config , 'object' ) ;
2020-06-08 16:25:00 +02:00
2024-11-06 14:53:41 +01:00
// note that df returns the disk size (as opposed to the apparent size)
2025-08-02 01:46:29 +02:00
const [ error , dfResult ] = await safe ( df . file ( getRootPath ( config ) ) ) ;
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
}
2025-08-04 10:47:00 +02:00
async function getStatus ( config ) {
assert . strictEqual ( typeof config , 'object' ) ;
let hostPath ;
if ( mounts . isManagedProvider ( config . _provider ) ) {
hostPath = config . _managedMountPath ;
} else if ( config . _provider === 'mountpoint' ) {
hostPath = config . mountPoint ;
} else if ( config . _provider === 'filesystem' ) {
hostPath = config . backupDir ;
}
return await mounts . getStatus ( config . _provider , hostPath ) ; // { state, message }
}
2025-08-01 18:55:04 +02:00
function hasChownSupportSync ( config ) {
2025-08-02 01:46:29 +02:00
switch ( config . _provider ) {
2025-08-01 14:54:32 +02:00
case mounts . MOUNT _TYPE _NFS :
case mounts . MOUNT _TYPE _EXT4 :
case mounts . MOUNT _TYPE _XFS :
case mounts . MOUNT _TYPE _DISK :
case mounts . MOUNT _TYPE _FILESYSTEM :
2021-07-10 08:36:30 -07:00
return true ;
2025-08-01 14:54:32 +02:00
case mounts . MOUNT _TYPE _SSHFS :
2021-07-10 08:36:30 -07:00
// 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)
2025-08-01 18:55:04 +02:00
return config . mountOptions . user === 'root' ;
2025-08-01 14:54:32 +02:00
case mounts . MOUNT _TYPE _CIFS :
2021-07-10 08:36:30 -07:00
return true ;
2025-08-01 14:54:32 +02:00
case mounts . MOUNT _TYPE _MOUNTPOINT :
2025-08-01 18:55:04 +02:00
return config . chown ;
2021-07-10 08:36:30 -07:00
}
}
2025-08-02 01:46:29 +02:00
async function upload ( config , remotePath ) {
2025-08-01 18:55:04 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-02 01:46:29 +02:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
const fullRemotePath = path . join ( getRootPath ( config ) , remotePath ) ;
2016-09-15 11:29:45 +02:00
2025-08-02 01:46:29 +02:00
const [ mkdirError ] = await safe ( fs . promises . mkdir ( path . dirname ( fullRemotePath ) , { recursive : true } ) ) ;
if ( mkdirError ) throw new BoxError ( BoxError . FS _ERROR , ` Error creating directory ${ fullRemotePath } : ${ mkdirError . message } ` ) ;
2016-09-16 11:21:08 +02:00
2025-08-02 01:46:29 +02:00
await safe ( fs . promises . unlink ( fullRemotePath ) ) ; // remove any hardlink
2017-09-22 14:40:37 -07:00
2024-07-05 17:53:35 +02:00
return {
2025-08-02 01:46:29 +02:00
stream : fs . createWriteStream ( fullRemotePath , { autoClose : true } ) ,
2024-07-05 17:53:35 +02:00
async finish ( ) {
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
2025-08-01 18:55:04 +02:00
if ( hasChownSupportSync ( config ) ) {
2025-08-02 01:46:29 +02:00
if ( ! safe . fs . chownSync ( fullRemotePath , backupUid , backupUid ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unable to chown ${ fullRemotePath } : ${ safe . error . message } ` ) ;
if ( ! safe . fs . chownSync ( path . dirname ( fullRemotePath ) , backupUid , backupUid ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unable to chown parentdir ${ fullRemotePath } : ${ safe . error . message } ` ) ;
2020-12-18 17:14:31 -08:00
}
2024-07-05 17:53:35 +02:00
}
} ;
2016-09-16 11:21:08 +02:00
}
2025-08-02 01:46:29 +02:00
async function download ( config , remotePath ) {
2025-08-01 18:55:04 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-02 01:46:29 +02:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2016-09-15 11:29:45 +02:00
2025-08-02 01:46:29 +02:00
const fullRemotePath = path . join ( getRootPath ( config ) , remotePath ) ;
debug ( ` download: ${ fullRemotePath } ` ) ;
2017-04-11 11:00:55 +02:00
2025-08-02 01:46:29 +02:00
if ( ! safe . fs . existsSync ( fullRemotePath ) ) throw new BoxError ( BoxError . NOT _FOUND , ` File not found: ${ fullRemotePath } ` ) ;
2017-09-28 14:26:39 -07:00
2025-08-02 01:46:29 +02:00
return fs . createReadStream ( fullRemotePath ) ;
2017-04-17 14:46:19 +02:00
}
2025-08-02 01:46:29 +02:00
async function exists ( config , remotePath ) {
2025-08-01 18:55:04 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-02 01:46:29 +02:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
const fullRemotePath = path . join ( getRootPath ( config ) , remotePath ) ;
2021-02-18 16:51:43 -08:00
// do not use existsSync because it does not return EPERM etc
2025-08-02 01:46:29 +02:00
if ( ! safe . fs . statSync ( fullRemotePath ) ) {
2022-04-14 08:07:03 -05:00
if ( safe . error && safe . error . code === 'ENOENT' ) return false ;
2025-08-02 01:46:29 +02:00
if ( safe . error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Exists ${ fullRemotePath } : ${ 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
}
2025-08-02 01:46:29 +02:00
async function listDir ( config , remotePath , batchSize , marker ) {
2025-08-01 18:55:04 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-02 01:46:29 +02:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2018-07-27 13:55:54 -07:00
assert . strictEqual ( typeof batchSize , 'number' ) ;
2025-02-12 18:46:54 +01:00
assert ( typeof marker !== 'undefined' ) ;
2025-08-02 01:46:29 +02:00
const fullRemotePath = path . join ( getRootPath ( config ) , remotePath ) ;
const stack = marker ? marker . stack : [ fullRemotePath ] ;
2025-02-12 18:46:54 +01:00
const fileStream = marker ? marker . fileStream : [ ] ;
if ( ! marker ) marker = { stack , fileStream } ;
while ( stack . length > 0 ) {
const currentDir = stack . pop ( ) ;
const dirents = await fs . promises . readdir ( currentDir , { withFileTypes : true } ) ;
for ( const dirent of dirents ) {
2025-08-02 01:46:29 +02:00
const fullEntryPath = path . join ( currentDir , dirent . name ) ;
2025-02-12 18:46:54 +01:00
if ( dirent . isDirectory ( ) ) {
2025-08-02 01:46:29 +02:00
stack . push ( fullEntryPath ) ;
2025-02-12 18:46:54 +01:00
} else if ( dirent . isFile ( ) ) { // does not include symlink
2025-08-02 01:46:29 +02:00
const stat = await fs . promises . lstat ( fullEntryPath ) ;
2025-08-02 10:24:51 +02:00
fileStream . push ( { path : path . relative ( fullRemotePath , fullEntryPath ) , size : stat . size } ) ;
2025-02-12 18:46:54 +01:00
}
}
if ( fileStream . length >= batchSize ) return { entries : fileStream . splice ( 0 , batchSize ) , marker } ; // note: splice also modifies the array
}
if ( fileStream . length === 0 ) return { entries : [ ] , marker : null } ;
return { entries : fileStream . splice ( 0 , batchSize ) , marker } ; // note: splice also modifies the array
2018-07-27 13:55:54 -07:00
}
2025-08-02 01:46:29 +02:00
async function copy ( config , fromPath , toPath , progressCallback ) {
2025-08-01 18:55:04 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-02 01:46:29 +02:00
assert . strictEqual ( typeof fromPath , 'string' ) ;
assert . strictEqual ( typeof toPath , 'string' ) ;
2022-04-30 16:01:42 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-04-17 14:46:19 +02:00
2025-08-02 01:46:29 +02:00
const fullFromPath = path . join ( getRootPath ( config ) , fromPath ) ;
const fullToPath = path . join ( getRootPath ( config ) , toPath ) ;
const [ mkdirError ] = await safe ( fs . promises . mkdir ( path . dirname ( fullToPath ) , { recursive : true } ) ) ;
2022-04-30 16:01:42 -07:00
if ( mkdirError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , mkdirError . message ) ;
2017-10-04 11:00:30 -07:00
2025-08-02 01:46:29 +02:00
progressCallback ( { message : ` Copying ${ fullFromPath } to ${ fullToPath } ` } ) ;
2017-04-17 14:46:19 +02:00
2025-08-02 01:46:29 +02:00
let cpOptions = ( ( config . _provider !== mounts . MOUNT _TYPE _MOUNTPOINT && config . _provider !== mounts . MOUNT _TYPE _CIFS ) || config . preserveAttributes ) ? '-a' : '-dR' ;
2025-08-01 18:55:04 +02:00
cpOptions += config . noHardlinks ? '' : 'l' ; // this will hardlink backups saving space
2019-04-03 11:54:46 -07:00
2025-08-02 01:46:29 +02:00
if ( config . _provider === mounts . MOUNT _TYPE _SSHFS ) {
2025-08-01 18:55:04 +02:00
const identityFilePath = path . join ( paths . SSHFS _KEYS _DIR , ` id_rsa_ ${ config . mountOptions . host } ` ) ;
2024-07-02 19:02:36 +02:00
2025-08-01 18:55:04 +02:00
const sshOptions = [ '-o' , '"StrictHostKeyChecking no"' , '-i' , identityFilePath , '-p' , config . mountOptions . port , ` ${ config . mountOptions . user } @ ${ config . mountOptions . host } ` ] ;
2025-08-02 01:46:29 +02:00
const sshArgs = sshOptions . concat ( [ 'cp' , cpOptions , path . join ( config . prefix , fromPath ) , path . join ( config . prefix , toPath ) ] ) ;
2024-10-15 10:10:15 +02:00
const [ remoteCopyError ] = await safe ( shell . spawn ( 'ssh' , sshArgs , { shell : true } ) ) ;
2024-07-22 20:53:19 +02:00
if ( ! remoteCopyError ) return ;
2024-07-22 21:24:27 +02:00
if ( remoteCopyError . code === 255 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` SSH connection error: ${ remoteCopyError . message } ` ) ; // do not attempt fallback copy for ssh errors
2025-04-09 10:33:23 +02:00
debug ( 'SSH remote copy failed, trying sshfs copy' ) ; // this can happen for sshfs mounted windows server
2024-07-02 19:02:36 +02:00
}
2024-07-22 20:53:19 +02:00
2025-08-02 01:46:29 +02:00
const [ copyError ] = await safe ( shell . spawn ( 'cp' , [ cpOptions , fullFromPath , fullToPath ] , { } ) ) ;
2024-07-22 20:53:19 +02:00
if ( copyError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , copyError . message ) ;
2016-09-15 11:29:45 +02:00
}
2025-08-02 01:46:29 +02:00
async function remove ( config , remotePath ) {
2025-08-01 18:55:04 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-02 01:46:29 +02:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2017-09-27 17:34:49 -07:00
2025-08-02 01:46:29 +02:00
const fullRemotePath = path . join ( getRootPath ( config ) , remotePath ) ;
const stat = safe . fs . statSync ( fullRemotePath ) ;
2022-04-14 16:07:01 -05:00
if ( ! stat ) return ;
2017-09-27 17:34:49 -07:00
2017-09-28 20:55:39 -07:00
if ( stat . isFile ( ) ) {
2025-08-02 01:46:29 +02:00
if ( ! safe . fs . unlinkSync ( fullRemotePath ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , safe . error . message ) ;
2017-09-28 20:55:39 -07:00
} else if ( stat . isDirectory ( ) ) {
2025-08-02 01:46:29 +02:00
if ( ! safe . fs . rmdirSync ( fullRemotePath , { recursive : false } ) ) 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
}
2025-08-02 01:46:29 +02:00
async function removeDir ( config , remotePathPrefix , progressCallback ) {
2025-08-01 18:55:04 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-02 01:46:29 +02:00
assert . strictEqual ( typeof remotePathPrefix , 'string' ) ;
2022-04-14 16:07:01 -05:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-10-10 20:23:04 -07:00
2025-08-02 01:46:29 +02:00
const fullPathPrefix = path . join ( getRootPath ( config ) , remotePathPrefix ) ;
progressCallback ( { message : ` Removing directory ${ fullPathPrefix } ` } ) ;
2017-10-10 20:23:04 -07:00
2025-08-02 01:46:29 +02:00
if ( config . _provider === mounts . MOUNT _TYPE _SSHFS ) {
2025-08-01 18:55:04 +02:00
const identityFilePath = path . join ( paths . SSHFS _KEYS _DIR , ` id_rsa_ ${ config . mountOptions . host } ` ) ;
2025-04-09 10:33:23 +02:00
2025-08-01 18:55:04 +02:00
const sshOptions = [ '-o' , '"StrictHostKeyChecking no"' , '-i' , identityFilePath , '-p' , config . mountOptions . port , ` ${ config . mountOptions . user } @ ${ config . mountOptions . host } ` ] ;
2025-08-02 01:46:29 +02:00
const sshArgs = sshOptions . concat ( [ 'rm' , '-rf' , path . join ( config . prefix , remotePathPrefix ) ] ) ;
2025-04-09 10:33:23 +02:00
const [ remoteRmError ] = await safe ( shell . spawn ( 'ssh' , sshArgs , { shell : true } ) ) ;
if ( ! remoteRmError ) return ;
if ( remoteRmError . code === 255 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` SSH connection error: ${ remoteRmError . message } ` ) ; // do not attempt fallback copy for ssh errors
debug ( 'SSH remote rm failed, trying sshfs rm' ) ; // this can happen for sshfs mounted windows server
}
2025-08-02 01:46:29 +02:00
const [ error ] = await safe ( shell . spawn ( 'rm' , [ '-rf' , fullPathPrefix ] , { } ) ) ;
2022-04-14 16:07:01 -05:00
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ;
2017-04-17 15:45:58 +02:00
}
2025-08-02 01:46:29 +02:00
function validateDestDir ( dir ) {
assert . strictEqual ( typeof dir , 'string' ) ;
2020-12-01 12:11:55 -08:00
2025-08-02 10:37:37 +02:00
if ( path . normalize ( dir ) !== dir ) return new BoxError ( BoxError . BAD _FIELD , 'backupDir/mountpoint must contain a normalized path' ) ;
if ( ! path . isAbsolute ( dir ) ) return new BoxError ( BoxError . BAD _FIELD , 'backupDir/mountpoint must be an absolute path' ) ;
2020-12-01 12:11:55 -08:00
2025-08-02 10:37:37 +02:00
if ( dir === '/' ) return new BoxError ( BoxError . BAD _FIELD , 'backupDir/mountpoint cannot be /' ) ;
2020-12-01 12:11:55 -08:00
2025-08-02 01:46:29 +02:00
if ( ! dir . endsWith ( '/' ) ) dir = dir + '/' ; // ensure trailing slash for the prefix matching to work
2020-12-01 12:11:55 -08:00
const PROTECTED _PREFIXES = [ '/boot/' , '/usr/' , '/bin/' , '/lib/' , '/root/' , '/var/lib/' , paths . baseDir ( ) ] ;
2025-08-02 10:37:37 +02:00
if ( PROTECTED _PREFIXES . some ( p => dir . startsWith ( p ) ) ) return new BoxError ( BoxError . BAD _FIELD , 'backupDir path is protected' ) ;
2020-12-01 12:11:55 -08:00
return null ;
}
2025-08-01 18:55:04 +02:00
async function cleanup ( config , progressCallback ) {
assert . strictEqual ( typeof config , 'object' ) ;
2025-02-13 11:08:00 +01:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
}
2025-08-01 18:55:04 +02:00
async function setupManagedMount ( provider , mountOptions , hostPath ) {
assert . strictEqual ( typeof provider , 'string' ) ;
assert . strictEqual ( typeof mountOptions , 'object' ) ;
2025-08-01 14:54:32 +02:00
assert . strictEqual ( typeof hostPath , 'string' ) ;
2025-08-01 18:55:04 +02:00
debug ( ` setupManagedMount: setting up mount at ${ hostPath } with ${ provider } ` ) ;
2025-08-01 14:54:32 +02:00
const newMount = {
2025-08-01 15:48:09 +02:00
description : ` Cloudron Managed Mount ` ,
2025-08-01 14:54:32 +02:00
hostPath ,
2025-08-01 18:55:04 +02:00
mountType : provider ,
mountOptions
2025-08-01 14:54:32 +02:00
} ;
await mounts . tryAddMount ( newMount , { timeout : 10 } ) ; // 10 seconds
return newMount ;
}
2025-08-01 18:55:04 +02:00
async function setup ( config ) {
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-01 14:54:32 +02:00
debug ( 'setup: removing old storage configuration' ) ;
2025-08-02 01:46:29 +02:00
if ( ! mounts . isManagedProvider ( config . _provider ) ) return ;
2025-08-01 14:54:32 +02:00
2025-08-01 23:20:51 +02:00
const mountPath = path . join ( paths . MANAGED _BACKUP _MOUNT _DIR , config . id ) ;
await safe ( mounts . removeMount ( mountPath ) , { debug } ) ; // ignore error
2025-08-01 14:54:32 +02:00
debug ( 'setup: setting up new storage configuration' ) ;
2025-08-02 01:46:29 +02:00
await setupManagedMount ( config . _provider , config . mountOptions , mountPath ) ;
2025-08-01 14:54:32 +02:00
}
2025-08-01 18:55:04 +02:00
async function teardown ( config ) {
assert . strictEqual ( typeof config , 'object' ) ;
2025-08-01 14:54:32 +02:00
2025-08-02 01:46:29 +02:00
if ( ! mounts . isManagedProvider ( config . _provider ) ) return ;
2025-08-01 14:54:32 +02:00
2025-08-01 23:20:51 +02:00
const mountPath = path . join ( paths . MANAGED _BACKUP _MOUNT _DIR , config . id ) ;
await safe ( mounts . removeMount ( mountPath ) , { debug } ) ; // ignore error
2025-08-01 14:54:32 +02:00
}
2025-08-01 18:55:04 +02:00
async function verifyConfig ( { id , provider , config } ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof provider , 'string' ) ;
assert . strictEqual ( typeof config , 'object' ) ;
2016-10-11 11:36:25 +02:00
2025-08-01 18:55:04 +02:00
if ( 'noHardlinks' in config && typeof config . noHardlinks !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'noHardlinks must be boolean' ) ;
if ( 'chown' in config && typeof config . chown !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'chown must be boolean' ) ;
if ( 'preserveAttributes' in config && typeof config . preserveAttributes !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'preserveAttributes must be boolean' ) ;
2021-05-17 15:58:38 -07:00
2025-08-02 01:46:29 +02:00
const managedMountPath = path . join ( paths . MANAGED _BACKUP _MOUNT _DIR , id ) ;
2025-08-01 23:20:51 +02:00
2025-08-02 01:46:29 +02:00
if ( 'prefix' in config ) {
2025-08-01 18:55:04 +02:00
if ( typeof config . prefix !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'prefix must be a string' ) ;
if ( config . prefix !== '' ) {
if ( path . isAbsolute ( config . prefix ) ) throw new BoxError ( BoxError . BAD _FIELD , 'prefix must be a relative path' ) ;
if ( path . normalize ( config . prefix ) !== config . prefix ) throw new BoxError ( BoxError . BAD _FIELD , 'prefix must contain a normalized relative path' ) ;
2020-12-18 14:41:59 -08:00
}
2025-08-02 01:46:29 +02:00
}
2017-10-16 15:15:15 -07:00
2025-08-02 01:46:29 +02:00
if ( provider === mounts . MOUNT _TYPE _FILESYSTEM ) {
2025-08-02 10:37:37 +02:00
if ( ! config . backupDir || typeof config . backupDir !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'backupDir must be non-empty string' ) ;
const error = validateDestDir ( config . backupDir ) ;
2025-08-02 01:46:29 +02:00
if ( error ) throw error ;
} else {
2025-08-01 18:55:04 +02:00
if ( mounts . isManagedProvider ( provider ) ) {
if ( ! config . mountOptions || typeof config . mountOptions !== 'object' ) throw new BoxError ( BoxError . BAD _FIELD , 'mountOptions must be an object' ) ;
const error = mounts . validateMountOptions ( provider , config . mountOptions ) ;
if ( error ) throw error ;
2025-08-02 01:46:29 +02:00
await setupManagedMount ( provider , config . mountOptions , ` ${ managedMountPath } -validation ` ) ;
2025-08-01 18:55:04 +02:00
} else if ( provider === mounts . MOUNT _TYPE _MOUNTPOINT ) {
if ( ! config . mountPoint || typeof config . mountPoint !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'mountPoint must be non-empty string' ) ;
2025-08-02 01:46:29 +02:00
const error = validateDestDir ( config . mountPoint ) ;
2025-08-01 14:54:32 +02:00
if ( error ) throw error ;
2025-08-01 18:55:04 +02:00
const [ mountError ] = await safe ( shell . spawn ( 'mountpoint' , [ '-q' , '--' , config . mountPoint ] , { timeout : 5000 } ) ) ;
if ( mountError ) throw new BoxError ( BoxError . BAD _FIELD , ` ${ config . mountPoint } is not mounted: ${ mountError . message } ` ) ;
2024-04-09 13:53:15 +02:00
} else {
2025-08-01 18:55:04 +02:00
throw new BoxError ( BoxError . BAD _FIELD , ` Unknown provider: ${ provider } ` ) ;
2024-04-09 13:53:15 +02:00
}
2021-06-22 15:28:48 -07:00
}
2025-08-02 10:37:37 +02:00
const tmp = _ . pick ( config , [ 'noHardlinks' , 'chown' , 'preserveAttributes' , 'backupDir' , 'prefix' , 'mountOptions' , 'mountPoint' ] ) ;
2025-08-02 01:46:29 +02:00
const newConfig = { _provider : provider , _managedMountPath : managedMountPath , ... tmp } ;
const fullPath = getRootPath ( newConfig ) ;
if ( ! safe . fs . mkdirSync ( path . join ( fullPath , 'snapshot' ) , { recursive : true } ) && safe . error . code !== 'EEXIST' ) {
if ( safe . error && safe . error . code === 'EACCES' ) throw new BoxError ( BoxError . BAD _FIELD , ` Access denied. Create ${ fullPath } /snapshot and run "chown yellowtent:yellowtent ${ fullPath } " on the server ` ) ;
2024-04-09 13:53:15 +02:00
throw new BoxError ( BoxError . BAD _FIELD , safe . error . message ) ;
2021-05-26 23:01:05 -07:00
}
2017-10-26 11:27:36 -07:00
2025-08-02 01:46:29 +02:00
if ( ! safe . fs . writeFileSync ( path . join ( fullPath , 'cloudron-testfile' ) , 'testcontent' ) ) {
throw new BoxError ( BoxError . BAD _FIELD , ` Unable to create test file as 'yellowtent' user in ${ fullPath } : ${ safe . error . message } . Check dir/mount permissions ` ) ;
2021-05-26 23:01:05 -07:00
}
2020-05-26 14:57:20 -07:00
2025-08-02 01:46:29 +02:00
if ( ! safe . fs . unlinkSync ( path . join ( fullPath , 'cloudron-testfile' ) ) ) {
throw new BoxError ( BoxError . BAD _FIELD , ` Unable to remove test file as 'yellowtent' user in ${ fullPath } : ${ safe . error . message } . Check dir/mount permissions ` ) ;
2020-05-26 14:57:20 -07:00
}
2025-08-01 14:54:32 +02:00
2025-08-02 01:46:29 +02:00
if ( mounts . isManagedProvider ( provider ) ) await mounts . removeMount ( ` ${ managedMountPath } -validation ` ) ;
2025-08-01 18:55:04 +02:00
2025-08-02 01:46:29 +02:00
return newConfig ;
2016-10-11 11:36:25 +02:00
}
2017-01-04 16:22:58 -08:00
2025-08-01 18:55:04 +02:00
function removePrivateFields ( config ) {
if ( config . mountOptions && config . mountOptions . password ) config . mountOptions . password = constants . SECRET _PLACEHOLDER ;
if ( config . mountOptions && config . mountOptions . privateKey ) config . mountOptions . privateKey = constants . SECRET _PLACEHOLDER ;
2021-07-09 15:29:29 -07:00
2025-08-02 01:46:29 +02:00
delete config . _provider ;
delete config . _managedMountPath ;
2025-08-01 18:55:04 +02:00
return config ;
2019-02-09 18:08:10 -08:00
}
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 ;
2025-08-01 18:55:04 +02:00
2025-08-02 01:46:29 +02:00
newConfig . _provider = currentConfig . _provider ;
if ( currentConfig . _managedMountPath ) newConfig . _managedMountPath = currentConfig . _managedMountPath ;
2019-02-09 18:08:10 -08:00
}