Metadata-Version: 2.4
Name: proton-home-sync
Version: 0.1.1
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
Author: vality
License: MIT
Project-URL: Homepage, https://github.com/vality/proton-home-sync
Project-URL: Issues, https://github.com/vality/proton-home-sync/issues
Keywords: steam,proton,wine,saves,backup,symlink
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Filesystems
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: vdf>=3.4
Provides-Extra: daemon
Requires-Dist: watchfiles>=0.21; extra == "daemon"
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Requires-Dist: ruff>=0.4; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: watchfiles>=0.21; extra == "dev"
Dynamic: license-file

# proton-home-sync

Merge every Steam Proton prefix's home directory into a single shared
home, then replace each prefix's home with a symlink to it. Backup tools
that don't cooperate well with symlinks (most of them) can target one
real directory and get every game's saves.

```
~/.steam/steam/steamapps/compatdata/
├── 11111/pfx/drive_c/users/steamuser  ──►  ~/proton-shared-home/steamuser/
├── 22222/pfx/drive_c/users/steamuser  ──►  ~/proton-shared-home/steamuser/
└── 44444/pfx/drive_c/users/steamuser  ──►  ~/proton-shared-home/steamuser/
                                              └── (real directory — the only backup target)
```

The arrows go **prefix → shared home**. The shared home is one real
directory on disk. Once a prefix is linked, it stays linked — there is
no deletion tracking to worry about. When Steam creates a brand-new
prefix, the daemon picks it up, merges its initial Wine state into the
shared home, and turns the prefix's home into a symlink.

## Why this shape

* Most backup tools either de-duplicate symlinks to nothing or chase
  pointers back into your Steam library. With a real shared home, the
  tool walks one tree and sees real files.
* No deletion tracking. New prefixes just *add* their initial files to
  the shared home; after the symlink flip they write straight into it.
* One backup target: `~/proton-shared-home/steamuser/` (or wherever you
  point `--central-home`).

The cost is that all games now share one Windows user namespace, so two
games that use the same path under `%USERPROFILE%` would collide. In
practice most games namespace themselves via `%APPDATA%\<Studio>\<Game>\`,
`Documents/My Games/<Game>/`, or `Saved Games/<Game>/` and don't trip
on each other.

## Install

```bash
pipx install .                                # from a cloned checkout
# or directly from GitHub:
pipx install git+https://github.com/vality/proton-home-sync
# or
pip install --user .
# plus, for daemon mode:
pip install 'proton-home-sync[daemon]'
```

Requires Python ≥ 3.9 and the `vdf` package (pulled in automatically).

## Usage

```bash
# First run — merges every existing prefix into the shared home and links them.
proton-home-sync sync

# Use a custom shared home location.
proton-home-sync sync --central-home ~/backups/proton-shared-home

# See what would happen.
proton-home-sync sync --dry-run

# Show every prefix and its link state (✓ linked, · real dir, ↗ manual symlink).
proton-home-sync list

# Health summary — central home size + per-prefix link state.
proton-home-sync status

# Tear down the shared home (does NOT unlink the prefixes).
proton-home-sync clean

# Long-running watcher — auto-migrates new prefixes as games install.
proton-home-sync daemon
```

### Conflict policy

When a prefix's home contains a file that's already in the shared home,
the default is `--on-conflict skip` — the shared copy wins, the
prefix's copy is left behind (and effectively discarded because the
prefix's home is then replaced with a symlink). Pass
`--on-conflict overwrite` to make the prefix's copy win instead; this
is useful when re-migrating after a manual rollback.

## Hooking up a backup tool

```bash
# restic — encrypted, incremental
restic backup ~/proton-shared-home/steamuser

# borg — encrypted, deduplicated
borg create ~/backup::saves-$(date +%F) ~/proton-shared-home/steamuser

# rclone — to a cloud target
rclone sync ~/proton-shared-home/steamuser remote:proton-saves

# rsync — to a USB drive
rsync -a --delete ~/proton-shared-home/steamuser/ /mnt/usb/saves/
```

That's the entire backup story — one real directory, one command.

## Daemon mode

`proton-home-sync daemon` runs forever, watching each Steam library's
`compatdata/` for new prefixes. When one shows up (or Steam re-creates
an existing one), the daemon migrates it.

```bash
pip install 'proton-home-sync[daemon]'
proton-home-sync daemon
```

Useful flags:

```bash
# React faster to filesystem events.
proton-home-sync daemon --debounce-ms 500

# Less frequent full rescans (default every 5 minutes).
proton-home-sync daemon --rescan-seconds 900
```

The daemon logs to stderr. Run it under your service supervisor of
choice.

### systemd --user unit

```ini
# ~/.config/systemd/user/proton-home-sync.service
[Unit]
Description=Merge new Proton prefix home directories into the shared home
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/proton-home-sync daemon
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
```

```bash
systemctl --user enable --now proton-home-sync.service
```

## Configuration

Defaults live in `proton_home_sync/config.py`:

| Setting           | Default                                       | Override                          |
|-------------------|-----------------------------------------------|-----------------------------------|
| Steam roots       | `~/.steam/steam`, Flatpak variants            | `--steam-root`                    |
| Shared home       | `~/proton-shared-home/steamuser`              | `--central-home`                  |
| Wine user         | `steamuser`                                   | `--username`                      |
| Conflict policy   | `skip` (shared home wins)                     | `--on-conflict overwrite`         |

## Safety properties

* **Idempotent.** Re-running `sync` is a no-op once everything is linked.
* **Atomic flip.** When migrating, the prefix's real directory is
  renamed aside first, the symlink is created, and only then is the
  backup deleted. If anything fails midway, the prefix is restored.
* **Manual symlinks are respected.** A prefix whose home already
  symlinks somewhere else is left alone with a warning.
* **`clean` is paranoid.** Refuses `~`, `/`, symlinks, or non-directories.

## Development

[![CI status](.github/workflows/test.yml)](https://github.com/vality/proton-home-sync/actions/workflows/test.yml)

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e '.[daemon,test]'
pytest
```

The test suite builds a fake Steam install on disk (see
`tests/conftest.py`) — two libraries, three valid prefixes plus one
broken one — and exercises every code path.

## Project layout

```
proton-home-sync/
├── pyproject.toml
├── README.md
├── proton_home_sync/
│   ├── __init__.py
│   ├── __main__.py
│   ├── cli.py            # argparse + subcommand dispatch
│   ├── config.py         # default paths and tunables
│   ├── daemon.py         # long-running watcher (uses watchfiles)
│   ├── merge.py          # merge-then-symlink migration logic
│   ├── models.py         # ProtonPrefix, MigrationResult, MigrationReport
│   ├── prefix.py         # scan libraries for prefixes
│   ├── steam.py          # locate Steam root + libraryfolders.vdf parser
│   ├── utils.py          # human_path, helpers
│   └── linker.py         # deprecation shim → re-exports merge.py
└── tests/
    ├── conftest.py       # fake_steam fixture (two libraries, 3 valid prefixes)
    ├── test_steam.py
    ├── test_prefix.py
    ├── test_merge.py
    ├── test_linker.py    # deprecation marker
    ├── test_cli.py
    └── test_daemon.py
```

## License

MIT.
