backup.js 5.5 KB

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