---
title: "Files and folders"
description: "Your own filesystem, taken by your own worker."
url: "https://saved.sh/docs/backups/sources/files"
---

Two source types read the disk of the machine your worker runs on.

| Type     | Takes            | Produces                     |
| -------- | ---------------- | ---------------------------- |
| `file`   | One regular file | The file, under its own name |
| `folder` | A directory tree | One zip                      |

**Local only, permanently.** There is nothing for our cloud to connect to: your filesystem
does not exist there. The workflows are not registered on our side, so no configuration can
route one to us.

## `file` [#file]

| Key               | Required | Notes                                |
| ----------------- | -------- | ------------------------------------ |
| `path`            | Yes      | Absolute path to a regular file      |
| `follow_symlinks` | No       | `true` to back up a symlink's target |

```yaml title="config.yaml"
backups:
  "<backup-uuid>":
    source_type: file
    source:
      path: /var/lib/app/state.db
```

The artifact keeps the file's base name. With `follow_symlinks` set, that is the **target's**
name, not the link's: a link `current.db` pointing at `db-2026-08.sqlite` produces an artifact
called `db-2026-08.sqlite`.

Anything that is not a regular file is refused with an explanation rather than silently
producing something odd:

| Path is                              | Result                                      |
| ------------------------------------ | ------------------------------------------- |
| A symlink, without `follow_symlinks` | Refused. Set the flag to back up its target |
| A directory                          | Refused, telling you to use `folder`        |
| A socket, device or FIFO             | Refused. There is nothing to copy           |

<Callout type="warn">
  A `file` backup copies the bytes as they are at that moment. If the file is being written
  while the copy runs, you get a torn read. For a SQLite database, back it up with a
  [`script` source](/docs/backups/sources/script) running `sqlite3 db ".backup"` rather than
  copying the file underneath a live writer.
</Callout>

## `folder` [#folder]

| Key               | Required | Notes                                           |
| ----------------- | -------- | ----------------------------------------------- |
| `path`            | Yes      | Absolute path to a directory                    |
| `include`         | No       | A list of globs. Empty means every regular file |
| `exclude`         | No       | A list of globs. Applied before `include`       |
| `follow_symlinks` | No       | `true` resolves links instead of storing them   |

```yaml title="config.yaml"
backups:
  "<backup-uuid>":
    source_type: folder
    source:
      path: /srv/uploads
      exclude: ["*.tmp", "node_modules", ".git", "*.log"]
```

The artifact is `<folder-name>.zip`.

### Patterns [#patterns]

A pattern matches if it matches **any** of these:

* the full path relative to the root (`docs/notes/todo.md`)
* the base name (`todo.md`)
* any single path segment (`notes`)

That is why `node_modules` excludes the directory wherever it appears, at any depth, without
needing `**/node_modules/**`.

| Pattern        | Matches                                    |
| -------------- | ------------------------------------------ |
| `*.tmp`        | Any file ending `.tmp`, at any depth       |
| `node_modules` | Any directory or file with that exact name |
| `cache/*`      | Direct children of a top-level `cache`     |

**Exclude is evaluated first**, and excluding a directory skips its whole subtree, so an
`include` cannot pull a file back out of an excluded directory. Order the two accordingly.

<Callout type="warn">
  **`include` filters regular files only.** Directory entries and symlinks are archived
  whatever it says, so a zip built with `include: ["*.pdf"]` still contains the tree's
  directory structure and its links. `exclude` is the one that applies to everything, and it
  is the right tool for leaving something out.
</Callout>

Excluding aggressively is worth the effort. Build output, dependency directories and logs are
usually the majority of the bytes and none of the value.

### Symlinks [#symlinks]

| `follow_symlinks` | Behaviour                                                  |
| ----------------- | ---------------------------------------------------------- |
| Unset             | Links are stored **as links**, recording their target text |
| `true`            | Links are resolved and their targets archived in place     |

With following on, each resolved target is archived **once**, so a link cycle cannot produce
an infinite archive. Two links to the same file mean one copy, under the first name reached.

### What survives the round trip [#what-survives-the-round-trip]

| Preserved                        | Not preserved                             |
| -------------------------------- | ----------------------------------------- |
| Paths and directory structure    | Ownership (uid/gid)                       |
| File contents                    | Unix permission bits                      |
| Symlinks, as links or as targets | Modification and access times             |
|                                  | Extended attributes, ACLs, SELinux labels |
|                                  | Hard link relationships                   |

<Callout type="warn">
  If ownership, permissions or timestamps matter, do not use `folder`. Use a
  [`script` source](/docs/backups/sources/script) running `tar` with the flags you need:
  `tar --numeric-owner --acls --xattrs -czf "$SAVED_OUTPUT" -C /srv app`. Tar preserves what
  zip cannot, and you keep control of exactly which metadata is captured.
</Callout>

Entries are stored **without zip compression**, because the encryption step compresses
anyway and doing it twice costs CPU for nothing. The `.zip` you download is a container, not
a compressed archive.

## Consistency [#consistency]

Neither type snapshots. A tree walked over ten minutes reflects the filesystem at ten
different moments, and a file changed halfway through is captured mid-change.

For anything actively written, back up the **application's** notion of a consistent copy
rather than the filesystem's:

* A database: use its own source type, or its dump tool via `script`.
* An LVM or ZFS volume: snapshot it in a `script`, back up the snapshot, release it.
* An upload directory that only ever appends: `folder` is fine as-is.

## Sizing [#sizing]

The zip is the sum of the files plus per-entry overhead, uncompressed. A tree of a million
small files produces a large archive and a slow walk; progress is reported per 200 files so
you can see it moving.

Budget roughly twice the tree's size on the worker's staging disk: the zip and its encrypted
copy exist at the same time.

## Restoring [#restoring]

```bash
sctl restore folder <artifact-id> --path ./uploads.zip
unzip ./uploads.zip -d /srv/uploads
```

Check ownership, permissions and timestamps afterwards. None of them are in the archive.
