Last active
September 29, 2025 13:00
-
-
Save foxt/247f9480103696e1fadc88fa1021f1f1 to your computer and use it in GitHub Desktop.
Node.js script to extract files from an iOS backup created by iTunes or Finder.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // / \ WARNING: This script *moves* files from the backup directory to the output directory. | |
| // / ! \ Meaning, if you after this script is run, the backup directory will not be able to be restored. | |
| // /_____\ If you want to keep the backup directory intact, make a copy of it first. | |
| // Extract files from an iOS backup created by iTunes or Finder. | |
| // Usage: node extract.js <input_backup_directory> <output_directory> | |
| import { existsSync, mkdirSync, renameSync } from 'node:fs'; | |
| import path, { join } from 'node:path'; | |
| import { DatabaseSync } from 'node:sqlite'; | |
| // const bplist = require('bplist-parser'); | |
| if (!process.argv[2] || !process.argv[3]) { | |
| console.error('Usage: node extract.js <input_backup_directory> <output_directory>'); | |
| process.exit(1); | |
| } | |
| const indir = path.resolve(process.argv[2]); | |
| console.log('Input directory:', indir); | |
| if (!existsSync(path.join(indir, 'Manifest.db'))) { | |
| console.error('Manifest.db not found in', indir); | |
| process.exit(1); | |
| } | |
| const outdir = path.resolve(process.argv[3]); | |
| console.log('Output directory:', outdir); | |
| const db = new DatabaseSync(join(indir,'Manifest.db'), { readOnly: true }); | |
| const query = db.prepare('SELECT * FROM Files'); | |
| const rows = query.all(); | |
| function swapDomain(domain) { | |
| // e.g., 'AppDomain-com.example.app' -> 'com.example.app-AppDomain' (better experience viewing in file browser when sorted alphabetically) | |
| let parts = domain.split('-'); | |
| parts.push(parts.shift()); | |
| return parts.join('-'); | |
| } | |
| for (let file of rows) { | |
| let fileId = file.domain + '-' + file.relativePath; | |
| let swappedDomain = swapDomain(file.domain) | |
| let tgtPath = path.join(outdir, swappedDomain, file.relativePath); | |
| if (file.flags == 4) { | |
| console.warn('Skipping symlink', fileId); | |
| } else if (file.flags == 2) { | |
| mkdirSync(tgtPath, { recursive: true }); | |
| } else if (file.flags == 1) { | |
| let srcPath = path.join(indir, file.fileID.substring(0,2), file.fileID); | |
| if (!existsSync(srcPath)) { | |
| console.warn('Source file not found for', fileId, 'at', srcPath); | |
| continue; | |
| } | |
| mkdirSync(path.dirname(tgtPath), { recursive: true }); | |
| renameSync(srcPath, tgtPath); | |
| } else { | |
| throw new Error('Unknown file flag ' + file.flags + ' for ' + fileId); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment