backup.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. const fs = require('fs')
  2. const sqlite3 = require('sqlite3')
  3. const path = require('path')
  4. const log = require('./util/log')
  5. const filehash = require('./util/backup_filehash')
  6. const manifestMBDBParse = require('./util/manifest_mbdb_parse')
  7. const os = require('os')
  8. /**
  9. * Backup3 is the version 4 of the backup library.
  10. * It focuses on file lookups, and better error handling.
  11. */
  12. class Backup {
  13. /**
  14. * Create a new backup instance.
  15. * @param {*} base path to backups folder.. Defaults to '~/Library/Application Support/MobileSync/Backup/'
  16. * @param {*} id directory name of the backup.
  17. */
  18. constructor (base, id) {
  19. log.verbose(`create backup with base=${base}, id=${id}`)
  20. id = id || ''
  21. base = base || ''
  22. // Very wierd, but unwrap from existing backup instance.
  23. if (id.constructor === Backup) {
  24. id = id.id
  25. log.verbose(`unwrapping backup to id=${id}`)
  26. }
  27. this.id = id
  28. this.base = base
  29. // Get the path of the folder.
  30. if (base) {
  31. this.path = path.join(base, id)
  32. } else {
  33. this.path = path.join(os.homedir(), '/Library/Application Support/MobileSync/Backup/', id)
  34. }
  35. }
  36. /**
  37. * Derive a file's ID from it's filename and domain.
  38. * @param {string} file the path to the file in the domain
  39. * @param {string=} domain (optional) the file's domain. Default: HomeDomain
  40. */
  41. getFileID (path, domain) {
  42. return Backup.getFileID(path, domain)
  43. }
  44. /**
  45. * Derive a file's ID from it's filename and domain.
  46. * @param {string} file the path to the file in the domain
  47. * @param {string=} domain (optional) the file's domain. Default: HomeDomain
  48. */
  49. static getFileID (path, domain) {
  50. return filehash(path, domain)
  51. }
  52. /**
  53. * Get the on-disk filename of a fileID.
  54. * You shouldn't really ever need to use the isAbsolute flag at all.
  55. * By default, it searches both possibile paths.
  56. *
  57. * @param {*} fileID the file ID. derive using getFileID()
  58. * @param {boolean=} isAbsoulte (optional) default: false. should we check other file locations?.
  59. * @throws Throws an error if no file is found
  60. */
  61. getFileName (fileID, isAbsoulte) {
  62. // Default to non-absolute paths.
  63. isAbsoulte = isAbsoulte || false
  64. // Possible file locations for an ID
  65. let possibilities
  66. if (isAbsoulte) {
  67. // We must only check in the root folder of the backup.
  68. possibilities = [
  69. path.join(this.path, fileID)
  70. ]
  71. } else {
  72. // Check in both /abcdefghi and /ab/abcdefghi
  73. possibilities = [
  74. path.join(this.path, fileID),
  75. path.join(this.path, fileID.substr(0, 2), fileID)
  76. ]
  77. }
  78. // Return first path that works.
  79. for (let path of possibilities) {
  80. log.verbose('trying', path, fs.existsSync(path))
  81. // Check if the path exists
  82. if (fs.existsSync(path)) {
  83. log.verbose('trying', path, '[found]')
  84. // If it does, return it.
  85. return path
  86. }
  87. }
  88. // Throw an error.
  89. throw new Error(`Could not find a file needed for this report. It may not be compatibile with this specific backup or iOS Version.`)
  90. }
  91. /// Get the manifest for an sqlite database if available
  92. getSqliteFileManifest () {
  93. return new Promise(async (resolve, reject) => {
  94. this.openDatabase('Manifest.db', true)
  95. .then(db => {
  96. db.all('SELECT fileID, domain, relativePath as filename from FILES', async function (err, rows) {
  97. if (err) reject(err)
  98. resolve(rows)
  99. })
  100. })
  101. .catch(reject)
  102. })
  103. }
  104. /// Get the manifest from the mbdb file
  105. getMBDBFileManifest () {
  106. return new Promise((resolve, reject) => {
  107. let mbdbPath = this.getFileName('Manifest.mbdb', true)
  108. manifestMBDBParse.process(mbdbPath, resolve, reject)
  109. })
  110. }
  111. /// Try to load both of the manifest files
  112. getManifest () {
  113. return new Promise(async (resolve, reject) => {
  114. // Try the new sqlite file database.
  115. try {
  116. log.verbose('Trying sqlite manifest...')
  117. let item = await this.getSqliteFileManifest(this)
  118. return resolve(item)
  119. } catch (e) {
  120. log.verbose('Trying sqlite manifest... [failed]', e)
  121. }
  122. // Try the mbdb file database
  123. try {
  124. log.verbose('Trying mbdb manifest...')
  125. let item = await this.getMBDBFileManifest(this)
  126. return resolve(item)
  127. } catch (e) {
  128. log.verbose('Trying mbdb manifest...[failed]', e)
  129. }
  130. reject(new Error('Could not find a manifest.'))
  131. })
  132. }
  133. /**
  134. * Open a database referenced by a fileID
  135. * It uses getFileName(), so it tries both v2 and v3 paths.
  136. * @param {string} fileID ihe file id
  137. * @param {boolean=} isAbsoulte is this an absolute path? default: false.
  138. * @returns {Promise.<sqlite3.Database>} database instance.
  139. */
  140. openDatabase (fileID, isAbsoulte) {
  141. return new Promise((resolve, reject) => {
  142. try {
  143. // Lookup the filename
  144. let file = this.getFileName(fileID, isAbsoulte)
  145. // Open as read only
  146. let db = new sqlite3.Database(file, sqlite3.OPEN_READONLY, (err) => {
  147. if (err) { return reject(err) }
  148. if (db != null) {
  149. resolve(db)
  150. } else {
  151. reject(new Error('did not get a database instance.'))
  152. }
  153. })
  154. } catch (e) {
  155. return reject(e)
  156. }
  157. })
  158. }
  159. }
  160. module.exports = Backup