Apollo PS4 USB vs HDD Save Handling: Storage Architecture and Code Differences
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, 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.
#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 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.
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.
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 handles this by calling mountSave() with the appropriate volume and key paths. This creates a sandboxed mount point under APOLLO_SANDBOX_PATH.
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, 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".
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. 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.
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.
/* 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_HDDflag (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 invokessceSaveDataDelete()viaorbis_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) determines the UI behavior. When _addBackupCommands() in 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →