diff --git a/setup/start.sh b/setup/start.sh index c67e83d70..8d97733de 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -62,7 +62,7 @@ mkdir -p "${BOX_DATA_DIR}" "${APPS_DATA_DIR}" "${MAIL_DATA_DIR}" # keep these in sync with paths.js log "Ensuring directories" -mkdir -p "${PLATFORM_DATA_DIR}/"{graphite,mysql,postgresql,mongodb,redis,tls,logrotate.d,acme,backup,update,firewall,sshfs,cifs,oidc,diskusage} +mkdir -p "${PLATFORM_DATA_DIR}/"{graphite,mysql,postgresql,mongodb,redis,tls,logrotate.d,acme,backup,update,firewall,sshfs,cifs,oidc,diskusage,source-archives} mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/"{banner,dkim} mkdir -p "${PLATFORM_DATA_DIR}/logs/"{backup,updater,tasks} mkdir -p "${PLATFORM_DATA_DIR}/sftp/ssh" # sftp keys @@ -223,7 +223,7 @@ log "Changing ownership" # note, change ownership after db migrate. this allow db migrate to move files around as root and then we can fix it up here # be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change chown -R "${USER}" /etc/cloudron -chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/"{nginx,addons,acme,backup,logs,update,sftp,firewall,sshfs,cifs,tls,oidc,diskusage} +chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/"{nginx,addons,acme,backup,logs,update,sftp,firewall,sshfs,cifs,tls,oidc,diskusage,source-archives} chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}" chown "${USER}:${USER}" "${APPS_DATA_DIR}" diff --git a/src/apps.js b/src/apps.js index c0d8ac504..08997f667 100644 --- a/src/apps.js +++ b/src/apps.js @@ -16,6 +16,7 @@ import docker from './docker.js'; import domains from './domains.js'; import eventlog from './eventlog.js'; import fs from 'node:fs'; +import fileUtils from './file-utils.js'; import Location from './location.js'; import locks from './locks.js'; import logs from './logs.js'; @@ -1794,6 +1795,8 @@ async function install(data, auditSource) { let error = manifestFormat.parse(manifest); if (error) throw new BoxError(BoxError.BAD_FIELD, `Manifest error: ${error.message}`); + if (data.sourceArchiveFilePath) manifest.dockerImage = `local/${manifest.id}:${manifest.version}-${Date.now()}`; + error = await checkManifest(manifest); if (error) throw error; @@ -1868,11 +1871,6 @@ async function install(data, auditSource) { const appId = crypto.randomUUID(); debug(`Installing app ${appId}`); - // if we have a sourceArchive rename it to appId to be picked up later in the apptask - if (data.sourceArchiveFilePath) { - if (!safe.fs.renameSync(data.sourceArchiveFilePath, `/tmp/${appId}.tar.gz`)) throw new BoxError(BoxError.FS_ERROR, 'Error moving source archive'); - } - const app = { accessRestriction, operators, @@ -1906,6 +1904,8 @@ async function install(data, auditSource) { if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings); if (addError) throw addError; + if (data.sourceArchiveFilePath) await fileUtils.renameFile(data.sourceArchiveFilePath, `${paths.SOURCE_ARCHIVES_DIR}/${appId}.tar.gz`); + const task = { args: { restoreConfig: null, skipDnsSetup, overwriteDns }, values: { }, @@ -2312,6 +2312,8 @@ async function updateApp(app, data, auditSource) { error = manifestFormat.parse(manifest); if (error) throw new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message); + if (data.sourceArchiveFilePath) manifest.dockerImage = `local/${manifest.id}:${manifest.version}-${Date.now()}`; + error = await checkManifest(manifest); if (error) throw error; @@ -2364,10 +2366,7 @@ async function updateApp(app, data, auditSource) { const hasSso = !!updateConfig.manifest.addons?.proxyAuth || !!updateConfig.manifest.addons?.ldap || !!manifest.addons?.oidc; if (!hasSso && app.sso) values.sso = false; // turn off sso flag, if the update removes sso options - // if we have a sourceArchive rename it to appId to be picked up later in the apptask - if (data.sourceArchiveFilePath) { - if (!safe.fs.renameSync(data.sourceArchiveFilePath, `/tmp/${appId}.tar.gz`)) throw new BoxError(BoxError.FS_ERROR, 'Error moving source archive'); - } + if (data.sourceArchiveFilePath) await fileUtils.renameFile(data.sourceArchiveFilePath, `${paths.SOURCE_ARCHIVES_DIR}/${appId}.tar.gz`); const task = { args: { updateConfig }, diff --git a/src/apptask.js b/src/apptask.js index d2cb3eb44..91f369ed1 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -203,7 +203,7 @@ async function buildLocalImage(app) { const sourceFilePath = path.join(paths.APPS_DATA_DIR, app.id, 'source.tar.gz'); // if we have a newly uploaded source archive, use that - const uploadedSourceArchiveFilePath = `/tmp/${app.id}.tar.gz`; + const uploadedSourceArchiveFilePath = `${paths.SOURCE_ARCHIVES_DIR}/${app.id}.tar.gz`; if (fs.existsSync(uploadedSourceArchiveFilePath)) { const [renameError] = await safe(fsPromises.rename(uploadedSourceArchiveFilePath, sourceFilePath)); if (renameError) { @@ -340,6 +340,7 @@ async function uninstallCommand(app, args, progressCallback) { await progressCallback({ percent: 60, message: 'Deleting image' }); await docker.deleteImage(app.manifest.dockerImage); + await safe(fsPromises.unlink(`${paths.SOURCE_ARCHIVES_DIR}/${app.id}.tar.gz`)); await progressCallback({ percent: 70, message: 'Unregistering domains' }); await dns.unregisterLocations([ { subdomain: app.subdomain, domain: app.domain } ].concat(app.secondaryDomains).concat(app.redirectDomains).concat(app.aliasDomains), progressCallback); diff --git a/src/file-utils.js b/src/file-utils.js new file mode 100644 index 000000000..1421215a6 --- /dev/null +++ b/src/file-utils.js @@ -0,0 +1,23 @@ +import assert from 'node:assert'; +import { promises as fsPromises } from 'node:fs'; +import BoxError from './boxerror.js'; +import safe from 'safetydance'; + +// cross device file rename +async function renameFile(sourcePath, destPath) { + assert.strictEqual(typeof sourcePath, 'string'); + assert.strictEqual(typeof destPath, 'string'); + const [renameError] = await safe(fsPromises.rename(sourcePath, destPath)); + if (renameError) { + if (renameError.code === 'EXDEV') { + const [copyError] = await safe(fsPromises.copyFile(sourcePath, destPath)); + if (copyError) throw new BoxError(BoxError.FS_ERROR, copyError); + const [unlinkError] = await safe(fsPromises.unlink(sourcePath)); + if (unlinkError) throw new BoxError(BoxError.FS_ERROR, unlinkError); + } else { + throw new BoxError(BoxError.FS_ERROR, renameError); + } + } +} + +export default { renameFile }; diff --git a/src/paths.js b/src/paths.js index 89e3437c6..c131cf0db 100644 --- a/src/paths.js +++ b/src/paths.js @@ -31,6 +31,7 @@ export default { PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'), APPS_DATA_DIR: path.join(baseDir(), 'appsdata'), + SOURCE_ARCHIVES_DIR: path.join(baseDir(), 'platformdata/source-archives'), ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'), ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'), diff --git a/src/routes/apps.js b/src/routes/apps.js index e3478546b..2340dfbee 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -145,11 +145,6 @@ async function install(req, res, next) { data.sourceArchiveFilePath = req.files && req.files.sourceArchive?.path || null; - // if we have a source archive upload, craft a custom docker image URI for later - if (data.sourceArchiveFilePath) { - data.manifest.dockerImage = `local/${data.manifest.id}:${data.manifest.version}-${Date.now()}`; - } - [error, result] = await safe(apps.install(data, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); @@ -659,11 +654,6 @@ async function update(req, res, next) { data.sourceArchiveFilePath = req.files && req.files.sourceArchive?.path || null; - // if we have a source archive upload, craft a custom docker image URI for later - if (data.sourceArchiveFilePath) { - data.manifest.dockerImage = `local/${data.manifest.id}:${data.manifest.version}-${Date.now()}`; - } - [error, result] = await safe(apps.updateApp(req.resources.app, data, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error));