'use strict'; exports = module.exports = { get, getIcons, getIcon, add, list, listBackupIds, del, }; const assert = require('node:assert'), BoxError = require('./boxerror.js'), crypto = require('node:crypto'), database = require('./database.js'), eventlog = require('./eventlog.js'), safe = require('safetydance'); const ARCHIVE_FIELDS = [ 'archives.id', 'backupId', 'archives.creationTime', 'backups.remotePath', 'backups.manifestJson', 'backups.appConfigJson', '(archives.icon IS NOT NULL) AS hasIcon', '(archives.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ]; function postProcess(result) { assert.strictEqual(typeof result, 'object'); result.appConfig = result.appConfigJson ? safe.JSON.parse(result.appConfigJson) : null; delete result.appConfigJson; result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null; delete result.manifestJson; result.iconUrl = result.hasIcon || result.hasAppStoreIcon ? `/api/v1/archives/${result.id}/icon` : null; return result; } async function get(id) { assert.strictEqual(typeof id, 'string'); const result = await database.query(`SELECT ${ARCHIVE_FIELDS} FROM archives LEFT JOIN backups ON archives.backupId = backups.id WHERE archives.id = ? ORDER BY creationTime DESC`, [ id ]); if (result.length === 0) return null; return postProcess(result[0]); } async function getIcons(id) { assert.strictEqual(typeof id, 'string'); const results = await database.query('SELECT icon, appStoreIcon FROM archives WHERE id=?', [ id ]); if (results.length === 0) return null; return { icon: results[0].icon, appStoreIcon: results[0].appStoreIcon }; } async function getIcon(id, options) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof options, 'object'); const icons = await getIcons(id); if (!icons) throw new BoxError(BoxError.NOT_FOUND, 'No such backup'); if (!options.original && icons.icon) return icons.icon; if (icons.appStoreIcon) return icons.appStoreIcon; return null; } async function add(backupId, data, auditSource) { assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof data, 'object'); assert(auditSource && typeof auditSource === 'object'); const id = crypto.randomUUID(); const [error] = await safe(database.query('INSERT INTO archives (id, backupId, icon, appStoreIcon) VALUES (?, ?, ?, ?)', [ id, backupId, data.icon, data.appStoreIcon ])); if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, 'Backup not found'); if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'Archive already exists'); if (error) throw error; await eventlog.add(eventlog.ACTION_ARCHIVES_ADD, auditSource, { id, backupId }); return id; } async function list(page, perPage) { assert(typeof page === 'number' && page > 0); assert(typeof perPage === 'number' && perPage > 0); const results = await database.query(`SELECT ${ARCHIVE_FIELDS} FROM archives LEFT JOIN backups ON archives.backupId = backups.id ORDER BY creationTime DESC LIMIT ?,?`, [ (page-1)*perPage, perPage ]); results.forEach(function (result) { postProcess(result); }); return results; } async function listBackupIds() { const results = await database.query(`SELECT backupId FROM archives`, []); return results.map(r => r.backupId); } async function del(archive, auditSource) { assert.strictEqual(typeof archive, 'object'); assert(auditSource && typeof auditSource === 'object'); const result = await database.query('DELETE FROM archives WHERE id=?', [ archive.id ]); if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'No such archive'); await eventlog.add(eventlog.ACTION_ARCHIVES_DEL, auditSource, { id: archive.id, backupId: archive.backupId }); }