diff --git a/AGENTS.md b/AGENTS.md index 00e7efb..ec51064 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -341,6 +341,7 @@ Pacotes de sessao e desktop usados diretamente pelos configs: - `slurp` - `wl-clipboard` - `networkmanager` +- `ludusavi` - `hyprpicker` - `papirus-icon-theme` - `mpv` @@ -574,6 +575,31 @@ Estado atual: - aponta o provider Ollama para `http://127.0.0.1:11434` - usa modelo `ollama/gemma4:e4b` +### Backups locais de saves de jogos + +Fonte principal: + +- `home/ltadeu6.nix` + +Estado atual: + +- O Home Manager instala `ludusavi`. +- O timer de usuario `game-save-ludusavi-backup.timer` roda de hora em hora. +- O servico `game-save-ludusavi-backup.service` executa Ludusavi em modo CLI. +- Os backups locais ficam em `~/Backups/game-saves/ludusavi`. +- O formato usado e `simple`, com retencao de 14 backups completos e 14 diferenciais por jogo. +- Para jogos do Lutris no prefix `~/Games/none`, o servico tambem cria snapshots locais de `~/Games/none/drive_c/users/steamuser`. +- Esses snapshots ficam em `~/Backups/game-saves/lutris-wine-userdata/snapshots`, com `latest` apontando para o snapshot mais recente. +- Os snapshots do Lutris usam hardlinks via `rsync --link-dest` e mantem os 14 mais recentes. + +Cuidados: + +- Este fluxo e apenas local; nao ha Restic, OCI ou outro destino remoto para saves de jogos no estado atual. +- Se adicionar backup remoto depois, mantenha segredos fora do repo e prefira um repo separado dos backups do VPS. +- Nao faca backup bruto de `~/.local/share/Steam/steamapps`; prefira Ludusavi e excecoes pontuais para jogos nao detectados. +- Nao use `--wine-prefix ~/Games/none` em uma varredura geral do Ludusavi sem nomes de jogos; isso gera muitos falsos positivos por causa dos arquivos de registro do prefix. +- Se adicionar jogos novos ao Lutris usando o mesmo prefix `~/Games/none`, eles entram automaticamente no snapshot local do Lutris, mesmo que o Ludusavi nao conheca o jogo. + ## Secrets e variaveis de ambiente ### Segredos versionados hoje diff --git a/home/ltadeu6.nix b/home/ltadeu6.nix index c0a472e..fbb294d 100644 --- a/home/ltadeu6.nix +++ b/home/ltadeu6.nix @@ -1,6 +1,57 @@ { config, pkgs, lib, ... }: let + gameSaveBackupRoot = "${config.home.homeDirectory}/Backups/game-saves"; + gameSaveLudusaviPath = "${gameSaveBackupRoot}/ludusavi"; + gameSaveLutrisWinePrefix = "${config.home.homeDirectory}/Games/none"; + gameSaveLutrisUserPath = "${gameSaveLutrisWinePrefix}/drive_c/users/steamuser"; + gameSaveLutrisBackupPath = "${gameSaveBackupRoot}/lutris-wine-userdata"; + gameSaveLudusaviBackup = pkgs.writeShellScript "game-save-ludusavi-backup" '' + set -eu + + ${pkgs.coreutils}/bin/mkdir -p ${lib.escapeShellArg gameSaveLudusaviPath} + + ${pkgs.ludusavi}/bin/ludusavi \ + --try-manifest-update \ + backup \ + --path ${lib.escapeShellArg gameSaveLudusaviPath} \ + --format simple \ + --full-limit 14 \ + --differential-limit 14 \ + --force + + if [ ! -d ${lib.escapeShellArg gameSaveLutrisUserPath} ]; then + echo "Skipping Lutris Wine user-data snapshot: missing ${gameSaveLutrisUserPath}" >&2 + exit 0 + fi + + snapshot_root=${lib.escapeShellArg "${gameSaveLutrisBackupPath}/snapshots"} + snapshot="$snapshot_root/backup-$(${pkgs.coreutils}/bin/date -u +%Y%m%dT%H%M%SZ)" + latest_link=${lib.escapeShellArg "${gameSaveLutrisBackupPath}/latest"} + link_dest_args=() + + ${pkgs.coreutils}/bin/mkdir -p "$snapshot_root" + + if [ -L "$latest_link" ]; then + latest_target="$(${pkgs.coreutils}/bin/readlink -f "$latest_link" || true)" + if [ -n "$latest_target" ] && [ -d "$latest_target" ]; then + link_dest_args=(--link-dest "$latest_target") + fi + fi + + ${pkgs.rsync}/bin/rsync -a --delete \ + "''${link_dest_args[@]}" \ + ${lib.escapeShellArg "${gameSaveLutrisUserPath}/"} \ + "$snapshot/" + ${pkgs.coreutils}/bin/ln -sfn "$snapshot" "$latest_link" + + ${pkgs.findutils}/bin/find "$snapshot_root" -mindepth 1 -maxdepth 1 -type d \ + -printf '%T@ %p\n' \ + | ${pkgs.coreutils}/bin/sort -rn \ + | ${pkgs.coreutils}/bin/tail -n +15 \ + | ${pkgs.gnused}/bin/sed 's/^[^ ]* //' \ + | ${pkgs.findutils}/bin/xargs -r ${pkgs.coreutils}/bin/rm -rf + ''; opencodeWithLibstdcxx = pkgs.symlinkJoin { name = "opencode"; paths = [ pkgs.unstable.opencode ]; @@ -247,6 +298,7 @@ in man hyprpicker networkmanager + ludusavi prismlauncher gimp inkscape @@ -375,6 +427,24 @@ in ".config/wofi/menu.css".source = ../configs/wofi/menu.css; }; + systemd.user.services.game-save-ludusavi-backup = { + Unit.Description = "Back up game saves with Ludusavi"; + Service = { + Type = "oneshot"; + ExecStart = "${gameSaveLudusaviBackup}"; + }; + }; + + systemd.user.timers.game-save-ludusavi-backup = { + Unit.Description = "Run Ludusavi game-save backup"; + Timer = { + OnCalendar = "hourly"; + Persistent = true; + RandomizedDelaySec = "10min"; + }; + Install.WantedBy = [ "timers.target" ]; + }; + home.activation.ociCredentials = lib.hm.dag.entryAfter [ "writeBoundary" ] '' set -euo pipefail key_file="/run/agenix/oci_key"