LLM-generated GUI for exploring backups and restoring files

Today a friend wanted help to restore something with duplicacy.

But the restore experience in the Web UI is too clunky, so I decided to complain to my robot friend (Gemini 2.5 Pro) about it instead, and to ask it to create an UI app for me to compare file versions and restore, using duplicacy CLI. In 10 iterations of me asking for ever more increasing features (in the hindsight, I probably should have thought it through in the beginning, and produced complete requirements) it (he? she? we?) arrived to a very much usable prototype, that allows to:

  • see revisions and versions (you need to point it to initialized repository, it will read .duplicacy/preferences file)
  • Caches stuff locally, so you don’t have to wait on slow connections. On the other hand, duplicacy caches too, so I guess that wasn’t necessary.
  • browse the files in the tree for available snapshot
  • Search/filter files, with regex support
  • Restore selected files in-place or to some other folder
  • See all the existing versions of every file (Very few backup app vendors understand this, and I believe now only Synology of all the things implements this properly. Crashplan did too.)
  • preserve the state in the tree when switching the revisions (yay)
  • diff two revisions of the same file using available diff tool (I had bbdiff installed).

I have explained to the robot-friend how to use the clunky duplicacy CLI interface, especially the need to use temporary repository if restoring files not in-place, that one has to use snapshot IDs in some commands, but snapshot names in others, and how to find them in preferences file, etc, etc, but the result is magnificent. The whole ordeal took about 30 minutes; most of the time I was waiting for gemini to generate code and me typing the explanation of how to use duplicacy list and duplicacy restore commands, and providing with examples.

The robot-friend came up with the app name by the way, I did not specified it, I referred to it as “the app”.

The below demo is on a small test repository, because I did not want to expose my actual file names.

screenshot

screencast

https://www.youtube.com/watch?v=IJ_DZkkVjC0

generated code

Duplicacy Restore by Robot Friend · GitHub

To run: save that single file somewhere and uv run code.py

final screenshot from a robot-friend

Perhaps gchen can consider asking robot-friend to help implement the user-facing features in the Web Edition, especially the restore workflow, that users keep asking for? We have technology, there is no reason not to take advantage of it, to help deliver good experience.

2 Likes

Some dumb questions:

  1. I assume I need to install uv first, correct? Or would this work some other way, simply by having python installed (I have python, but not uv, on my Mac).
  2. Do I need to put code.py in any particular location?
  3. Will Duplicacy Restore ask me for the location of my .duplicacy folder, or other bits of info it needs to run? Or do I need to set other things up beforehand?
  1. You can use it just with python. Since you are on a Mac – create virtual environment first, because system python on macOS is not only quite old, but is a bit concussed:

    python3 -m venv .venv
    source .venv/bin/activate
    python3 code.py
    

    I would even install latest python from brew. But again, you can do all that much faster with uv: uv run --python 3.12 code.py will install requested python, create virtual environment, and run the code.

  2. Nope, put it anywhere you want. e.g. /tmp/code.py

  3. There is a button to browse to your repository, that is already initialized to be used by duplicacy CLI. It also need to be able to find duplicacy CLI of course… Put it somewhere in the path. I put mine /usr/local/bin/duplicacy.

OK!

I already had python 3.13.2 installed by homebrew (in /opt/homebrew/bin), so I just created a temporary folder, put code.py there, and then init'd a repo as a subdirectory of that.

Then python3 code.py was all I needed.

It worked totally smoothly – simple to use and very functional! I browsed files and snapshot revisions, restored a file to the desktop, etc. For me, this is already a better restore solution than either the CLI (where I’m truly not comfortable trying to restore single files or directories using include/exclude search rules – it just feels like flying blind in exactly the instance where I don’t want to be blind) or the web version of duplicacy.

The only real limitation to the current version of Duplicacy Restore is that as far as I can tell, it only works on unencrypted backups. @saspus Is that correct? (I tried to point it to an encrypted repo and it failed with a helpful error that said it could not decrypt.)

You shall be able to define any environment variables you need, and the child process will inherit them, including passwords: Managing Passwords · gilbertchen/duplicacy Wiki · GitHub

e.g.

DUPLICACY_PASSWORD='pa$$word' python3 code.py

Or you have have duplicacy store password in the keychain, or in the .duplciacy/preferences file. The script/app does not care, it just calls duplciacy.

It’s mind boggling what can be accomplished with 30 min investment of time today… And I looked through the code, it’s not the best nor is the worst, and it does not matter – it works. And you want to make improvement or add features – you can feed it back to LLM and ask for changes… But I too, just use this one as-is. It works.

Ah, my mistake: it seems that repo had a glitch in its preferences file. Fixed now, and can access the encrypted repo.

Though this did lead me to another issue: when I set up a small temp repo. Restore Sel. To worked fine. But now that I’m trying to browse a backup of my home folder (150 Gb in size, 141 revisions), I find that Restore Sel. To always fails with this error:

No differing files found to restore (1 skipped).

I get this no matter where I try to restore, even to a different hard drive, and regardless of whether I choose 1 or 2 file (in the latter case the message is “2 skipped”).

I tried this 47 different ways, and once I got a fuller error message:

File .zsh_history already exists. Please specify the -overwrite option to overwrite

I’m guessing the effort to create a temp folder and restore there is not working with this many files and this many revisions.

When I ran this in a very small repo init'd for testing, code.py created its own cache, duplicacy_ui_cache.json, but when I browse the repo** where the source is my home directory, I never see that cache created. Without it, I’m guessing it’s trying to restore first to the actual repo/source** and it can’t write there without the -overwrite flag.

**One thing that is fundamentally ambiguous (to me, at least) about the Duplicacy nomenclature is that the word “repo” is meant to designate both the source directory to be backed up, and the location of the .duplicacy directory. Of course, the way duplicacy actually works, those directories need not be the same. So I find it challenging to speak clearly above because for the backup to a local HD I’ve been running, they are not the same. I’ve told Duplicacy Restore to browse the directory where my .duplicacy folder is located, and it rightly reads my source files for backing up as my ~/ directory. But perhaps something goes awry when it’s trying to restore. I realize the code is trying to create the temp folder in /var/xx, but I find it odd that the .json cache file only contains info about the small number of test files I put in my original temp folder (it still works for that repo).

I really don’t want to debug this. But if you want, you can feed this code back to Gemini 2 Pro (at aistudio.google.com), give it description of the problem, give it logs (it outputs what it’s doing in the console) and ask it to fix the issue. That’s what I’ve been doing interactively until I arrived at working solution.

Absolutely agree. Repo, storage, backup, snapshot, snapshot ID, revision, version, it’s all a mess, and even different commands expect different things as input (some storage ID, some storage Name, etc)…

Maybe it confused your new repo with the old repo, due to this cache file being always in the current directory. You can try deleting it before trying a different repository.

I’ll try to mess with this later tonight, and If I can reproduce – I can ask the robot-friend to fix it :slight_smile:

To be clear: I absolutely didn’t mean to presume that you should have to debug this or rewrite it!!!

But I’ll take your suggestions above and play around some more. I didn’t move code.py, so I think you may be right about what’s going wrong.

I’ll report back if I learn anything useful. :smile:

1 Like

Quick update:

  1. Deleted the .json cache file and then ran Duplicacy Restore, again choosing the .duplicacy folder for the backup of my ~/ directory.
  2. It did not create a new .json cache file.
  3. It did repeat the same error (more useful error message below).
  4. Then I chose the small tmp folder as the repo.
  5. It still didn’t create a new .json file.
  6. It restored fine with the tmp.

Error message in terminal:

Created temp restore dir: /var/folders/fk/90w2cm6n695ddqjdthv1hp380000gn/T/dupli_restore_to_5t_tlab3

Copied preferences to /var/folders/fk/90w2cm6n695ddqjdthv1hp380000gn/T/dupli_restore_to_5t_tlab3/.duplicacy

CMD: duplicacy restore -storage local -r 141 -ignore-owner -- .zprofile in /var/folders/fk/90w2cm6n695ddqjdthv1hp380000gn/T/dupli_restore_to_5t_tlab3

I: Source path not found after restore, likely skipped by Duplicacy: /var/folders/fk/90w2cm6n695ddqjdthv1hp380000gn/T/dupli_restore_to_5t_tlab3/.zprofile

My two guesses at the cause of the problem:

  1. Backup too big for the temp folders to work.
  2. Problem created because I’m specifying the path to the source folder (the .duplicacy folder is in a different location).

This is what amounts to a lightbulb moment for me:

  • The restore process (in Duplicacy Restore) starts by copying the preferences file to the temp folder that’s meant to be the “repo” for the restore process.
  • If that preference file lists the repo as “” then this will work great, because it will be restoring to the temp folder.
  • But if that preferences file specifies a path (in my case ~/) then the process is going to be restoring to the original repo, and in order to write there it would need the -overwrite flag.

So our AI friend needs to edit the preference file when it copies it over to the temp folder, modifying the third line so that it always reads:

"repository": "",

After two more iterations

me:

Let’s make two modifications please.

  1. After copying the .duplicacy/preferences into a temporary directory, please set “repository” value to an empty string, if it is set to anything else but empty string: this is necessary to restore into the local temporary repository; otherwise duplicacy will attempt to to restore to the repository pointed to by the path contained in the value of “repository” key, which would defeat the purpose of creating temporary repository to restore the file into, as opposed to in-place. Same needs to be done for the Diff function.

  2. Please remember path to the last used repository, perhaps in the same cache file.

Robot-friend:

Okay, these are excellent refinements for robustness and usability. Let’s implement them.

  1. Modify Temporary Preferences: We’ll load the copied preferences JSON in the temporary restore directories, modify the “repository” key to “” if needed, and save it back before running duplicacy restore. This applies to both _restore_files_to_task and _diff_files_task.

  2. Remember Last Repository Path: We’ll add a top-level key “last_repo_path” to the cache JSON. We’ll load this on startup and save the current path on closing.

Here’s the updated code:

…

me:

It does remember the path – but when the app starts, it shall also load that repository, not just display it in the edit box :slight_smile:

Robot-friend:

You’re absolutely right! Just displaying the path isn’t very helpful if the user then immediately has to trigger the load manually. Let’s add the automatic loading step.

Modification:

In the init method, after setting the repo_path from the cache, add a call to self.load_repository() (potentially delayed slightly with root.after to let the UI draw first).

I shall learn how to talk with the robot-friend more effectively. He also rewrote a bunch of code I did not ask it to…

But that bug is fixed now, and nothing else seems to be broken: Revision3: Duplicacy Restore by Robot Friend · GitHub

Works like a charm!

It’s slow listing out all the revisions (I’ve got over 140), but it’s not like one needs to do this all the time. Repeating what I said above: I think this is a better restore workflow than currently available in the CLI or the web-gui. If I had to restore something right now, this is what I’d use.

2 Likes