diff --git a/package-lock.json b/package-lock.json index da97d5634..9cdab25da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "async": "^3.2.5", "aws-sdk": "^2.1637.0", "basic-auth": "^2.0.1", - "cloudron-manifestformat": "^5.25.0", + "cloudron-manifestformat": "^5.26.1", "connect": "^3.7.0", "connect-lastmile": "^2.2.0", "connect-timeout": "^1.9.0", @@ -1086,9 +1086,9 @@ } }, "node_modules/cloudron-manifestformat": { - "version": "5.25.0", - "resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.25.0.tgz", - "integrity": "sha512-xkwqtro1Grnk4ilGvnpPxF1vcvVjVgE50n4966qRDSS6T3EyfBr1XiNBNuOJOPzHuTswxV/eQTPVG04UOlSRtg==", + "version": "5.26.1", + "resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.26.1.tgz", + "integrity": "sha512-BrTQ9FGXXohRmCJo2ZFn1CYSr07qk3GLCchjdD0NCzzp+cTc7zyJmloT6p+jZQWHbqFyYOINH+LqghBWp8cLjQ==", "license": "MIT", "dependencies": { "cron": "^3.2.1", diff --git a/package.json b/package.json index deca04cc3..c0ec1d030 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "async": "^3.2.5", "aws-sdk": "^2.1637.0", "basic-auth": "^2.0.1", - "cloudron-manifestformat": "^5.25.0", + "cloudron-manifestformat": "^5.26.1", "connect": "^3.7.0", "connect-lastmile": "^2.2.0", "connect-timeout": "^1.9.0", diff --git a/src/services.js b/src/services.js index 40a0e8a63..d2be05d56 100644 --- a/src/services.js +++ b/src/services.js @@ -42,7 +42,6 @@ const addonConfigs = require('./addonconfigs.js'), assert = require('assert'), blobs = require('./blobs.js'), BoxError = require('./boxerror.js'), - child_process = require('child_process'), constants = require('./constants.js'), crypto = require('crypto'), dashboard = require('./dashboard.js'), @@ -106,8 +105,8 @@ const ADDONS = { localstorage: { setup: setupLocalStorage, teardown: teardownLocalStorage, - backup: NOOP, // no backup because it's already inside app data - restore: NOOP, + backup: backupLocalStorage, // no backup because it's already inside app data + restore: restoreLocalStorage, getDynamicEnvironment: NOOP, clear: clearLocalStorage, }, @@ -206,15 +205,7 @@ const ADDONS = { restore: setupOidc, getDynamicEnvironment: getDynamicEnvironmentOidc, clear: NOOP, - }, - sqlite: { - setup: setupSqlite, - teardown: NOOP, - backup: backupSqlite, - restore: restoreSqlite, - getDynamicEnvironment: NOOP, - clear: clearSqlite, - }, + } }; // services are actual containers that are running. addons are the concepts requested by app @@ -912,6 +903,56 @@ async function teardownLocalStorage(app, options) { if (error) throw new BoxError(BoxError.FS_ERROR, error); } +async function backupSqlite(app, options) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + + debug('Backing up sqlite'); + + const volumeDataDir = await apps.getStorageDir(app); + + const cmd = `sqlite3 ${options.path} ".dump"`; + const runCmd = `docker run --rm --name=sqlite-${app.id} \ + --net cloudron \ + -v ${volumeDataDir}:/app/data \ + --label isCloudronManaged=true \ + --read-only -v /tmp -v /run ${infra.images.base} ${cmd} > ${dumpPath('sqlite', app.id)}`; + + await shell.bash(runCmd, { encoding: 'utf8' }); +} + +async function backupLocalStorage(app, options) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + + if (options.sqlite) await backupSqlite(app, options.sqlite); +} + +async function restoreSqlite(app, options) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + + debug('Restoring sqlite'); + + const volumeDataDir = await apps.getStorageDir(app); + + const cmd = `sqlite3 ${options.path}`; + const runCmd = `docker run --rm --name=sqlite-${app.id} \ + --net cloudron \ + -v ${volumeDataDir}:/app/data \ + --label isCloudronManaged=true \ + --read-only -v /tmp -v /run ${infra.images.base} ${cmd} < ${dumpPath('sqlite', app.id)}`; + + await shell.bash(runCmd, { encoding: 'utf8' }); +} + +async function restoreLocalStorage(app, options) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + + if (options.sqlite) await restoreSqlite(app, options.sqlite); +} + async function setupTurn(app, options) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); @@ -1688,101 +1729,6 @@ async function restartMongodb() { return await docker.restartContainer('mongodb'); } -async function setupSqlite(app, options) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof options, 'object'); - - debug(`Setting up sqlite`); - - await shell.spawn('docker', ['exec', '-t', app.containerId, 'sqlite3', options.path], {}); -} - -async function pipeProcessToFile(program, args, filename) { - assert.strictEqual(typeof program, 'string'); - assert(Array.isArray(args)); - assert.strictEqual(typeof filename, 'string'); - - return new Promise((resolve, reject) => { - const writeStream = fs.createWriteStream(filename); - const cp = child_process.spawn(program, args); - pipeline(cp.stdout, writeStream, (error) => { - if (error) return reject(new BoxError(BoxError.ADDONS_ERROR, `Error piping ${program} ${JSON.stringify(args)} to ${filename}: ${error.message}`)); - resolve(); - }); - - cp.on('close', function (code, signal) { // always called. after 'exit' or 'error' - if (code === 0) return; // program exited, we still have to wait for write stream to finish - - const e = new BoxError(BoxError.SHELL_ERROR, `${program} exited with code ${code} signal ${signal}`); - e.code = code; - e.signal = signal; - debug(`${program} ${args.join(' ').replace(/\n/g, '\\n')} errored`, e); - reject(e); - }); - - cp.on('error', function (error) { // when the command itself could not be started - debug(`${program} ${args.join(' ').replace(/\n/g, '\\n')} errored`, error); - }); - }); -} - -async function backupSqlite(app, options) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof options, 'object'); - - debug('Backing up sqlite'); - - await pipeProcessToFile('docker', ['exec', '-t', app.containerId, 'sqlite3', options.path, '.dump'], dumpPath('sqlite', app.id)); -} - -async function pipeFileToProcess(filename, program, args) { - assert.strictEqual(typeof url, 'string'); - assert.strictEqual(typeof program, 'string'); - assert(Array.isArray(args)); - - return new Promise((resolve, reject) => { - const readStream = fs.createReadStream(filename); - const cp = child_process.spawn(program, args); - pipeline(readStream, cp.stdin, function (error) { - if (error) return reject(new BoxError(BoxError.ADDONS_ERROR, `Error piping ${filename} to ${program} ${JSON.stringify(args)}: ${error.message}`)); - resolve(); - }); - - cp.on('close', function (code, signal) { // always called. after 'exit' or 'error' - if (code === 0) return; // program exited, we still have to wait for write stream to finish - - const e = new BoxError(BoxError.SHELL_ERROR, `${program} exited with code ${code} signal ${signal}`); - e.code = code; - e.signal = signal; - debug(`${program} ${args.join(' ').replace(/\n/g, '\\n')} errored`, e); - reject(e); - }); - - cp.on('error', function (error) { // when the command itself could not be started - debug(`${program} ${args.join(' ').replace(/\n/g, '\\n')} errored`, error); - }); - }); -} - -async function restoreSqlite(app, options) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof options, 'object'); - - debug('Restoring sqlite'); - - await pipeFileToProcess(dumpPath, 'docker', ['exec', '-t', app.containerId, 'sqlite3', dumpPath('sqlite', app.id)]); -} - -async function clearSqlite(app, options) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof options, 'object'); - - debug(`Clearing sqlite`); - - await shell.spawn('docker', ['exec', '-t', app.containerId, 'rm', '-f', options.path], {}); - await shell.spawn('docker', ['exec', '-t', app.containerId, 'sqlite3', options.path], {}); -} - async function startGraphite(existingInfra) { assert.strictEqual(typeof existingInfra, 'object');