2025-07-24 18:09:33 +02:00
'use strict' ;
exports = module . exports = {
get ,
getByIdentifierAndStatePaged ,
2025-07-25 14:03:31 +02:00
getLatestInTargetByIdentifier , // brutal function name
2025-07-24 18:09:33 +02:00
getByTypePaged ,
add ,
update ,
setState ,
2025-08-11 19:30:22 +05:30
setIntegrity ,
2025-07-24 18:09:33 +02:00
list ,
del ,
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' ,
} ;
const assert = require ( 'assert' ) ,
BoxError = require ( './boxerror.js' ) ,
database = require ( './database.js' ) ,
hat = require ( './hat.js' ) ,
safe = require ( 'safetydance' ) ;
2025-08-11 19:30:22 +05:30
const BACKUPS _FIELDS = [ 'id' , 'remotePath' , 'label' , 'identifier' , 'creationTime' , 'packageVersion' , 'type' , 'integrityJson' , 'dependsOnJson' , 'state' , 'manifestJson' , 'preserveSecs' , 'encryptionVersion' , 'appConfigJson' , 'targetId' ] . 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-07-24 18:09:33 +02:00
result . appConfig = result . appConfigJson ? safe . JSON . parse ( result . appConfigJson ) : null ;
delete result . appConfigJson ;
return result ;
}
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-07-25 07:44:25 +02:00
assert . strictEqual ( typeof data . targetId , '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 ;
const [ error ] = await safe ( database . query ( 'INSERT INTO backups (id, remotePath, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOnJson, manifestJson, preserveSecs, appConfigJson, targetId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
2025-07-25 07:44:25 +02:00
[ id , data . remotePath , data . identifier , data . encryptionVersion , data . packageVersion , data . type , creationTime , data . state , JSON . stringify ( data . dependsOn ) , manifestJson , data . preserveSecs , appConfigJson , data . targetId ] ) ) ;
2025-07-24 18:09:33 +02:00
if ( error && error . code === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'Backup already exists' ) ;
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-07-25 14:03:31 +02:00
async function getLatestInTargetByIdentifier ( identifier , targetId ) {
assert . strictEqual ( typeof identifier , 'string' ) ;
assert . strictEqual ( typeof targetId , 'string' ) ;
const results = await database . query ( ` SELECT ${ BACKUPS _FIELDS } FROM backups WHERE identifier = ? AND state = ? AND targetId = ? LIMIT 1 ` , [ identifier , exports . BACKUP _STATE _NORMAL , targetId ] ) ;
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 ] ) ;
}
async function getByTypePaged ( 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 ) ; } ) ;
return results ;
}
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 ) {
if ( p === 'label' || p === 'preserveSecs' ) {
fields . push ( p + ' = ?' ) ;
values . push ( data [ p ] ) ;
}
}
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 setState ( id , state ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof state , 'string' ) ;
const result = await database . query ( 'UPDATE backups SET state = ? WHERE id = ?' , [ state , id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
}
2025-08-11 19:30:22 +05:30
async function setIntegrity ( id , integrity ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof integrity , 'object' ) ;
const result = await database . query ( 'UPDATE backups SET integrityJson = ? WHERE id = ?' , [ JSON . stringify ( integrity ) , id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
}
2025-07-24 18:09:33 +02:00
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 ) ; } ) ;
return results ;
}
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' ) ;
}