diff --git a/src/apps.js b/src/apps.js index e3fb1efe8..553de4531 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1486,6 +1486,11 @@ 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, diff --git a/src/apptask.js b/src/apptask.js index a7fe7d37f..12f8128ff 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -25,6 +25,7 @@ const apps = require('./apps.js'), docker = require('./docker.js'), ejs = require('ejs'), fs = require('node:fs'), + fsPromises = require('node:fs').promises, iputils = require('./iputils.js'), manifestFormat = require('@cloudron/manifest-format'), os = require('node:os'), @@ -226,11 +227,35 @@ async function downloadImage(manifest) { if (diskUsage.available < (1024*1024*1024)) throw new BoxError(BoxError.DOCKER_ERROR, `Not enough disk space to pull docker image. available: ${diskUsage.available}`); - if (manifest.dockerImage.indexOf('local/') === 0) { - await docker.buildImage(manifest); - } else { - await docker.downloadImage(manifest); + await docker.downloadImage(manifest); +} + +async function buildLocalImage(app) { + assert.strictEqual(typeof app, 'object'); + + // TODO some precondition checks like downloadImage maybe + + 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`; + if (fs.existsSync(uploadedSourceArchiveFilePath)) { + const [renameError] = await safe(fsPromises.rename(uploadedSourceArchiveFilePath, sourceFilePath)); + if (renameError) { + if (renameError.code === 'EXDEV') { + // Cross-device rename not permitted, so copy and remove + let [error] = await safe(fsPromises.copyFile(uploadedSourceArchiveFilePath, sourceFilePath)); + if (error) throw new BoxError(BoxError.FS_ERROR, error); + + [error] = await safe(fsPromises.unlink(uploadedSourceArchiveFilePath)); + if (error) throw new BoxError(BoxError.FS_ERROR, error); + } else { + throw new BoxError(BoxError.FS_ERROR, renameError); + } + } } + + await docker.buildImage(app.manifest.dockerImage, sourceFilePath); } async function updateChecklist(app, newChecks, acknowledged = false) { @@ -321,8 +346,11 @@ async function installCommand(app, args, progressCallback) { await dns.registerLocations([ { subdomain: app.subdomain, domain: app.domain }].concat(app.secondaryDomains).concat(app.redirectDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback); } - await progressCallback({ percent: 40, message: 'Downloading image' }); - await downloadImage(app.manifest); + // only download non-local images, local ones are built after restore + if (app.manifest.dockerImage.indexOf('local/') !== 0) { + await progressCallback({ percent: 40, message: 'Downloading image' }); + await downloadImage(app.manifest); + } await progressCallback({ percent: 50, message: 'Creating app data directory' }); await createAppDir(app); @@ -360,6 +388,12 @@ async function installCommand(app, args, progressCallback) { await services.restoreAddons(app, app.manifest.addons); } + // now we have the local package tarball, so lets build + if (app.manifest.dockerImage.indexOf('local/') === 0) { + await progressCallback({ percent: 75, message: 'Building image' }); + await buildLocalImage(app); + } + await progressCallback({ percent: 80, message: 'Creating container' }); await createContainer(app); diff --git a/src/docker.js b/src/docker.js index 0fe7bdbf9..e5240bd88 100644 --- a/src/docker.js +++ b/src/docker.js @@ -160,16 +160,15 @@ async function pullImage(imageRef) { }); } -async function buildImage(manifest) { - assert.strictEqual(typeof manifest, 'object'); +async function buildImage(dockerImage, sourceArchiveFilePath) { + assert.strictEqual(typeof dockerImage, 'string'); + assert.strictEqual(typeof sourceArchiveFilePath, 'string'); - const sourceArchivePath = '/tmp/' + manifest.dockerImage.slice('local/'.length); + debug(`buildImage: building ${dockerImage} from ${sourceArchiveFilePath}`); - debug(`buildImage: building ${manifest.dockerImage} from ${sourceArchivePath}`); - - const tarStream = fs.createReadStream(sourceArchivePath); - const [error, stream] = await safe(gConnection.buildImage(tarStream, { t: manifest.dockerImage })); - if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to build image from ${sourceArchivePath}: ${error.message}`); + const tarStream = fs.createReadStream(sourceArchiveFilePath); + const [error, stream] = await safe(gConnection.buildImage(tarStream, { t: dockerImage })); + if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to build image from ${sourceArchiveFilePath}: ${error.message}`); return new Promise((resolve, reject) => { let buildError = null; @@ -190,14 +189,14 @@ async function buildImage(manifest) { debug(`buildImage: error ${buildError}`); return reject(new BoxError(buildError.message.includes('no space') ? BoxError.FS_ERROR : BoxError.DOCKER_ERROR, buildError.message)); } else { - debug(`buildImage: success ${manifest.dockerImage}`); + debug(`buildImage: success ${dockerImage}`); } resolve(); }); stream.on('error', (error) => { - debug(`buildImage: error building image ${manifest.dockerImage}: %o`, error); + debug(`buildImage: error building image ${dockerImage}: %o`, error); reject(new BoxError(BoxError.DOCKER_ERROR, error.message)); }); }); diff --git a/src/routes/apps.js b/src/routes/apps.js index 821c13fe5..b20c35304 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -205,12 +205,11 @@ async function install(req, res, next) { data.appStoreId = result.appStoreId; data.manifest = result.manifest; + data.sourceArchiveFilePath = req.files.sourceArchive?.path || null; // if we have a source archive upload, craft a custom docker image URI for later - if (req.files.sourceArchive?.path) { - const newFileName = `${data.manifest.id}:${data.manifest.version}-${Date.now()}`; - if (!safe.fs.renameSync(req.files.sourceArchive?.path, `/tmp/${newFileName}`)) return next(new HttpError(500, 'unable to move source archive')); - data.manifest.dockerImage = `local/${newFileName}`; + 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)));