2025-07-24 18:09:33 +02:00
'use strict' ;
2025-10-08 20:11:55 +02:00
exports = module . exports = {
get ,
getByIdentifierAndStatePaged ,
getLatestInTargetByIdentifier , // brutal function name
add ,
update ,
list ,
listBySiteId ,
listByTypePaged ,
del ,
removePrivateFields ,
startIntegrityCheck ,
BACKUP _IDENTIFIER _BOX : 'box' ,
BACKUP _IDENTIFIER _MAIL : 'mail' ,
BACKUP _TYPE _APP : 'app' ,
BACKUP _TYPE _BOX : 'box' ,
BACKUP _TYPE _MAIL : 'mail' ,
BACKUP _STATE _NORMAL : 'normal' , // should rename to created to avoid listing in UI?
BACKUP _STATE _CREATING : 'creating' ,
BACKUP _STATE _ERROR : 'error' ,
} ;
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2025-07-24 18:09:33 +02:00
BoxError = require ( './boxerror.js' ) ,
database = require ( './database.js' ) ,
2025-08-15 16:09:58 +05:30
debug = require ( 'debug' ) ( 'box:backups' ) ,
2025-07-24 18:09:33 +02:00
hat = require ( './hat.js' ) ,
2025-08-15 16:09:58 +05:30
safe = require ( 'safetydance' ) ,
tasks = require ( './tasks.js' ) ;
2025-07-24 18:09:33 +02:00
2025-09-12 09:48:37 +02:00
const BACKUPS _FIELDS = [ 'id' , 'remotePath' , 'label' , 'identifier' , 'creationTime' , 'packageVersion' , 'type' , 'integrityJson' , 'statsJson' , 'dependsOnJson' , 'state' , 'manifestJson' , 'preserveSecs' , 'encryptionVersion' , 'appConfigJson' , 'siteId' ] . join ( ',' ) ;
2025-07-24 18:09:33 +02:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
result . dependsOn = result . dependsOnJson ? safe . JSON . parse ( result . dependsOnJson ) : [ ] ;
delete result . dependsOnJson ;
result . manifest = result . manifestJson ? safe . JSON . parse ( result . manifestJson ) : null ;
delete result . manifestJson ;
2025-08-11 19:30:22 +05:30
result . integrity = result . integrityJson ? safe . JSON . parse ( result . integrityJson ) : null ;
delete result . integrityJson ;
2025-08-12 19:41:50 +05:30
result . stats = result . statsJson ? safe . JSON . parse ( result . statsJson ) : null ;
delete result . statsJson ;
2025-07-24 18:09:33 +02:00
result . appConfig = result . appConfigJson ? safe . JSON . parse ( result . appConfigJson ) : null ;
delete result . appConfigJson ;
return result ;
}
2025-08-13 19:45:52 +05:30
function removePrivateFields ( backup ) {
return backup ;
}
2025-07-24 18:09:33 +02:00
async function add ( data ) {
assert ( data && typeof data === 'object' ) ;
assert . strictEqual ( typeof data . remotePath , 'string' ) ;
assert ( data . encryptionVersion === null || typeof data . encryptionVersion === 'number' ) ;
assert . strictEqual ( typeof data . packageVersion , 'string' ) ;
assert . strictEqual ( typeof data . type , 'string' ) ;
assert . strictEqual ( typeof data . identifier , 'string' ) ;
assert . strictEqual ( typeof data . state , 'string' ) ;
assert ( Array . isArray ( data . dependsOn ) ) ;
assert . strictEqual ( typeof data . manifest , 'object' ) ;
assert . strictEqual ( typeof data . preserveSecs , 'number' ) ;
assert . strictEqual ( typeof data . appConfig , 'object' ) ;
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof data . siteId , 'string' ) ;
2025-07-24 18:09:33 +02:00
const creationTime = data . creationTime || new Date ( ) ; // allow tests to set the time
const manifestJson = JSON . stringify ( data . manifest ) ;
const prefixId = data . type === exports . BACKUP _TYPE _APP ? ` ${ data . type } _ ${ data . identifier } ` : data . type ; // type and identifier are same for other types
const id = ` ${ prefixId } _v ${ data . packageVersion } _ ${ hat ( 32 ) } ` ; // id is used by the UI to derive dependent packages. making this a UUID will require a lot of db querying
const appConfigJson = data . appConfig ? JSON . stringify ( data . appConfig ) : null ;
2025-08-13 19:33:39 +05:30
const statsJson = data . stats ? JSON . stringify ( data . stats ) : null ;
const integrityJson = data . integrity ? JSON . stringify ( data . integrity ) : null ;
2025-07-24 18:09:33 +02:00
2025-09-12 09:48:37 +02:00
const [ error ] = await safe ( database . query ( 'INSERT INTO backups (id, remotePath, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOnJson, manifestJson, preserveSecs, appConfigJson, siteId, statsJson, integrityJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
[ id , data . remotePath , data . identifier , data . encryptionVersion , data . packageVersion , data . type , creationTime , data . state , JSON . stringify ( data . dependsOn ) , manifestJson , data . preserveSecs , appConfigJson , data . siteId , statsJson , integrityJson ] ) ) ;
2025-07-24 18:09:33 +02:00
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'Backup already exists' ) ;
2025-07-24 18:09:33 +02:00
if ( error ) throw error ;
return id ;
}
async function getByIdentifierAndStatePaged ( identifier , state , page , perPage ) {
assert . strictEqual ( typeof identifier , 'string' ) ;
assert . strictEqual ( typeof state , 'string' ) ;
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
const results = await database . query ( ` SELECT ${ BACKUPS _FIELDS } FROM backups WHERE identifier = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,? ` , [ identifier , state , ( page - 1 ) * perPage , perPage ] ) ;
2025-07-25 14:03:31 +02:00
results . forEach ( postProcess ) ;
2025-07-24 18:09:33 +02:00
return results ;
}
2025-09-12 09:48:37 +02:00
async function getLatestInTargetByIdentifier ( identifier , siteId ) {
2025-07-25 14:03:31 +02:00
assert . strictEqual ( typeof identifier , 'string' ) ;
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof siteId , 'string' ) ;
2025-07-25 14:03:31 +02:00
2025-09-12 09:48:37 +02:00
const results = await database . query ( ` SELECT ${ BACKUPS _FIELDS } FROM backups WHERE identifier = ? AND state = ? AND siteId = ? LIMIT 1 ` , [ identifier , exports . BACKUP _STATE _NORMAL , siteId ] ) ;
2025-07-25 14:03:31 +02:00
if ( ! results . length ) return null ;
return postProcess ( results [ 0 ] ) ;
}
2025-07-24 18:09:33 +02:00
async function get ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
const result = await database . query ( ` SELECT ${ BACKUPS _FIELDS } FROM backups WHERE id = ? ORDER BY creationTime DESC ` , [ id ] ) ;
if ( result . length === 0 ) return null ;
return postProcess ( result [ 0 ] ) ;
}
function validateLabel ( label ) {
assert . strictEqual ( typeof label , 'string' ) ;
if ( label . length >= 200 ) return new BoxError ( BoxError . BAD _FIELD , 'label too long' ) ;
if ( /[^a-zA-Z0-9._() -]/ . test ( label ) ) return new BoxError ( BoxError . BAD _FIELD , 'label can only contain alphanumerals, space, dot, hyphen, brackets or underscore' ) ;
return null ;
}
// this is called by REST API
2025-07-25 11:49:13 +02:00
async function update ( backup , data ) {
assert . strictEqual ( typeof backup , 'object' ) ;
2025-07-24 18:09:33 +02:00
assert . strictEqual ( typeof data , 'object' ) ;
let error ;
if ( 'label' in data ) {
error = validateLabel ( data . label ) ;
if ( error ) throw error ;
}
const fields = [ ] , values = [ ] ;
for ( const p in data ) {
2025-10-20 13:22:51 +02:00
if ( p === 'label' || p === 'preserveSecs' || p === 'state' ) {
2025-07-24 18:09:33 +02:00
fields . push ( p + ' = ?' ) ;
values . push ( data [ p ] ) ;
2025-10-20 13:22:51 +02:00
} else if ( p === 'stats' ) {
fields . push ( ` ${ p } Json=? ` ) ;
values . push ( JSON . stringify ( data [ p ] ) ) ;
2025-07-24 18:09:33 +02:00
}
}
2025-07-25 11:49:13 +02:00
values . push ( backup . id ) ;
2025-07-24 18:09:33 +02:00
const result = await database . query ( 'UPDATE backups SET ' + fields . join ( ', ' ) + ' WHERE id = ?' , values ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
if ( 'preserveSecs' in data ) {
// update the dependancies
for ( const depId of backup . dependsOn ) {
await database . query ( 'UPDATE backups SET preserveSecs=? WHERE id = ?' , [ data . preserveSecs , depId ] ) ;
}
}
}
async function list ( page , perPage ) {
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
const results = await database . query ( ` SELECT ${ BACKUPS _FIELDS } FROM backups ORDER BY creationTime DESC LIMIT ?,? ` , [ ( page - 1 ) * perPage , perPage ] ) ;
results . forEach ( function ( result ) { postProcess ( result ) ; } ) ;
2025-10-07 12:07:27 +02:00
return results ;
}
async function listByTypePaged ( type , page , perPage ) {
assert . strictEqual ( typeof type , 'string' ) ;
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
const results = await database . query ( ` SELECT ${ BACKUPS _FIELDS } FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,? ` , [ type , ( page - 1 ) * perPage , perPage ] ) ;
results . forEach ( function ( result ) { postProcess ( result ) ; } ) ;
2025-07-24 18:09:33 +02:00
return results ;
}
2025-10-06 14:10:29 +02:00
async function listBySiteId ( siteId , page , perPage ) {
assert . strictEqual ( typeof siteId , 'string' ) ;
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
2025-10-07 12:11:16 +02:00
const results = await database . query ( ` SELECT ${ BACKUPS _FIELDS } FROM backups WHERE siteId=? AND type=? ORDER BY creationTime DESC LIMIT ?,? ` , [ siteId , exports . BACKUP _TYPE _BOX , ( page - 1 ) * perPage , perPage ] ) ;
2025-10-06 14:10:29 +02:00
results . forEach ( function ( result ) { postProcess ( result ) ; } ) ;
return results ;
}
2025-07-24 18:09:33 +02:00
async function del ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
const result = await database . query ( 'DELETE FROM backups WHERE id=?' , [ id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
}
2025-08-15 16:09:58 +05:30
async function startIntegrityCheck ( backup ) {
assert . strictEqual ( typeof backup , 'object' ) ;
const taskId = await tasks . add ( tasks . TASK _CHECK _BACKUP _INTEGRITY , [ backup . id ] ) ;
safe ( tasks . startTask ( taskId , { } ) , { debug } ) ; // background
return taskId ;
}
2025-10-07 18:42:51 +02:00