# `Mob.Storage`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/storage.ex#L1)

App-local file storage.

Provides a thin Elixir wrapper over the device filesystem using named
locations instead of raw paths. Basic file operations (`list`, `stat`,
`delete`, `copy`, `move`, `read`, `write`) are pure `File.*` calls —
no NIF overhead except for the initial `dir/1` path resolution.

## Locations

  - `:temp`        — ephemeral; may be purged by the OS at any time
  - `:documents`   — persists across app sessions; backed up to iCloud on
                     iOS; user-visible there when `UIFileSharingEnabled` is
                     set (see `mix mob.enable`)
  - `:cache`       — persists until OS needs space; not user-visible, not
                     backed up
  - `:app_support` — persists, hidden from user; backed up to iCloud on iOS
                     unless excluded (see `exclude_from_backup/1`)

On Android a generated app sets `android:allowBackup="false"`, so none of
these go to a cloud backup. On Android 12 and higher that flag doesn't stop
a device-to-device transfer; only `android:dataExtractionRules` does.

## Backup

Apple rejects apps that back up data they can download again. Keep such data
(downloaded models, content packs, media caches you don't want purged) in
`:app_support` and mark its directory with `exclude_from_backup/1`.

## Platform-specific storage

See `Mob.Storage.Apple` and `Mob.Storage.Android` for saving to the native
photo/media library and accessing platform-specific directories.

## Results

Operations that can fail return `{:ok, value} | {:error, posix}`.
`dir/1` raises on an unknown location atom.

# `copy`

```elixir
@spec copy(String.t(), atom() | String.t()) :: {:ok, String.t()} | {:error, atom()}
```

Copy `src` to `dest`.

`dest` may be a location atom (file is placed there keeping its basename)
or a full absolute path.

Returns `{:ok, dest_path}` on success.

# `delete`

```elixir
@spec delete(String.t()) :: :ok | {:error, atom()}
```

Delete a file.

# `dir`

```elixir
@spec dir(atom()) :: String.t()
```

Resolve a location atom to its absolute path on the current device.

# `exclude_from_backup`

```elixir
@spec exclude_from_backup(String.t(), module()) ::
  :ok | {:error, :enoent | :unsupported | String.t()}
```

Exclude a file or directory from iCloud and device backups.

On iOS this sets `NSURLIsExcludedFromBackupKey` on `path`. On a directory
the flag covers everything beneath it, including files created later, so
marking the top directory of your downloaded data is enough. Call it at
every launch, not once: restoring the app or moving it to a new device can
drop the attribute, and setting it again is cheap.

On Android this does nothing; Android has no per-file flag. Generated apps
set `android:allowBackup="false"`, which keeps app data out of cloud
backups (and, up to Android 11, device-to-device transfers). To keep a
path out of either when backup is on, or out of an Android 12+
device-to-device transfer, exclude it in the app's backup rules
(`android:fullBackupContent` for Android 11 and lower,
`android:dataExtractionRules` for 12 and higher). Off device (host, tests)
it also does nothing.

Returns `{:error, :enoent}` on every platform when `path` doesn't exist,
and on iOS `{:error, description}` with the system's message when the
attribute can't be set. On an iOS app whose native build predates this
function (mob 0.9.20 or older, not rebuilt with `mix mob.deploy --native`)
it returns `{:error, :unsupported}`: the path stays backed up, and saying
`:ok` would hide that.

# `excluded_from_backup?`

```elixir
@spec excluded_from_backup?(String.t(), module()) :: boolean()
```

Whether `path` is excluded from backup, read back from the filesystem.

`true` only on iOS, for a path whose `NSURLIsExcludedFromBackupKey` is set.
A path inside an excluded directory reports `false` (the flag is on the
directory, not copied to its contents), yet it is not backed up. Android and
the host always answer `false`, and so does an iOS native build that
predates this function; see `exclude_from_backup/1`.

# `extension`

```elixir
@spec extension(String.t()) :: String.t()
```

Return the file extension including the leading dot, or `""` if none.

No I/O — derived from the filename only.

    Mob.Storage.extension("/tmp/clip.mp4")   #=> ".mp4"
    Mob.Storage.extension("/tmp/notes")      #=> ""

# `list`

```elixir
@spec list(atom() | String.t()) :: {:ok, [String.t()]} | {:error, atom()}
```

List all files in a location or an absolute path.

Returns full paths, not just names.

# `move`

```elixir
@spec move(String.t(), atom() | String.t()) :: {:ok, String.t()} | {:error, atom()}
```

Move `src` to `dest`.

`dest` may be a location atom or a full absolute path.

Returns `{:ok, dest_path}` on success.

# `read`

```elixir
@spec read(String.t()) :: {:ok, binary()} | {:error, atom()}
```

Read a file's contents as a binary.

# `stat`

```elixir
@spec stat(String.t()) :: {:ok, map()} | {:error, atom()}
```

Return metadata for a file.

    %{name: "clip.mp4", path: "/…/clip.mp4", size: 204_800,
      modified_at: ~U[2026-04-24 10:00:00Z]}

# `write`

```elixir
@spec write(String.t(), binary()) :: {:ok, String.t()} | {:error, atom()}
```

Write `data` to `path`.

Returns `{:ok, path}` on success.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
