proton-home-sync 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 (33) hide show
  1. proton_home_sync-0.1.1/LICENSE +21 -0
  2. proton_home_sync-0.1.1/PKG-INFO +248 -0
  3. proton_home_sync-0.1.1/README.md +216 -0
  4. proton_home_sync-0.1.1/proton_home_sync/__init__.py +8 -0
  5. proton_home_sync-0.1.1/proton_home_sync/__main__.py +10 -0
  6. proton_home_sync-0.1.1/proton_home_sync/cli.py +415 -0
  7. proton_home_sync-0.1.1/proton_home_sync/config.py +37 -0
  8. proton_home_sync-0.1.1/proton_home_sync/daemon.py +185 -0
  9. proton_home_sync-0.1.1/proton_home_sync/linker.py +27 -0
  10. proton_home_sync-0.1.1/proton_home_sync/merge.py +524 -0
  11. proton_home_sync-0.1.1/proton_home_sync/models.py +114 -0
  12. proton_home_sync-0.1.1/proton_home_sync/notifier.py +165 -0
  13. proton_home_sync-0.1.1/proton_home_sync/prefix.py +103 -0
  14. proton_home_sync-0.1.1/proton_home_sync/py.typed +0 -0
  15. proton_home_sync-0.1.1/proton_home_sync/steam.py +105 -0
  16. proton_home_sync-0.1.1/proton_home_sync/utils.py +16 -0
  17. proton_home_sync-0.1.1/proton_home_sync.egg-info/PKG-INFO +248 -0
  18. proton_home_sync-0.1.1/proton_home_sync.egg-info/SOURCES.txt +31 -0
  19. proton_home_sync-0.1.1/proton_home_sync.egg-info/dependency_links.txt +1 -0
  20. proton_home_sync-0.1.1/proton_home_sync.egg-info/entry_points.txt +2 -0
  21. proton_home_sync-0.1.1/proton_home_sync.egg-info/requires.txt +13 -0
  22. proton_home_sync-0.1.1/proton_home_sync.egg-info/top_level.txt +1 -0
  23. proton_home_sync-0.1.1/pyproject.toml +52 -0
  24. proton_home_sync-0.1.1/setup.cfg +4 -0
  25. proton_home_sync-0.1.1/tests/test_cli.py +397 -0
  26. proton_home_sync-0.1.1/tests/test_daemon.py +348 -0
  27. proton_home_sync-0.1.1/tests/test_linker.py +14 -0
  28. proton_home_sync-0.1.1/tests/test_merge.py +690 -0
  29. proton_home_sync-0.1.1/tests/test_models.py +31 -0
  30. proton_home_sync-0.1.1/tests/test_notifier.py +196 -0
  31. proton_home_sync-0.1.1/tests/test_prefix.py +149 -0
  32. proton_home_sync-0.1.1/tests/test_steam.py +125 -0
  33. proton_home_sync-0.1.1/tests/test_utils.py +29 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vality
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: proton-home-sync
3
+ Version: 0.1.1
4
+ Summary: Merge every Steam Proton prefix's home directory into one shared home and replace each prefix with a symlink, so backup tools see a single real directory of game saves
5
+ Author: vality
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/vality/proton-home-sync
8
+ Project-URL: Issues, https://github.com/vality/proton-home-sync/issues
9
+ Keywords: steam,proton,wine,saves,backup,symlink
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Filesystems
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: vdf>=3.4
22
+ Provides-Extra: daemon
23
+ Requires-Dist: watchfiles>=0.21; extra == "daemon"
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=7.0; extra == "test"
26
+ Requires-Dist: ruff>=0.4; extra == "test"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.4; extra == "dev"
30
+ Requires-Dist: watchfiles>=0.21; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # proton-home-sync
34
+
35
+ Merge every Steam Proton prefix's home directory into a single shared
36
+ home, then replace each prefix's home with a symlink to it. Backup tools
37
+ that don't cooperate well with symlinks (most of them) can target one
38
+ real directory and get every game's saves.
39
+
40
+ ```
41
+ ~/.steam/steam/steamapps/compatdata/
42
+ ├── 11111/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
43
+ ├── 22222/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
44
+ └── 44444/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
45
+ └── (real directory — the only backup target)
46
+ ```
47
+
48
+ The arrows go **prefix → shared home**. The shared home is one real
49
+ directory on disk. Once a prefix is linked, it stays linked — there is
50
+ no deletion tracking to worry about. When Steam creates a brand-new
51
+ prefix, the daemon picks it up, merges its initial Wine state into the
52
+ shared home, and turns the prefix's home into a symlink.
53
+
54
+ ## Why this shape
55
+
56
+ * Most backup tools either de-duplicate symlinks to nothing or chase
57
+ pointers back into your Steam library. With a real shared home, the
58
+ tool walks one tree and sees real files.
59
+ * No deletion tracking. New prefixes just *add* their initial files to
60
+ the shared home; after the symlink flip they write straight into it.
61
+ * One backup target: `~/proton-shared-home/steamuser/` (or wherever you
62
+ point `--central-home`).
63
+
64
+ The cost is that all games now share one Windows user namespace, so two
65
+ games that use the same path under `%USERPROFILE%` would collide. In
66
+ practice most games namespace themselves via `%APPDATA%\<Studio>\<Game>\`,
67
+ `Documents/My Games/<Game>/`, or `Saved Games/<Game>/` and don't trip
68
+ on each other.
69
+
70
+ ## Install
71
+
72
+ ```bash
73
+ pipx install . # from a cloned checkout
74
+ # or directly from GitHub:
75
+ pipx install git+https://github.com/vality/proton-home-sync
76
+ # or
77
+ pip install --user .
78
+ # plus, for daemon mode:
79
+ pip install 'proton-home-sync[daemon]'
80
+ ```
81
+
82
+ Requires Python ≥ 3.9 and the `vdf` package (pulled in automatically).
83
+
84
+ ## Usage
85
+
86
+ ```bash
87
+ # First run — merges every existing prefix into the shared home and links them.
88
+ proton-home-sync sync
89
+
90
+ # Use a custom shared home location.
91
+ proton-home-sync sync --central-home ~/backups/proton-shared-home
92
+
93
+ # See what would happen.
94
+ proton-home-sync sync --dry-run
95
+
96
+ # Show every prefix and its link state (✓ linked, · real dir, ↗ manual symlink).
97
+ proton-home-sync list
98
+
99
+ # Health summary — central home size + per-prefix link state.
100
+ proton-home-sync status
101
+
102
+ # Tear down the shared home (does NOT unlink the prefixes).
103
+ proton-home-sync clean
104
+
105
+ # Long-running watcher — auto-migrates new prefixes as games install.
106
+ proton-home-sync daemon
107
+ ```
108
+
109
+ ### Conflict policy
110
+
111
+ When a prefix's home contains a file that's already in the shared home,
112
+ the default is `--on-conflict skip` — the shared copy wins, the
113
+ prefix's copy is left behind (and effectively discarded because the
114
+ prefix's home is then replaced with a symlink). Pass
115
+ `--on-conflict overwrite` to make the prefix's copy win instead; this
116
+ is useful when re-migrating after a manual rollback.
117
+
118
+ ## Hooking up a backup tool
119
+
120
+ ```bash
121
+ # restic — encrypted, incremental
122
+ restic backup ~/proton-shared-home/steamuser
123
+
124
+ # borg — encrypted, deduplicated
125
+ borg create ~/backup::saves-$(date +%F) ~/proton-shared-home/steamuser
126
+
127
+ # rclone — to a cloud target
128
+ rclone sync ~/proton-shared-home/steamuser remote:proton-saves
129
+
130
+ # rsync — to a USB drive
131
+ rsync -a --delete ~/proton-shared-home/steamuser/ /mnt/usb/saves/
132
+ ```
133
+
134
+ That's the entire backup story — one real directory, one command.
135
+
136
+ ## Daemon mode
137
+
138
+ `proton-home-sync daemon` runs forever, watching each Steam library's
139
+ `compatdata/` for new prefixes. When one shows up (or Steam re-creates
140
+ an existing one), the daemon migrates it.
141
+
142
+ ```bash
143
+ pip install 'proton-home-sync[daemon]'
144
+ proton-home-sync daemon
145
+ ```
146
+
147
+ Useful flags:
148
+
149
+ ```bash
150
+ # React faster to filesystem events.
151
+ proton-home-sync daemon --debounce-ms 500
152
+
153
+ # Less frequent full rescans (default every 5 minutes).
154
+ proton-home-sync daemon --rescan-seconds 900
155
+ ```
156
+
157
+ The daemon logs to stderr. Run it under your service supervisor of
158
+ choice.
159
+
160
+ ### systemd --user unit
161
+
162
+ ```ini
163
+ # ~/.config/systemd/user/proton-home-sync.service
164
+ [Unit]
165
+ Description=Merge new Proton prefix home directories into the shared home
166
+ After=network-online.target
167
+
168
+ [Service]
169
+ Type=simple
170
+ ExecStart=/usr/bin/proton-home-sync daemon
171
+ Restart=on-failure
172
+ RestartSec=5
173
+
174
+ [Install]
175
+ WantedBy=default.target
176
+ ```
177
+
178
+ ```bash
179
+ systemctl --user enable --now proton-home-sync.service
180
+ ```
181
+
182
+ ## Configuration
183
+
184
+ Defaults live in `proton_home_sync/config.py`:
185
+
186
+ | Setting | Default | Override |
187
+ |-------------------|-----------------------------------------------|-----------------------------------|
188
+ | Steam roots | `~/.steam/steam`, Flatpak variants | `--steam-root` |
189
+ | Shared home | `~/proton-shared-home/steamuser` | `--central-home` |
190
+ | Wine user | `steamuser` | `--username` |
191
+ | Conflict policy | `skip` (shared home wins) | `--on-conflict overwrite` |
192
+
193
+ ## Safety properties
194
+
195
+ * **Idempotent.** Re-running `sync` is a no-op once everything is linked.
196
+ * **Atomic flip.** When migrating, the prefix's real directory is
197
+ renamed aside first, the symlink is created, and only then is the
198
+ backup deleted. If anything fails midway, the prefix is restored.
199
+ * **Manual symlinks are respected.** A prefix whose home already
200
+ symlinks somewhere else is left alone with a warning.
201
+ * **`clean` is paranoid.** Refuses `~`, `/`, symlinks, or non-directories.
202
+
203
+ ## Development
204
+
205
+ [![CI status](.github/workflows/test.yml)](https://github.com/vality/proton-home-sync/actions/workflows/test.yml)
206
+
207
+ ```bash
208
+ python -m venv .venv
209
+ source .venv/bin/activate
210
+ pip install -e '.[daemon,test]'
211
+ pytest
212
+ ```
213
+
214
+ The test suite builds a fake Steam install on disk (see
215
+ `tests/conftest.py`) — two libraries, three valid prefixes plus one
216
+ broken one — and exercises every code path.
217
+
218
+ ## Project layout
219
+
220
+ ```
221
+ proton-home-sync/
222
+ ├── pyproject.toml
223
+ ├── README.md
224
+ ├── proton_home_sync/
225
+ │ ├── __init__.py
226
+ │ ├── __main__.py
227
+ │ ├── cli.py # argparse + subcommand dispatch
228
+ │ ├── config.py # default paths and tunables
229
+ │ ├── daemon.py # long-running watcher (uses watchfiles)
230
+ │ ├── merge.py # merge-then-symlink migration logic
231
+ │ ├── models.py # ProtonPrefix, MigrationResult, MigrationReport
232
+ │ ├── prefix.py # scan libraries for prefixes
233
+ │ ├── steam.py # locate Steam root + libraryfolders.vdf parser
234
+ │ ├── utils.py # human_path, helpers
235
+ │ └── linker.py # deprecation shim → re-exports merge.py
236
+ └── tests/
237
+ ├── conftest.py # fake_steam fixture (two libraries, 3 valid prefixes)
238
+ ├── test_steam.py
239
+ ├── test_prefix.py
240
+ ├── test_merge.py
241
+ ├── test_linker.py # deprecation marker
242
+ ├── test_cli.py
243
+ └── test_daemon.py
244
+ ```
245
+
246
+ ## License
247
+
248
+ MIT.
@@ -0,0 +1,216 @@
1
+ # proton-home-sync
2
+
3
+ Merge every Steam Proton prefix's home directory into a single shared
4
+ home, then replace each prefix's home with a symlink to it. Backup tools
5
+ that don't cooperate well with symlinks (most of them) can target one
6
+ real directory and get every game's saves.
7
+
8
+ ```
9
+ ~/.steam/steam/steamapps/compatdata/
10
+ ├── 11111/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
11
+ ├── 22222/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
12
+ └── 44444/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
13
+ └── (real directory — the only backup target)
14
+ ```
15
+
16
+ The arrows go **prefix → shared home**. The shared home is one real
17
+ directory on disk. Once a prefix is linked, it stays linked — there is
18
+ no deletion tracking to worry about. When Steam creates a brand-new
19
+ prefix, the daemon picks it up, merges its initial Wine state into the
20
+ shared home, and turns the prefix's home into a symlink.
21
+
22
+ ## Why this shape
23
+
24
+ * Most backup tools either de-duplicate symlinks to nothing or chase
25
+ pointers back into your Steam library. With a real shared home, the
26
+ tool walks one tree and sees real files.
27
+ * No deletion tracking. New prefixes just *add* their initial files to
28
+ the shared home; after the symlink flip they write straight into it.
29
+ * One backup target: `~/proton-shared-home/steamuser/` (or wherever you
30
+ point `--central-home`).
31
+
32
+ The cost is that all games now share one Windows user namespace, so two
33
+ games that use the same path under `%USERPROFILE%` would collide. In
34
+ practice most games namespace themselves via `%APPDATA%\<Studio>\<Game>\`,
35
+ `Documents/My Games/<Game>/`, or `Saved Games/<Game>/` and don't trip
36
+ on each other.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pipx install . # from a cloned checkout
42
+ # or directly from GitHub:
43
+ pipx install git+https://github.com/vality/proton-home-sync
44
+ # or
45
+ pip install --user .
46
+ # plus, for daemon mode:
47
+ pip install 'proton-home-sync[daemon]'
48
+ ```
49
+
50
+ Requires Python ≥ 3.9 and the `vdf` package (pulled in automatically).
51
+
52
+ ## Usage
53
+
54
+ ```bash
55
+ # First run — merges every existing prefix into the shared home and links them.
56
+ proton-home-sync sync
57
+
58
+ # Use a custom shared home location.
59
+ proton-home-sync sync --central-home ~/backups/proton-shared-home
60
+
61
+ # See what would happen.
62
+ proton-home-sync sync --dry-run
63
+
64
+ # Show every prefix and its link state (✓ linked, · real dir, ↗ manual symlink).
65
+ proton-home-sync list
66
+
67
+ # Health summary — central home size + per-prefix link state.
68
+ proton-home-sync status
69
+
70
+ # Tear down the shared home (does NOT unlink the prefixes).
71
+ proton-home-sync clean
72
+
73
+ # Long-running watcher — auto-migrates new prefixes as games install.
74
+ proton-home-sync daemon
75
+ ```
76
+
77
+ ### Conflict policy
78
+
79
+ When a prefix's home contains a file that's already in the shared home,
80
+ the default is `--on-conflict skip` — the shared copy wins, the
81
+ prefix's copy is left behind (and effectively discarded because the
82
+ prefix's home is then replaced with a symlink). Pass
83
+ `--on-conflict overwrite` to make the prefix's copy win instead; this
84
+ is useful when re-migrating after a manual rollback.
85
+
86
+ ## Hooking up a backup tool
87
+
88
+ ```bash
89
+ # restic — encrypted, incremental
90
+ restic backup ~/proton-shared-home/steamuser
91
+
92
+ # borg — encrypted, deduplicated
93
+ borg create ~/backup::saves-$(date +%F) ~/proton-shared-home/steamuser
94
+
95
+ # rclone — to a cloud target
96
+ rclone sync ~/proton-shared-home/steamuser remote:proton-saves
97
+
98
+ # rsync — to a USB drive
99
+ rsync -a --delete ~/proton-shared-home/steamuser/ /mnt/usb/saves/
100
+ ```
101
+
102
+ That's the entire backup story — one real directory, one command.
103
+
104
+ ## Daemon mode
105
+
106
+ `proton-home-sync daemon` runs forever, watching each Steam library's
107
+ `compatdata/` for new prefixes. When one shows up (or Steam re-creates
108
+ an existing one), the daemon migrates it.
109
+
110
+ ```bash
111
+ pip install 'proton-home-sync[daemon]'
112
+ proton-home-sync daemon
113
+ ```
114
+
115
+ Useful flags:
116
+
117
+ ```bash
118
+ # React faster to filesystem events.
119
+ proton-home-sync daemon --debounce-ms 500
120
+
121
+ # Less frequent full rescans (default every 5 minutes).
122
+ proton-home-sync daemon --rescan-seconds 900
123
+ ```
124
+
125
+ The daemon logs to stderr. Run it under your service supervisor of
126
+ choice.
127
+
128
+ ### systemd --user unit
129
+
130
+ ```ini
131
+ # ~/.config/systemd/user/proton-home-sync.service
132
+ [Unit]
133
+ Description=Merge new Proton prefix home directories into the shared home
134
+ After=network-online.target
135
+
136
+ [Service]
137
+ Type=simple
138
+ ExecStart=/usr/bin/proton-home-sync daemon
139
+ Restart=on-failure
140
+ RestartSec=5
141
+
142
+ [Install]
143
+ WantedBy=default.target
144
+ ```
145
+
146
+ ```bash
147
+ systemctl --user enable --now proton-home-sync.service
148
+ ```
149
+
150
+ ## Configuration
151
+
152
+ Defaults live in `proton_home_sync/config.py`:
153
+
154
+ | Setting | Default | Override |
155
+ |-------------------|-----------------------------------------------|-----------------------------------|
156
+ | Steam roots | `~/.steam/steam`, Flatpak variants | `--steam-root` |
157
+ | Shared home | `~/proton-shared-home/steamuser` | `--central-home` |
158
+ | Wine user | `steamuser` | `--username` |
159
+ | Conflict policy | `skip` (shared home wins) | `--on-conflict overwrite` |
160
+
161
+ ## Safety properties
162
+
163
+ * **Idempotent.** Re-running `sync` is a no-op once everything is linked.
164
+ * **Atomic flip.** When migrating, the prefix's real directory is
165
+ renamed aside first, the symlink is created, and only then is the
166
+ backup deleted. If anything fails midway, the prefix is restored.
167
+ * **Manual symlinks are respected.** A prefix whose home already
168
+ symlinks somewhere else is left alone with a warning.
169
+ * **`clean` is paranoid.** Refuses `~`, `/`, symlinks, or non-directories.
170
+
171
+ ## Development
172
+
173
+ [![CI status](.github/workflows/test.yml)](https://github.com/vality/proton-home-sync/actions/workflows/test.yml)
174
+
175
+ ```bash
176
+ python -m venv .venv
177
+ source .venv/bin/activate
178
+ pip install -e '.[daemon,test]'
179
+ pytest
180
+ ```
181
+
182
+ The test suite builds a fake Steam install on disk (see
183
+ `tests/conftest.py`) — two libraries, three valid prefixes plus one
184
+ broken one — and exercises every code path.
185
+
186
+ ## Project layout
187
+
188
+ ```
189
+ proton-home-sync/
190
+ ├── pyproject.toml
191
+ ├── README.md
192
+ ├── proton_home_sync/
193
+ │ ├── __init__.py
194
+ │ ├── __main__.py
195
+ │ ├── cli.py # argparse + subcommand dispatch
196
+ │ ├── config.py # default paths and tunables
197
+ │ ├── daemon.py # long-running watcher (uses watchfiles)
198
+ │ ├── merge.py # merge-then-symlink migration logic
199
+ │ ├── models.py # ProtonPrefix, MigrationResult, MigrationReport
200
+ │ ├── prefix.py # scan libraries for prefixes
201
+ │ ├── steam.py # locate Steam root + libraryfolders.vdf parser
202
+ │ ├── utils.py # human_path, helpers
203
+ │ └── linker.py # deprecation shim → re-exports merge.py
204
+ └── tests/
205
+ ├── conftest.py # fake_steam fixture (two libraries, 3 valid prefixes)
206
+ ├── test_steam.py
207
+ ├── test_prefix.py
208
+ ├── test_merge.py
209
+ ├── test_linker.py # deprecation marker
210
+ ├── test_cli.py
211
+ └── test_daemon.py
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT.
@@ -0,0 +1,8 @@
1
+ """proton-home-sync — sync Proton prefix home directories into one place."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .notifier import notify
6
+
7
+ __version__ = "0.1.1"
8
+ __all__ = ["__version__", "notify"]
@@ -0,0 +1,10 @@
1
+ """Allow ``python -m proton_home_sync`` invocation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())