const fs = require('fs') const sqlite3 = require('sqlite3') const path = require('path') const log = require('./util/log') const filehash = require('./util/backup_filehash') const manifestMBDBParse = require('./util/manifest_mbdb_parse') const os = require('os') /** * Backup3 is the version 4 of the backup library. * It focuses on file lookups, and better error handling. */ class Backup { /** * Create a new backup instance. * @param {*} base path to backups folder.. Defaults to '~/Library/Application Support/MobileSync/Backup/' * @param {*} id directory name of the backup. */ constructor (base, id) { log.verbose(`create backup with base=${base}, id=${id}`) id = id || '' base = base || '' // Very wierd, but unwrap from existing backup instance. if (id.constructor === Backup) { id = id.id log.verbose(`unwrapping backup to id=${id}`) } this.id = id this.base = base // Get the path of the folder. if (base) { this.path = path.join(base, id) } else { this.path = path.join(os.homedir(), '/Library/Application Support/MobileSync/Backup/', id) } } /** * Derive a file's ID from it's filename and domain. * @param {string} file the path to the file in the domain * @param {string=} domain (optional) the file's domain. Default: HomeDomain */ getFileID (path, domain) { return Backup.getFileID(path, domain) } /** * Derive a file's ID from it's filename and domain. * @param {string} file the path to the file in the domain * @param {string=} domain (optional) the file's domain. Default: HomeDomain */ static getFileID (path, domain) { return filehash(path, domain) } /** * Get the on-disk filename of a fileID. * You shouldn't really ever need to use the isAbsolute flag at all. * By default, it searches both possibile paths. * * @param {*} fileID the file ID. derive using getFileID() * @param {boolean=} isAbsoulte (optional) default: false. should we check other file locations?. * @throws Throws an error if no file is found */ getFileName (fileID, isAbsoulte) { // Default to non-absolute paths. isAbsoulte = isAbsoulte || false // Possible file locations for an ID let possibilities if (isAbsoulte) { // We must only check in the root folder of the backup. possibilities = [ path.join(this.path, fileID) ] } else { // Check in both /abcdefghi and /ab/abcdefghi possibilities = [ path.join(this.path, fileID), path.join(this.path, fileID.substr(0, 2), fileID) ] } // Return first path that works. for (let path of possibilities) { log.verbose('trying', path, fs.existsSync(path)) // Check if the path exists if (fs.existsSync(path)) { log.verbose('trying', path, '[found]') // If it does, return it. return path } } // Throw an error. throw new Error(`Could not find a file needed for this report. It may not be compatibile with this specific backup or iOS Version.`) } /// Get the manifest for an sqlite database if available getSqliteFileManifest () { return new Promise(async (resolve, reject) => { this.openDatabase('Manifest.db', true) .then(db => { db.all('SELECT fileID, domain, relativePath as filename from FILES', async function (err, rows) { if (err) reject(err) resolve(rows) }) }) .catch(reject) }) } /// Get the manifest from the mbdb file getMBDBFileManifest () { return new Promise((resolve, reject) => { let mbdbPath = this.getFileName('Manifest.mbdb', true) manifestMBDBParse.process(mbdbPath, resolve, reject) }) } /// Try to load both of the manifest files getManifest () { return new Promise(async (resolve, reject) => { // Try the new sqlite file database. try { log.verbose('Trying sqlite manifest...') let item = await this.getSqliteFileManifest(this) return resolve(item) } catch (e) { log.verbose('Trying sqlite manifest... [failed]', e) } // Try the mbdb file database try { log.verbose('Trying mbdb manifest...') let item = await this.getMBDBFileManifest(this) return resolve(item) } catch (e) { log.verbose('Trying mbdb manifest...[failed]', e) } reject(new Error('Could not find a manifest.')) }) } /** * Open a database referenced by a fileID * It uses getFileName(), so it tries both v2 and v3 paths. * @param {string} fileID ihe file id * @param {boolean=} isAbsoulte is this an absolute path? default: false. * @returns {Promise.} database instance. */ openDatabase (fileID, isAbsoulte) { return new Promise((resolve, reject) => { try { // Lookup the filename let file = this.getFileName(fileID, isAbsoulte) // Open as read only let db = new sqlite3.Database(file, sqlite3.OPEN_READONLY, (err) => { if (err) { return reject(err) } if (db != null) { resolve(db) } else { reject(new Error('did not get a database instance.')) } }) } catch (e) { return reject(e) } }) } } module.exports = Backup