files.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. const fs = require('fs-extra')
  2. const path = require('path')
  3. const log = require('../../util/log')
  4. const manifestMBDBParse = require('../../util/manifest_mbdb_parse')
  5. const plist = require('../../util/plist')
  6. const Mode = require('stat-mode');
  7. module.exports = {
  8. version: 4,
  9. name: 'backup.files',
  10. description: `Gets a backup's file list`,
  11. requiresBackup: true,
  12. // Run on a v3 lib / backup object.
  13. run (lib, { backup, extract, filter }) {
  14. return new Promise(async (resolve, reject) => {
  15. getManifest(backup)
  16. .then(files => {
  17. // Possibly extract objects.
  18. if (extract) {
  19. extractFiles(backup, extract, filter, files)
  20. }
  21. resolve(files)
  22. })
  23. .catch(reject)
  24. })
  25. },
  26. // Available fields.
  27. output: {
  28. id: el => el.fileID,
  29. domain: el => el.domain,
  30. path: el => el.filename,
  31. size: el => el.filelen || 0,
  32. mtime: el => el.mtime || 0,
  33. mode: el => new Mode(el).toString()
  34. }
  35. }
  36. /// Get the manifest for an sqlite database if available
  37. function getSqliteFileManifest (backup) {
  38. return new Promise(async (resolve, reject) => {
  39. backup.openDatabase('Manifest.db', true)
  40. .then(db => {
  41. db.all('SELECT fileID, domain, relativePath as filename, file from FILES', async function (err, rows) {
  42. if (err) reject(err)
  43. // Extract binary plist metadata
  44. for (var row of rows) {
  45. let data = plist.parseBuffer(row.file)
  46. let metadata = data['$objects'][1];
  47. row.filelen = metadata.Size
  48. row.mode = metadata.Mode
  49. row.mtime = row.atime = metadata.LastModified
  50. }
  51. resolve(rows)
  52. })
  53. })
  54. .catch(reject)
  55. })
  56. }
  57. /// Get the manifest from the mbdb file
  58. function getMBDBFileManifest (backup) {
  59. return new Promise((resolve, reject) => {
  60. let mbdbPath = backup.getFileName('Manifest.mbdb', true)
  61. manifestMBDBParse.process(mbdbPath, resolve, reject)
  62. })
  63. }
  64. /// Try to load both of the manifest files
  65. function getManifest (backup) {
  66. return new Promise(async (resolve, reject) => {
  67. // Try the new sqlite file database.
  68. try {
  69. log.verbose('Trying sqlite manifest...')
  70. let item = await getSqliteFileManifest(backup)
  71. return resolve(item)
  72. } catch (e) {
  73. log.verbose('Trying sqlite manifest... [failed]', e)
  74. }
  75. // Try the mbdb file database
  76. try {
  77. log.verbose('Trying mbdb manifest...')
  78. let item = await getMBDBFileManifest(backup)
  79. return resolve(item)
  80. } catch (e) {
  81. log.verbose('Trying mbdb manifest...[failed]', e)
  82. }
  83. reject(new Error('Could not find a manifest.'))
  84. })
  85. }
  86. /// Filter exclusion check
  87. function isIncludedByFilter (filter, item, filePath) {
  88. if (filter === 'all' || filter === undefined)
  89. return true;
  90. for (var f of Array.isArray(filter) ? filter : [filter]) {
  91. if (!isIncludedBySingleFilter(f, item, filePath))
  92. return false;
  93. }
  94. return true;
  95. }
  96. function isIncludedBySingleFilter (filter, item, filePath) {
  97. for (var x of [item.domain, item.filename, filePath]) {
  98. if (isIncludedBySingleFilterCheck(filter, x))
  99. return true;
  100. }
  101. }
  102. function isIncludedBySingleFilterCheck (filter, x) {
  103. if (filter instanceof RegExp)
  104. return x.search(filter) > -1
  105. else if (filter instanceof Function)
  106. return filter(x)
  107. else
  108. return x.indexOf(filter) > -1;
  109. }
  110. /// Extract files
  111. /// - backup: the backup api object
  112. /// - destination: file system location
  113. /// - filter: contains check filter for files
  114. /// - items: list of files.
  115. function extractFiles (backup, destination, filter, items) {
  116. for (var item of items) {
  117. try {
  118. var domainPath = item.domain
  119. if (domainPath.match(/^AppDomain.*-/)) {
  120. // Extract sub-domain from app domain
  121. domainPath = domainPath.replace('-', path.sep)
  122. }
  123. domainPath = domainPath.replace('Domain', '')
  124. var filePath = path.join(domainPath, item.filename)
  125. // Skip items not included by the filter
  126. if (!isIncludedByFilter(filter, item, filePath)) {
  127. // Skip to the next iteration of the loop.
  128. log.action('skipped', filePath)
  129. continue
  130. }
  131. var stat = new Mode(item)
  132. if (stat.isSymbolicLink()) {
  133. log.warning('skipping symlink', filePath, 'to', item.linktarget)
  134. // FIXME: Restore symlinks
  135. continue
  136. }
  137. // Calculate the output path
  138. var outPath = path.join(destination, filePath)
  139. if (stat.isDirectory()) {
  140. log.action('mkdir', filePath)
  141. fs.ensureDirSync(outPath)
  142. } else if (stat.isFile()) {
  143. let sourceFile = backup.getFileName(item.fileID)
  144. // Only process files that exist.
  145. if (fs.existsSync(sourceFile)) {
  146. log.action('export', filePath)
  147. fs.copySync(sourceFile, outPath)
  148. fs.utimesSync(outPath, item.atime, item.mtime)
  149. } else {
  150. log.error('not found', sourceFile)
  151. }
  152. } else {
  153. throw new Error('unknown filetype')
  154. }
  155. // Save output info to the data item.
  156. item.output_dir = outPath
  157. } catch (e) {
  158. log.error(item.fileID, item.filename, e.toString())
  159. }
  160. }
  161. }