# Apollo PS4 USB vs HDD Save Handling: Storage Architecture and Code Differences

> Explore Apollo PS4 USB vs HDD save handling differences. Understand storage architecture and code distinctions for `/mnt/usbX` and Orbis save-mount API.

- Repository: [Damián Parrino/apollo-ps4](https://github.com/bucanero/apollo-ps4)
- Tags: architecture
- Published: 2026-02-26

---

**Apollo PS4 treats USB saves as direct filesystem objects under `/mnt/usbX/` while HDD saves are managed through the Orbis save-mount API and a SQLite database, with the `SAVE_FLAG_HDD` flag determining UI behavior and copy commands.**

The `bucanero/apollo-ps4` project implements a dual-storage architecture that distinguishes between external USB save files and internal HDD save data. Understanding the handling and storage between USB saves and HDD saves is essential for developers extending Apollo's functionality or troubleshooting save management issues.

## Core Architectural Differences

### Storage Path Definitions and Macros

In [`include/saves.h`](https://github.com/bucanero/apollo-ps4/blob/main/include/saves.h), the root paths are defined distinctly for each storage type. USB paths use device-mounted directories, while HDD paths use the internal user directory structure.

```c
#define USB0_PATH               "/mnt/usb0/"
#define USB1_PATH               "/mnt/usb1/"
#define PS4_SAVES_PATH_USB      "PS4/APOLLO/"
#define SAVES_PATH_HDD          "/user/home/%08x/savedata/"
#define SAVE_FLAG_HDD           1024

```

USB saves reside under `/mnt/usbX/PS4/APOLLO/` and are accessed as regular files. HDD saves live in `/user/home/%08x/savedata/` where `%08x` represents the hexadecimal user ID, and require SQLite database queries for enumeration.

### Discovery and Enumeration Methods

The detection logic differs fundamentally between the two storage types. For USB saves, `read_usb_savegames()` in [`source/saves.c`](https://github.com/bucanero/apollo-ps4/blob/main/source/saves.c) performs direct filesystem traversal. It opens the USB directory, iterates through entries using `readdir()`, and validates saves by checking for the existence of `sce_sys/param.sfo`.

For HDD saves, `read_hdd_savegames()` queries the system's SQLite database. It opens `savedata.db` located at `SAVES_DB_PATH` and executes a SQL statement selecting from the `savedata` table.

## Data Access Models: Filesystem vs Database

### USB Save Detection via Direct Filesystem Access

The `read_usb_savegames()` function walks the directory tree and parses SFO files directly from the USB storage.

```c
while ((dir = readdir(d)) != NULL) {
    if (dir->d_type != DT_DIR) continue;
    snprintf(sfoPath, sizeof(sfoPath), "%s%s/sce_sys/param.sfo", userPath, dir->d_name);
    if (file_exists(sfoPath) != SUCCESS) continue;

    sfo_context_t* sfo = sfo_alloc();
    if (sfo_read(sfo, sfoPath) < 0) { … }
    item = _createSaveEntry(SAVE_FLAG_PS4, "", sfo_data);
    item->type = FILE_TYPE_PS4;
    …
    list_append(list, item);
}

```

### HDD Save Detection via SQLite Database

HDD saves are enumerated through the `savedata.db` database, which contains metadata for all saves registered on the system.

```c
sqlite3 *db = open_sqlite_db(userPath);
sqlite3_prepare_v2(db,
    "SELECT title_id, dir_name, main_title, blocks, account_id, sub_title FROM savedata",
    -1, &res, NULL);
while (sqlite3_step(res) == SQLITE_ROW) {
    item = _createSaveEntry(SAVE_FLAG_PS4 | SAVE_FLAG_HDD, "", name);
    item->type = FILE_TYPE_PS4;
    item->path      = strdup(userPath);
    item->dir_name  = strdup((const char*)sqlite3_column_text(res, 1));
    item->title_id  = strdup((const char*)sqlite3_column_text(res, 0));
    item->blocks    = sqlite3_column_int(res, 3);
    if (apollo_config.account_id == (uint64_t)sqlite3_column_int64(res,4))
        item->flags |= SAVE_FLAG_OWNER;
    list_append(list, item);
}

```

## Mounting and Encryption Handling

Access methods diverge based on whether saves are encrypted. Plain USB saves require no mounting. The application reads files directly from the USB filesystem path stored in `save_entry_t.path`.

Encrypted USB saves and all HDD saves require mounting through the Orbis API. The `orbis_SaveMount()` function in [`source/saves.c`](https://github.com/bucanero/apollo-ps4/blob/main/source/saves.c) handles this by calling `mountSave()` with the appropriate volume and key paths. This creates a sandboxed mount point under `APOLLO_SANDBOX_PATH`.

```c
int orbis_SaveMount(const save_entry_t *save, uint32_t mount_mode, char* mount_path) {
    snprintf(mountDir, sizeof(mountDir), APOLLO_SANDBOX_PATH, save->dir_name);
    …
    int mountErrorCode = mountSave(volumePath, keyPath, mountDir);
    …
}

```

## UI and Command Routing

### Copy Command Directionality

The user interface dynamically adjusts available commands based on the `SAVE_FLAG_HDD` flag. In `_addBackupCommands()` within [`source/saves.c`](https://github.com/bucanero/apollo-ps4/blob/main/source/saves.c), the code checks `item->flags & SAVE_FLAG_HDD`. If set, the UI presents "Copy save game to USB". If not set (indicating a USB source), it presents "Copy save game to HDD".

```c
if (item->flags & SAVE_FLAG_HDD) {
    cmd = _createCmdCode(PATCH_COMMAND, CHAR_ICON_COPY " ",
                         _("Copy save game to USB"), CMD_CODE_NULL);
    _createOptions(cmd, _("Copy Save to USB"), CMD_COPY_SAVE_USB);
} else {
    cmd = _createCmdCode(PATCH_COMMAND, CHAR_ICON_COPY " ",
                         _("Copy save game to HDD"), (item->flags & SAVE_FLAG_LOCKED)
                         ? CMD_COPY_PFS : CMD_COPY_SAVE_HDD);
}

```

### Export Path Selection

Export operations use distinct destination macros defined in [`source/exec_cmd.c`](https://github.com/bucanero/apollo-ps4/blob/main/source/exec_cmd.c). When executing `CMD_EXPORT_ZIP_USB`, the code uses `EXPORT_PATH_USB0` or `EXPORT_PATH_USB1`. For `CMD_EXPORT_ZIP_HDD`, it uses `EXPORT_PATH_HDD`.

```c
case CMD_EXPORT_ZIP_USB:
    zipSave(selected_entry,
            codecmd[1] ? EXPORT_PATH_USB1 : EXPORT_PATH_USB0);
    break;
case CMD_EXPORT_ZIP_HDD:
    zipSave(selected_entry, EXPORT_PATH_HDD);
    break;

```

## Deletion Mechanisms

Removal operations differ significantly between storage types. For USB saves, deletion uses standard filesystem operations. The path stored in `save_entry_t.path` is passed to `unlink()` for files and `rmdir()` for directories.

For HDD saves, deletion uses the Orbis API. The `orbis_SaveDelete()` function calls `sceSaveDataDelete()` with the save's directory name and user ID.

```c
/* HDD delete – uses Orbis API */
int orbis_SaveDelete(const save_entry_t *save) {
    …
    if (sceSaveDataDelete(&del) < 0) { LOG("DELETE_ERROR"); return 0; }
    return 1;
}

/* USB delete – regular file removal (called from exec_cmd.c) */

```

## Summary

- USB saves reside under `/mnt/usbX/PS4/APOLLO/` and are accessed as regular files, while HDD saves live in `/user/home/%08x/savedata/` and require SQLite database queries for enumeration.
- The `SAVE_FLAG_HDD` flag (value 1024) distinguishes storage types in the UI, automatically flipping copy commands between "to USB" and "to HDD".
- Plain USB saves bypass mounting entirely, whereas HDD saves and encrypted USB saves require `orbis_SaveMount()` to create a sandboxed access point.
- Deletion uses `unlink()`/`rmdir()` for USB saves but invokes `sceSaveDataDelete()` via `orbis_SaveDelete()` for HDD saves.

## Frequently Asked Questions

### How does Apollo PS4 detect saves on USB versus the internal HDD?

Apollo detects USB saves by scanning the `/mnt/usbX/PS4/APOLLO/` directories directly using `read_usb_savegames()`, parsing each save's `param.sfo` file to extract metadata. For HDD saves, it queries the system's `savedata.db` SQLite database via `read_hdd_savegames()`, reading from the `savedata` table to retrieve title IDs, directory names, and block counts.

### Why do HDD saves require mounting while some USB saves do not?

HDD saves are encrypted and managed by the PS4's Orbis save system, requiring `orbis_SaveMount()` to decrypt and sandbox the data before file operations. Plain USB saves are stored as unencrypted files that Apollo reads directly from the filesystem without mounting. However, encrypted USB saves do require mounting through the same `orbis_SaveMount()` mechanism used for HDD saves.

### What determines whether the UI shows "Copy to USB" or "Copy to HDD"?

The `SAVE_FLAG_HDD` flag (defined as 1024 in [`include/saves.h`](https://github.com/bucanero/apollo-ps4/blob/main/include/saves.h)) determines the UI behavior. When `_addBackupCommands()` in [`source/saves.c`](https://github.com/bucanero/apollo-ps4/blob/main/source/saves.c) detects this flag on a `save_entry_t`, it generates "Copy save game to USB". If the flag is absent, indicating a USB source, the UI presents "Copy save game to HDD" instead.

### Can Apollo delete saves from both storage types, and how do the methods differ?

Yes, Apollo supports deletion for both types, but uses different mechanisms. USB saves are deleted using standard filesystem calls (`unlink()` and `rmdir()`) operating on the path stored in the save entry. HDD saves are deleted using the Orbis API via `orbis_SaveDelete()`, which internally invokes `sceSaveDataDelete()` to properly remove the save from the system's encrypted storage and database.