cli.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. #!/usr/bin/env node
  2. const program = require('commander')
  3. const chalk = require('chalk')
  4. const log = require('./util/log')
  5. const report = require('./reports')
  6. const matcher = require('./util/matcher')
  7. const Group = report.Group
  8. const core = require('./index')
  9. const packageJSON = require('../package.json')
  10. const { runSingleReport, runSwitchedReport } = require('./util/report_runner')
  11. var formatters = {
  12. 'json': require('./formatters/json'),
  13. 'table': require('./formatters/table'),
  14. 'raw': require('./formatters/raw-json'),
  15. 'raw-json': require('./formatters/raw-json'),
  16. 'csv': require('./formatters/csv'),
  17. 'raw-csv': require('./formatters/raw-csv')
  18. }
  19. process.on('unhandledRejection', (e) => {
  20. console.log('[cli.js] unhandled rejection', e)
  21. process.exit(1)
  22. })
  23. program
  24. .version(packageJSON.version)
  25. .option('-l, --list', 'List Backups')
  26. .option(`-b, --backup <backup>`, 'Backup ID')
  27. .option(`-d, --dir <directory>`, `Backup Directory (default: ${core.base})`)
  28. .option('-r, --report <report_type>', 'Select a report type. see below for a full list.')
  29. .option('-i, --id <id>', 'Specify an ID for filtering certain reports')
  30. .option('-f, --formatter <type>', 'Specify output format. default: table')
  31. .option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files, manifest, addressbook')
  32. .option('-o, --output <path>', 'Specify an output directory for files to be written to.')
  33. .option(`-v, --verbose`, 'Verbose debugging output')
  34. .option(` --plugins <plugins>`, 'List of pluging modules to use')
  35. .option(` --filter <filter...>`, 'Filters output for individual reports.')
  36. .option(` --regex-filter <filter...>`, 'Filters output for individual reports using a regular expression.')
  37. .option(' --join-reports', 'Join JSON reports together. (available for -f json or -f raw only!)')
  38. .option(` --no-color`, 'Disable colorized output')
  39. .option(` --dump`, 'alias for "--formatter raw"')
  40. .option(` --quiet`, 'quiet all messages, except for errors and raw output')
  41. .option(` --available`, 'output a list of available reports')
  42. program.on('--help', function () {
  43. console.log('')
  44. console.log(`Version: ${packageJSON.version}`)
  45. console.log('')
  46. console.log('Run ibackuptool --available for a listing of report types.')
  47. console.log('')
  48. console.log("If you're interested to know how this works, check out my post:")
  49. console.log('https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup')
  50. console.log('')
  51. console.log('Issue tracker:')
  52. console.log('https://github.com/richinfante/iphonebackuptools/issues')
  53. console.log('')
  54. })
  55. function printReportList () {
  56. // Print a report group and recursively print children
  57. function printGroup (group, i, pn) {
  58. i = i || 0
  59. pn = pn || ''
  60. for (let [name, report] of Object.entries(group)) {
  61. // Ignore groups
  62. if (name === '__group') { continue }
  63. if (report instanceof Group || report.__group === true) {
  64. // console.log(`${' '.repeat(i * 2)}- ${pn}${name}`.padStart(i * 2))
  65. printGroup(report, i + 1, `${pn}${name}.`)
  66. } else {
  67. console.log(`- ${chalk.green(pn + name)}${report.deprecated ? chalk.gray(' [deprecated]') : ''}: ${report.description} `)
  68. }
  69. }
  70. }
  71. printGroup(core.modules)
  72. }
  73. process.on('unhandledRejection', (e) => {
  74. console.log('[index.js] unhandled rejection', e)
  75. process.exit(1)
  76. })
  77. // If we're the main module, run some things.
  78. if (require.main === module) {
  79. init()
  80. }
  81. // Initialize the tool.
  82. function init () {
  83. program.parse(process.argv)
  84. if (program.available) {
  85. printReportList()
  86. process.exit(0)
  87. }
  88. // Set the log level
  89. log.setVerbose(program.quiet ? 0 : (program.verbose ? 2 : 1))
  90. // Parse plugins
  91. program.plugins = program.plugins || process.env.IBACKUPTOOL_PLUGINS || ''
  92. program.plugins = program.plugins.split(',')
  93. .map(name => name.trim())
  94. .filter(name => name !== '')
  95. .map(path => require(path))
  96. // Register witht the core module
  97. core.registerModule(program.plugins)
  98. log.verbose('Plugins:', program.plugins)
  99. // Save the formatter
  100. program.formatter = formatters[program.formatter] || formatters.table
  101. // Legacy support for `--dump` flag.
  102. if (program.dump) {
  103. program.formatter = formatters.raw
  104. }
  105. // Disable color for non-ttys.
  106. if (!process.stdout.isTTY) { program.color = false }
  107. // Find the base if it is set
  108. if (program.dir) {
  109. core.base = program.dir
  110. }
  111. log.verbose('Base Directory:', core.base)
  112. main().then(() => {})
  113. }
  114. /**
  115. * Main CLI function
  116. */
  117. async function main () {
  118. // Legacy support for --list (-l) flag
  119. if (program.list) {
  120. program.report = 'backups.list'
  121. }
  122. log.verbose('Top-level modules', Object.keys(core.modules))
  123. if (program.report) {
  124. var reportContents = []
  125. // Turn the report argument into an array of report type names
  126. var selectedTypes = program.report
  127. .split(',')
  128. .map(el => el.trim())
  129. .filter(el => el !== '')
  130. let selectedReports = []
  131. // Match an array of reports.
  132. for (let query of selectedTypes) {
  133. selectedReports = [
  134. ...selectedReports,
  135. ...matcher(core.modules, query, (el) => !(el instanceof Group || el.__group === true))
  136. ]
  137. }
  138. let set = new Set(selectedReports)
  139. log.verbose('selected set', set)
  140. // If the set's size is 0, throw an error.
  141. if (set.size === 0) {
  142. log.error(`Couldn't run reports specified by: '${program.report}'.`)
  143. log.error(`No matching reports were found.`)
  144. process.exit(1)
  145. }
  146. // Iterate over each item in the set.
  147. for (let report of set.values()) {
  148. if (report.version && report.version >= 3) {
  149. try {
  150. log.begin('run', report.name)
  151. var filters = program.filter || [];
  152. for (var filter of program.regexFilter || [])
  153. filters.push(new RegExp(filter));
  154. // Create some parameters to send by default.
  155. let params = {
  156. backup: program.backup,
  157. extract: program.extract,
  158. filter: filters,
  159. id: program.id,
  160. raw: !!program.formatter.isRaw
  161. }
  162. // Run a v3 report.
  163. let contents = await core.runReport(report, params)
  164. // Possibly symbolicate
  165. contents = await core.compileReport(report, contents, params)
  166. // Format the v3 report's result.
  167. let formattedContent = program.formatter.format(contents, {
  168. program
  169. })
  170. // Push onto the list to be compiled.
  171. reportContents.push({
  172. name: report.name,
  173. contents: formattedContent
  174. })
  175. log.end()
  176. } catch (e) {
  177. log.end()
  178. log.error(`Couldn't run '${report.name}'.`)
  179. log.error(e)
  180. }
  181. } else {
  182. // Older reports still work for file and screen output
  183. // They just may not be as compatible
  184. // They cannot be called from the module's api.
  185. if (selectedReports.length > 1 && !report.usesPromises) {
  186. log.warning('the report', report.name, 'does not utilize promises.')
  187. log.warning('this may not work')
  188. }
  189. log.begin('run', report.name)
  190. // Check if there's a backup specified and one is required.
  191. if (report.requiresBackup) {
  192. if (!program.backup) {
  193. log.error('use -b or --backup <id> to specify backup.')
  194. process.exit(1)
  195. }
  196. }
  197. try {
  198. if (report.func) {
  199. let contents = await runSingleReport(report, program)
  200. if (contents == null) { log.end(); continue }
  201. reportContents.push({
  202. name: report.name,
  203. contents: contents
  204. })
  205. } else if (report.functions) {
  206. let contents = await runSwitchedReport(report, program)
  207. if (contents == null) { log.end(); continue }
  208. reportContents.push({
  209. name: report.name,
  210. contents: contents
  211. })
  212. }
  213. } catch (e) {
  214. log.error(`Couldn't run '${report.name}'.`)
  215. log.error(e)
  216. }
  217. log.end()
  218. }
  219. }
  220. program.formatter.finalReport(reportContents, program)
  221. } else {
  222. program.outputHelp()
  223. }
  224. }