Restore files and folders
Unpacking file, folder, script and archive artifacts.
View as MarkdownEverything here is a standard archive or a plain file. Once decrypted, your operating system's own tools open all of it.
The short path
sctl restore file <artifact-id> --path ./restored.zip--path is a file to write, never a directory to unpack into. The CLI downloads,
decrypts and decompresses, and writes the result there. Unpacking it is the step after, and
deliberately yours: extracting on your behalf would overwrite a tree you had not looked at.
sctl restore folder does exactly the same thing. The two subcommands exist to mirror the
source types, not because they behave differently.
What each type unpacks to
| Source | Artifact | Unpack with |
|---|---|---|
file | The file itself | Nothing to do |
folder | <folder>.zip | unzip |
script | Whatever your command wrote | Whatever wrote it |
s3 | <bucket>.tar | tar -xf |
web | The response body | Depends what it was |
Folders
sctl restore folder <artifact-id> --path ./uploads.zip
unzip -l ./uploads.zip # list, without extracting
unzip ./uploads.zip -d ./out # extractWhat survived, and what did not
| Preserved | Lost |
|---|---|
| Paths and directory structure | Ownership (uid/gid) |
| File contents, byte for byte | Unix permission bits |
| Symlinks, as links or as resolved targets | Modification and access times |
| Extended attributes, ACLs, SELinux labels | |
| Hard link relationships |
Restore does not give you back ownership, permissions or timestamps. Everything lands
owned by the user running unzip, with default modes and the extraction time as its mtime. For a system directory, fix them afterwards, or
back it up with a script source using
tar --numeric-owner --acls --xattrs instead, which preserves what zip cannot.
sudo chown -R app:app ./out
sudo find ./out -type d -exec chmod 755 {} \;
sudo find ./out -type f -exec chmod 644 {} \;Pulling one file out
You rarely want the whole tree. Both archive formats support extracting a single member without unpacking the rest.
# zip, Linux and macOS
unzip -l ./uploads.zip | grep invoice
unzip ./uploads.zip 'invoices/2026/march.pdf' -d ./out
# tar, all three platforms (Windows 10+ ships bsdtar as `tar`)
tar -tf ./bucket.tar | grep invoice
tar -xf ./bucket.tar -C ./out 'invoices/2026/march.pdf'# zip, Windows PowerShell
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [IO.Compression.ZipFile]::OpenRead((Resolve-Path .\uploads.zip))
$zip.Entries | Where-Object Name -match 'invoice' | Select-Object FullName, Length
$zip.Dispose()
# Whole archive
Expand-Archive .\uploads.zip -DestinationPath .\outExpand-Archive extracts everything. For a single member, the .NET API above avoids
unpacking a large archive to retrieve one file.
Entries in a folder artifact are stored without zip compression, because the
encryption step compresses instead. That makes single-file extraction fast: unzip seeks
straight to the member rather than inflating everything before it.
Archives from s3 and the drives
sctl restore file <artifact-id> --path ./bucket.tar
tar -tvf ./bucket.tar | head # inspect
tar -xf ./bucket.tar -C ./out # extractEntry names are the original object keys or drive paths, so the tree comes back with the same shape it had at the source.
Putting the objects back is deliberately your step:
aws s3 sync ./out s3://uploads/We do not restore into a bucket or a drive on your behalf. Writing several hundred thousand objects into live storage is not a thing a backup tool should do without you watching.
aws s3 sync does not delete by default, so objects created since the backup survive. If you
need the bucket to match the backup exactly, --delete does that, and it destroys anything
newer. Know which of the two you want before you run it.
Script artifacts
A script artifact is exactly what your command wrote to $SAVED_OUTPUT. We add no wrapper,
so unpacking it is whatever reverses what you did.
# If your script wrote a tar
tar -xf ./restored -C ./out
# If it wrote a mongodump archive
mongorestore --archive=./restored
# If it wrote plain SQL
mysql -u root -p app < ./restoredA script artifact is named after your script, with its extension removed:
/opt/saved/dump-everything.sh produces dump-everything, plus .gz and .gpg for whatever
layers were applied. The worker cannot know what your command wrote, so the name says which
script ran and nothing about the format.
That is the case where writing down what your script produces is worth the two minutes. Whoever runs the restore may not be whoever wrote the script, and may be doing it at 3am.
Single files
Nothing to unpack.
sctl restore file <artifact-id> --path ./state.dbCheck it is what you expect before putting it back:
file ./state.db
sqlite3 ./state.db 'PRAGMA integrity_check;'A file backup of a live SQLite database can be a torn read, and the integrity check is how
you find out. If it fails, the backup captured the file mid-write. Back it up with a script
source running sqlite3 db ".backup '$SAVED_OUTPUT'" instead, which takes a consistent
copy.
Verifying before you trust it
Two checks, both quick, both worth doing before overwriting anything.
# Does it contain what you expect?
unzip -l ./uploads.zip | wc -l
tar -tf ./bucket.tar | wc -l
# Does it match the recorded checksum?
sha256sum ./restoredFor a local backup the checksum matches the downloaded file directly. For cloud and manual backups it matches the file after decryption. See verifying the checksum.