diff --git a/CHANGES b/CHANGES index ee20b160f..167918dbc 100644 --- a/CHANGES +++ b/CHANGES @@ -2496,4 +2496,5 @@ * dark mode fixes * sendmail: mail from display name * Use volumes for app data instead of raw path +* initial xfs support diff --git a/scripts/cloudron-setup b/scripts/cloudron-setup index 7a41d18f9..4617e0060 100755 --- a/scripts/cloudron-setup +++ b/scripts/cloudron-setup @@ -26,8 +26,8 @@ readonly GREEN='\033[32m' readonly DONE='\033[m' # verify the system has minimum requirements met -if [[ "${rootfs_type}" != "ext4" ]]; then - echo "Error: Cloudron requires '/' to be ext4" # see #364 +if [[ "${rootfs_type}" != "ext4" && "${rootfs_type}" != "xfs" ]]; then + echo "Error: Cloudron requires '/' to be ext4 or xfs" # see #364 exit 1 fi diff --git a/src/mounts.js b/src/mounts.js index 4c23285a7..1d94090c4 100644 --- a/src/mounts.js +++ b/src/mounts.js @@ -54,6 +54,7 @@ function validateMountOptions(type, options) { if (typeof options.remoteDir !== 'string') return new BoxError(BoxError.BAD_FIELD, 'remoteDir is not a string'); return null; case 'ext4': + case 'xfs': if (typeof options.diskPath !== 'string') return new BoxError(BoxError.BAD_FIELD, 'diskPath is not a string'); return null; default: @@ -62,7 +63,7 @@ function validateMountOptions(type, options) { } function isManagedProvider(provider) { - return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4'; + return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4' || provider === 'xfs'; } function mountObjectFromBackupConfig(backupConfig) { @@ -108,6 +109,11 @@ function renderMountFile(mount) { what = mountOptions.diskPath; // like /dev/disk/by-uuid/uuid or /dev/disk/by-id/scsi-id options = 'discard,defaults,noatime'; break; + case 'xfs': + type = 'xfs'; + what = mountOptions.diskPath; // like /dev/disk/by-uuid/uuid or /dev/disk/by-id/scsi-id + options = 'discard,defaults,noatime'; + break; case 'sshfs': { const keyFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${mountOptions.host}`); if (!safe.fs.writeFileSync(keyFilePath, `${mount.mountOptions.privateKey}\n`, { mode: 0o600 })) throw new BoxError(BoxError.FS_ERROR, `Could not write private key: ${safe.error.message}`); diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js index 37c2b14a1..679a5b218 100644 --- a/src/storage/filesystem.js +++ b/src/storage/filesystem.js @@ -26,6 +26,7 @@ const PROVIDER_FILESYSTEM = 'filesystem'; const PROVIDER_MOUNTPOINT = 'mountpoint'; const PROVIDER_SSHFS = 'sshfs'; const PROVIDER_CIFS = 'cifs'; +const PROVIDER_XFS = 'xfs'; const PROVIDER_NFS = 'nfs'; const PROVIDER_EXT4 = 'ext4'; @@ -52,6 +53,7 @@ function getRootPath(apiConfig) { case PROVIDER_NFS: case PROVIDER_CIFS: case PROVIDER_EXT4: + case PROVIDER_XFS: return path.join(paths.MANAGED_BACKUP_MOUNT_DIR, apiConfig.prefix); case PROVIDER_MOUNTPOINT: return path.join(apiConfig.mountPoint, apiConfig.prefix); @@ -89,7 +91,7 @@ async function checkPreconditions(apiConfig, dataLayout) { if (error) throw new BoxError(BoxError.FS_ERROR, `Error when checking for disk space: ${error.message}`); // Check filesystem is mounted so we don't write into the actual folder on disk - if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS || apiConfig.provider === PROVIDER_EXT4) { + if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS || apiConfig.provider === PROVIDER_EXT4 || apiConfig.provider === PROVIDER_XFS) { if (result.mountpoint !== paths.MANAGED_BACKUP_MOUNT_DIR) throw new BoxError(BoxError.FS_ERROR, 'Backup target is not mounted'); } else if (apiConfig.provider === PROVIDER_MOUNTPOINT) { if (result.mountpoint === '/') throw new BoxError(BoxError.FS_ERROR, `${apiConfig.backupFolder} is not mounted`); @@ -103,6 +105,7 @@ function hasChownSupportSync(apiConfig) { switch (apiConfig.provider) { case PROVIDER_NFS: case PROVIDER_EXT4: + case PROVIDER_XFS: case PROVIDER_FILESYSTEM: return true; case PROVIDER_SSHFS: @@ -287,7 +290,7 @@ async function testConfig(apiConfig) { 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; - } else { // cifs/ext4/nfs/mountpoint/sshfs + } else { // xfs/cifs/ext4/nfs/mountpoint/sshfs if (apiConfig.provider === PROVIDER_MOUNTPOINT) { if (!apiConfig.mountPoint || typeof apiConfig.mountPoint !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'mountPoint must be non-empty string'); const error = validateBackupTarget(apiConfig.mountPoint); diff --git a/src/system.js b/src/system.js index ddc336fae..6e2bf447f 100644 --- a/src/system.js +++ b/src/system.js @@ -69,7 +69,7 @@ async function getDisks() { if (error) throw new BoxError(BoxError.FS_ERROR, error); // filter by ext4 and then sort to make sure root disk is first - const ext4Disks = allDisks.filter((r) => r.type === 'ext4').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint)); + const ext4Disks = allDisks.filter((r) => r.type === 'ext4' || r.type === 'xfs').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint)); const diskInfos = []; for (const p of [ paths.BOX_DATA_DIR, paths.MAIL_DATA_DIR, paths.PLATFORM_DATA_DIR, paths.APPS_DATA_DIR, info.DockerRootDir ]) { @@ -81,7 +81,7 @@ async function getDisks() { const backupsFilesystem = await getBackupsFilesystem(); const result = { - disks: ext4Disks, // root disk is first. { filesystem, type, size, used, avialable, capacity, mountpoint } + disks: ext4Disks, // root disk is first. { filesystem, type, size, used, available, capacity, mountpoint } boxDataDisk: diskInfos[0].filesystem, mailDataDisk: diskInfos[1].filesystem, platformDataDisk: diskInfos[2].filesystem,