partial-backup 0.0.2__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {partial_backup-0.0.2 → partial_backup-0.1.1}/.github/workflows/python-app.yml +4 -1
  2. partial_backup-0.1.1/.github/workflows/python-publish.yml +29 -0
  3. partial_backup-0.1.1/PKG-INFO +169 -0
  4. partial_backup-0.1.1/README.md +146 -0
  5. partial_backup-0.1.1/partial_backup/__main__.py +48 -0
  6. partial_backup-0.1.1/partial_backup/_version.py +24 -0
  7. partial_backup-0.1.1/partial_backup/backup.py +541 -0
  8. partial_backup-0.1.1/partial_backup.egg-info/PKG-INFO +169 -0
  9. {partial_backup-0.0.2 → partial_backup-0.1.1}/partial_backup.egg-info/SOURCES.txt +4 -1
  10. partial_backup-0.1.1/partial_backup.egg-info/scm_file_list.json +24 -0
  11. partial_backup-0.1.1/partial_backup.egg-info/scm_version.json +8 -0
  12. partial_backup-0.1.1/tests/test_backup.py +138 -0
  13. partial_backup-0.0.2/.github/workflows/python-publish.yml +0 -29
  14. partial_backup-0.0.2/PKG-INFO +0 -121
  15. partial_backup-0.0.2/README.md +0 -98
  16. partial_backup-0.0.2/partial_backup/__main__.py +0 -28
  17. partial_backup-0.0.2/partial_backup/_version.py +0 -21
  18. partial_backup-0.0.2/partial_backup/backup.py +0 -288
  19. partial_backup-0.0.2/partial_backup.egg-info/PKG-INFO +0 -121
  20. {partial_backup-0.0.2 → partial_backup-0.1.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {partial_backup-0.0.2 → partial_backup-0.1.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  22. {partial_backup-0.0.2 → partial_backup-0.1.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {partial_backup-0.0.2 → partial_backup-0.1.1}/.github/pull_request_template.md +0 -0
  24. {partial_backup-0.0.2 → partial_backup-0.1.1}/.github/workflows/codeql.yml +0 -0
  25. {partial_backup-0.0.2 → partial_backup-0.1.1}/.github/workflows/markdown-lint.yml +0 -0
  26. {partial_backup-0.0.2 → partial_backup-0.1.1}/.gitignore +0 -0
  27. {partial_backup-0.0.2 → partial_backup-0.1.1}/.pre-commit-config.yaml +0 -0
  28. {partial_backup-0.0.2 → partial_backup-0.1.1}/CONTRIBUTING.md +0 -0
  29. {partial_backup-0.0.2 → partial_backup-0.1.1}/markdownlint-config.json +0 -0
  30. {partial_backup-0.0.2 → partial_backup-0.1.1}/partial_backup/__init__.py +0 -0
  31. {partial_backup-0.0.2 → partial_backup-0.1.1}/partial_backup/mbdb.py +0 -0
  32. {partial_backup-0.0.2 → partial_backup-0.1.1}/partial_backup.egg-info/dependency_links.txt +0 -0
  33. {partial_backup-0.0.2 → partial_backup-0.1.1}/partial_backup.egg-info/entry_points.txt +0 -0
  34. {partial_backup-0.0.2 → partial_backup-0.1.1}/partial_backup.egg-info/requires.txt +0 -0
  35. {partial_backup-0.0.2 → partial_backup-0.1.1}/partial_backup.egg-info/top_level.txt +0 -0
  36. {partial_backup-0.0.2 → partial_backup-0.1.1}/pyproject.toml +0 -0
  37. {partial_backup-0.0.2 → partial_backup-0.1.1}/requirements.txt +0 -0
  38. {partial_backup-0.0.2 → partial_backup-0.1.1}/setup.cfg +0 -0
@@ -5,7 +5,8 @@ on:
5
5
  branches: [ "master" ]
6
6
  paths:
7
7
  - '.github/workflows/python-app.yml'
8
- - 'pymobiledevice3/**'
8
+ - 'partial_backup/**'
9
+ - 'tests/**'
9
10
  - 'requirements.txt'
10
11
  - 'pyproject.toml'
11
12
  release:
@@ -43,3 +44,5 @@ jobs:
43
44
  python -m pip install -U '.[test]'
44
45
  - name: Test show usage
45
46
  run: partial-backup --help
47
+ - name: Run tests
48
+ run: python -m pytest tests
@@ -0,0 +1,29 @@
1
+ # This workflow builds and uploads the Python package to PyPI when a release is created
2
+
3
+ name: Upload Python Package
4
+
5
+ on:
6
+ release:
7
+ types: [ created ]
8
+
9
+ jobs:
10
+ deploy:
11
+ permissions:
12
+ id-token: write
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ # setuptools_scm needs full history + tags to derive the release version
18
+ # from the tag; a shallow clone yields a PyPI-rejected local version
19
+ # like 0.1.dev1+g<hash>.
20
+ fetch-depth: 0
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v7
23
+ - name: Build distributions
24
+ run: uv build
25
+ - name: Publish to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
27
+ with:
28
+ packages-dir: dist
29
+ verbose: true
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: partial-backup
3
+ Version: 0.1.1
4
+ Summary: Metadata-Aware Partial Backup Creator
5
+ Project-URL: Homepage, https://github.com/doronz88/partial-backup
6
+ Project-URL: Bug Reports, https://github.com/doronz88/partial-backup/issues
7
+ Keywords: ios,backup,partial-backup
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: rich
19
+ Requires-Dist: typer
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest; extra == "test"
22
+ Requires-Dist: pytest-asyncio; extra == "test"
23
+
24
+ # 🗂️ partial-backup: Metadata-Aware Partial Backup Creator
25
+
26
+ This utility scans a given directory in a structured *"unbacked"* format and generates a `Backup` object that can be
27
+ written back as a valid backup. It preserves directory and file metadata such as ownership (`user_`), group (`group_`),
28
+ and file mode (`mode_`), and skips common junk files.
29
+
30
+ ---
31
+
32
+ ## 📦 Features
33
+
34
+ - Recursively scans a directory organized by *domain* (e.g., `HomeDomain`)
35
+ - Supports embedded metadata in directory names (`user_501_group_0_mode_755`)
36
+ - Preserves and serializes:
37
+ - File contents
38
+ - Directory structure
39
+ - Ownership and permissions
40
+ - Skips system-specific noise like `.DS_Store` and `Thumbs.db`
41
+ - Outputs a fully reconstructable `Backup` object
42
+ - Simple CLI using [Typer](https://typer.tiangolo.com/)
43
+
44
+ ---
45
+
46
+ ## 🛠️ Installation
47
+
48
+ ```bash
49
+ pip install partial-backup
50
+ ```
51
+
52
+ ## 🧪 Example Usage
53
+
54
+ Assume your source directory is structured like this:
55
+
56
+ ```none
57
+ unback/HomeDomain/
58
+ └── Library
59
+ └── Preferences/
60
+ └── com.example.settings.plist
61
+ ```
62
+
63
+ To generate the backup:
64
+
65
+ ```shell
66
+ partial-backup unback/ output/
67
+ ```
68
+
69
+ It will:
70
+
71
+ - Detect `HomeDomain`
72
+ - Extract user/group/mode/protection class from directories like `user_501_group_0_mode_755`
73
+ - List every parent directory, including the domain root, so the device can create them
74
+ - Write an iOS 10+ backup (`Manifest.db`) into `output/partial-backup/`
75
+
76
+ ### Matching the device exactly
77
+
78
+ The device applies the owner, mode and data protection class listed in the backup, including to directories that
79
+ already exist. The built-in defaults follow a real iOS 27.2 backup, but for exact values take a small backup of the
80
+ same area from the target device and pass it as a reference:
81
+
82
+ ```shell
83
+ pymobiledevice3 backup2 backup bk/ --full --only sms
84
+ partial-backup unback/ output/ --reference bk/<UDID>
85
+ ```
86
+
87
+ Entries present in the reference keep its attributes (explicit metadata hints still win), and the device's own
88
+ `Info.plist`/`Manifest.plist` metadata and keybag are reused.
89
+
90
+ ## ♻️ Restore the Backup
91
+
92
+ The generated backup is compatible with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3), and can be
93
+ restored to an iOS device without a full erase. Installed apps stay installed:
94
+
95
+ ```shell
96
+ pymobiledevice3 backup2 restore output/ --source partial-backup --no-copy --system --no-remove --skip-apps
97
+ ```
98
+
99
+ > [!WARNING]
100
+ > A restore is not additive, even with `--no-remove`. On every restore the device replaces these paths with the
101
+ > backup's contents, deleting anything there the backup doesn't contain:
102
+ >
103
+ > - `HomeDomain`: `Library/Preferences`, `Library/AddressBook`, `Library/Calendar`, `Library/Notes`,
104
+ > `Library/SMS/sms.db`, `Library/Voicemail`, `Library/Cookies`, `Library/Safari/Bookmarks.db`,
105
+ > `Library/WebKit/Databases`, `Library/Caches/com.apple.WebAppCache`
106
+ > - `CameraRollDomain`: `Media/PhotoData` (the Photos library database)
107
+ > - every restored app container (`AppDomain-*`, `AppDomainGroup-*`, `AppDomainPlugin-*`), as a whole
108
+ >
109
+ > To change one of these on a device whose data matters, take a fresh backup of that device first and include its
110
+ > full current contents for each path, with your changes applied.
111
+
112
+ - `--system` is required unless `--reference` was used: `HomeDomain`, `MediaDomain` and the other domains in the
113
+ device's `MobileBackup.framework/Domains.plist` are "system domains", and the device skips them unless the host asks
114
+ for system files or the backup's `Lockdown.DeviceClass` matches the device.
115
+ - The device restores only what its `Domains.plist` allows. Paths outside a domain's rules (e.g. anything in
116
+ `MediaDomain` other than its `RelativePathsToBackupAndRestore`, such as `Media/Recordings` or
117
+ `Library/SMS/Attachments`) and some whole domains (e.g.
118
+ `SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles`) are skipped silently: the restore still
119
+ reports success.
120
+ - App containers (`AppDomain-<bundle id>`, `AppDomainGroup-<group id>`, `AppDomainPlugin-<bundle id>`) of installed
121
+ apps are restored too; their identifiers are listed in `Manifest.plist`'s `Applications` automatically, since the
122
+ device skips app domains that aren't listed there. A container of an app that isn't installed is set aside in
123
+ `/var/mobile/Library/SafeHarbor` instead.
124
+ - The device reboots when done and shows the post-restore Setup Assistant screens; backups are refused
125
+ (`MBErrorDomain/212`) until they are completed. `--no-reboot` doesn't avoid this, it only postpones it.
126
+ Developer Mode is turned off; re-enable it with `pymobiledevice3 amfi enable-developer-mode`.
127
+
128
+ For iOS 9 and older, `--legacy` writes the old `Manifest.mbdb` format directly into `output/`.
129
+
130
+ ## 📁 Metadata Syntax
131
+
132
+ This tool supports optional metadata hints in folder names:
133
+
134
+ | Syntax | Meaning |
135
+ |--------------------------------------|---------------------------------------|
136
+ | `user_501` | File owned by user `501` |
137
+ | `group_20` | File belongs to group `20` |
138
+ | `mode_755` | File or directory mode |
139
+ | `class_4` | Data protection class |
140
+ | `user_501_group_20_mode_644_class_3` | All of the above |
141
+
142
+ These metadata markers are automatically removed from paths and applied to the entries below them. Without hints,
143
+ files are `0644`, directories `0755`, files use protection class `3`, and the owner is `mobile` (`501`) for domains
144
+ under `/var/mobile` (`HomeDomain`, `AppDomain-*`, ...) and `root` otherwise.
145
+
146
+ ## ⛔ Ignored Files
147
+
148
+ The following files and folders are skipped automatically:
149
+
150
+ - `.DS_Store`
151
+ - `Thumbs.db`
152
+ - `desktop.ini`
153
+ - `.Spotlight-V100`
154
+ - `.Trashes`
155
+
156
+ ## API Overview
157
+
158
+ ```python
159
+ from pathlib import Path
160
+ from partial_backup.backup import Backup
161
+
162
+ backup = Backup.create_backup_from_directory(Path('/path/to/unback'))
163
+ backup.write_to_directory(Path('/path/to/output')) # -> /path/to/output/partial-backup
164
+ ```
165
+
166
+ ## 🙏 Acknowledgments
167
+
168
+ Huge thanks to [@JJTech0130](https://github.com/JJTech0130) for his insights and foundational work on the backup DB
169
+ implementation.
@@ -0,0 +1,146 @@
1
+ # 🗂️ partial-backup: Metadata-Aware Partial Backup Creator
2
+
3
+ This utility scans a given directory in a structured *"unbacked"* format and generates a `Backup` object that can be
4
+ written back as a valid backup. It preserves directory and file metadata such as ownership (`user_`), group (`group_`),
5
+ and file mode (`mode_`), and skips common junk files.
6
+
7
+ ---
8
+
9
+ ## 📦 Features
10
+
11
+ - Recursively scans a directory organized by *domain* (e.g., `HomeDomain`)
12
+ - Supports embedded metadata in directory names (`user_501_group_0_mode_755`)
13
+ - Preserves and serializes:
14
+ - File contents
15
+ - Directory structure
16
+ - Ownership and permissions
17
+ - Skips system-specific noise like `.DS_Store` and `Thumbs.db`
18
+ - Outputs a fully reconstructable `Backup` object
19
+ - Simple CLI using [Typer](https://typer.tiangolo.com/)
20
+
21
+ ---
22
+
23
+ ## 🛠️ Installation
24
+
25
+ ```bash
26
+ pip install partial-backup
27
+ ```
28
+
29
+ ## 🧪 Example Usage
30
+
31
+ Assume your source directory is structured like this:
32
+
33
+ ```none
34
+ unback/HomeDomain/
35
+ └── Library
36
+ └── Preferences/
37
+ └── com.example.settings.plist
38
+ ```
39
+
40
+ To generate the backup:
41
+
42
+ ```shell
43
+ partial-backup unback/ output/
44
+ ```
45
+
46
+ It will:
47
+
48
+ - Detect `HomeDomain`
49
+ - Extract user/group/mode/protection class from directories like `user_501_group_0_mode_755`
50
+ - List every parent directory, including the domain root, so the device can create them
51
+ - Write an iOS 10+ backup (`Manifest.db`) into `output/partial-backup/`
52
+
53
+ ### Matching the device exactly
54
+
55
+ The device applies the owner, mode and data protection class listed in the backup, including to directories that
56
+ already exist. The built-in defaults follow a real iOS 27.2 backup, but for exact values take a small backup of the
57
+ same area from the target device and pass it as a reference:
58
+
59
+ ```shell
60
+ pymobiledevice3 backup2 backup bk/ --full --only sms
61
+ partial-backup unback/ output/ --reference bk/<UDID>
62
+ ```
63
+
64
+ Entries present in the reference keep its attributes (explicit metadata hints still win), and the device's own
65
+ `Info.plist`/`Manifest.plist` metadata and keybag are reused.
66
+
67
+ ## ♻️ Restore the Backup
68
+
69
+ The generated backup is compatible with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3), and can be
70
+ restored to an iOS device without a full erase. Installed apps stay installed:
71
+
72
+ ```shell
73
+ pymobiledevice3 backup2 restore output/ --source partial-backup --no-copy --system --no-remove --skip-apps
74
+ ```
75
+
76
+ > [!WARNING]
77
+ > A restore is not additive, even with `--no-remove`. On every restore the device replaces these paths with the
78
+ > backup's contents, deleting anything there the backup doesn't contain:
79
+ >
80
+ > - `HomeDomain`: `Library/Preferences`, `Library/AddressBook`, `Library/Calendar`, `Library/Notes`,
81
+ > `Library/SMS/sms.db`, `Library/Voicemail`, `Library/Cookies`, `Library/Safari/Bookmarks.db`,
82
+ > `Library/WebKit/Databases`, `Library/Caches/com.apple.WebAppCache`
83
+ > - `CameraRollDomain`: `Media/PhotoData` (the Photos library database)
84
+ > - every restored app container (`AppDomain-*`, `AppDomainGroup-*`, `AppDomainPlugin-*`), as a whole
85
+ >
86
+ > To change one of these on a device whose data matters, take a fresh backup of that device first and include its
87
+ > full current contents for each path, with your changes applied.
88
+
89
+ - `--system` is required unless `--reference` was used: `HomeDomain`, `MediaDomain` and the other domains in the
90
+ device's `MobileBackup.framework/Domains.plist` are "system domains", and the device skips them unless the host asks
91
+ for system files or the backup's `Lockdown.DeviceClass` matches the device.
92
+ - The device restores only what its `Domains.plist` allows. Paths outside a domain's rules (e.g. anything in
93
+ `MediaDomain` other than its `RelativePathsToBackupAndRestore`, such as `Media/Recordings` or
94
+ `Library/SMS/Attachments`) and some whole domains (e.g.
95
+ `SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles`) are skipped silently: the restore still
96
+ reports success.
97
+ - App containers (`AppDomain-<bundle id>`, `AppDomainGroup-<group id>`, `AppDomainPlugin-<bundle id>`) of installed
98
+ apps are restored too; their identifiers are listed in `Manifest.plist`'s `Applications` automatically, since the
99
+ device skips app domains that aren't listed there. A container of an app that isn't installed is set aside in
100
+ `/var/mobile/Library/SafeHarbor` instead.
101
+ - The device reboots when done and shows the post-restore Setup Assistant screens; backups are refused
102
+ (`MBErrorDomain/212`) until they are completed. `--no-reboot` doesn't avoid this, it only postpones it.
103
+ Developer Mode is turned off; re-enable it with `pymobiledevice3 amfi enable-developer-mode`.
104
+
105
+ For iOS 9 and older, `--legacy` writes the old `Manifest.mbdb` format directly into `output/`.
106
+
107
+ ## 📁 Metadata Syntax
108
+
109
+ This tool supports optional metadata hints in folder names:
110
+
111
+ | Syntax | Meaning |
112
+ |--------------------------------------|---------------------------------------|
113
+ | `user_501` | File owned by user `501` |
114
+ | `group_20` | File belongs to group `20` |
115
+ | `mode_755` | File or directory mode |
116
+ | `class_4` | Data protection class |
117
+ | `user_501_group_20_mode_644_class_3` | All of the above |
118
+
119
+ These metadata markers are automatically removed from paths and applied to the entries below them. Without hints,
120
+ files are `0644`, directories `0755`, files use protection class `3`, and the owner is `mobile` (`501`) for domains
121
+ under `/var/mobile` (`HomeDomain`, `AppDomain-*`, ...) and `root` otherwise.
122
+
123
+ ## ⛔ Ignored Files
124
+
125
+ The following files and folders are skipped automatically:
126
+
127
+ - `.DS_Store`
128
+ - `Thumbs.db`
129
+ - `desktop.ini`
130
+ - `.Spotlight-V100`
131
+ - `.Trashes`
132
+
133
+ ## API Overview
134
+
135
+ ```python
136
+ from pathlib import Path
137
+ from partial_backup.backup import Backup
138
+
139
+ backup = Backup.create_backup_from_directory(Path('/path/to/unback'))
140
+ backup.write_to_directory(Path('/path/to/output')) # -> /path/to/output/partial-backup
141
+ ```
142
+
143
+ ## 🙏 Acknowledgments
144
+
145
+ Huge thanks to [@JJTech0130](https://github.com/JJTech0130) for his insights and foundational work on the backup DB
146
+ implementation.
@@ -0,0 +1,48 @@
1
+ from pathlib import Path
2
+ from typing import Annotated, Optional
3
+
4
+ import typer
5
+ from rich import print
6
+
7
+ from partial_backup.backup import DEFAULT_IDENTIFIER, Backup, ReferenceBackup
8
+
9
+
10
+ def create_backup_directory(
11
+ source: Path,
12
+ output: Path,
13
+ identifier: Annotated[str, typer.Option(
14
+ help="Name of the backup directory created under OUTPUT; pass it to `backup2 restore --source`.")
15
+ ] = DEFAULT_IDENTIFIER,
16
+ reference: Annotated[Optional[Path], typer.Option(
17
+ help="A real backup's <UDID> directory taken from the target device (e.g. "
18
+ "`pymobiledevice3 backup2 backup --only sms`). Its device metadata and the exact attributes of matching "
19
+ "entries are reused.")
20
+ ] = None,
21
+ legacy: Annotated[bool, typer.Option(
22
+ help="Write the legacy iOS <= 9 format (Manifest.mbdb) directly into OUTPUT.")
23
+ ] = False,
24
+ ) -> None:
25
+ """
26
+ Construct a backup directory from a given source directory given in an `unback` format
27
+ """
28
+ backup = Backup.create_backup_from_directory(source, ReferenceBackup.load(reference) if reference else None)
29
+ print(f"Constructing the following backup structure into: {output}")
30
+ print(backup.files)
31
+ if legacy:
32
+ backup.write_legacy_to_directory(output)
33
+ restore_source = "."
34
+ else:
35
+ backup.write_to_directory(output, identifier)
36
+ restore_source = identifier
37
+ print("[bold][green]Done![/green][/bold]")
38
+ print("[bold]Consider restoring this backup using:[/bold]")
39
+ print(f"[magenta]pymobiledevice3 backup2 restore {output} --source {restore_source} --no-copy --system "
40
+ f"--no-remove --skip-apps[/magenta]")
41
+
42
+
43
+ def cli() -> None:
44
+ typer.run(create_backup_directory)
45
+
46
+
47
+ if __name__ == "__main__":
48
+ cli()
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.1'
22
+ __version_tuple__ = version_tuple = (0, 1, 1)
23
+
24
+ __commit_id__ = commit_id = 'gfa08fb7c2'