---
title: "Restore files and folders"
description: "Unpacking file, folder, script and archive artifacts."
url: "https://saved.sh/docs/recover/restore-files"
---

Everything here is a standard archive or a plain file. Once decrypted, your operating
system's own tools open all of it.

## The short path [#the-short-path]

```bash
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 [#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 [#folders]

```bash
sctl restore folder <artifact-id> --path ./uploads.zip
unzip -l ./uploads.zip           # list, without extracting
unzip ./uploads.zip -d ./out     # extract
```

### What survived, and what did not [#what-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                   |

<Callout type="warn">
  **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](/docs/backups/sources/script) using
  `tar --numeric-owner --acls --xattrs` instead, which preserves what zip cannot.
</Callout>

```bash
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 [#pulling-one-file-out]

You rarely want the whole tree. Both archive formats support extracting a single member
without unpacking the rest.

```bash
# 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'
```

```powershell
# 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 .\out
```

<Callout>
  `Expand-Archive` extracts everything. For a single member, the .NET API above avoids
  unpacking a large archive to retrieve one file.
</Callout>

<Callout>
  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.
</Callout>

## Archives from `s3` and the drives [#archives-from-s3-and-the-drives]

```bash
sctl restore file <artifact-id> --path ./bucket.tar
tar -tvf ./bucket.tar | head          # inspect
tar -xf ./bucket.tar -C ./out         # extract
```

Entry 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:

```bash
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.

<Callout type="warn">
  `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.
</Callout>

## Script artifacts [#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.

```bash
# 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 < ./restored
```

A 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 [#single-files]

Nothing to unpack.

```bash
sctl restore file <artifact-id> --path ./state.db
```

Check it is what you expect before putting it back:

```bash
file ./state.db
sqlite3 ./state.db 'PRAGMA integrity_check;'
```

<Callout type="warn">
  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.
</Callout>

## Verifying before you trust it [#verifying-before-you-trust-it]

Two checks, both quick, both worth doing before overwriting anything.

```bash
# 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 ./restored
```

For 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](/docs/recover/artifact-format#verifying-the-checksum).

## Next [#next]

<Cards>
  <Card href="/docs/recover/artifact-format" title="Artifact format" description="Identifying an artifact from its bytes alone." />

  <Card href="/docs/recover/rehearsal" title="Rehearsal" description="Practising this before it matters." />
</Cards>
