3DSync

Homebrew for the Nintendo 3DS/2DS family that synchronises saves and files with cloud storage or a server on your own network.
Bidirectional sync with Google Drive, Dropbox, an SMB2/3 file server, FTP/FTPS and WebDAV.

settingsOpen Configurator file_downloadInstall Source Code

Quick Start

  1. Open the configurator and follow the three steps. To change a file you made before, import it in the first step.
  2. Place the downloaded 3DSync.ini on the SD card at /3ds/3DSync/3DSync.ini.
  3. Install 3DSync, most easily through Universal-Updater.
  4. Launch 3DSync and press A to sync all configured paths, or Y to sync with debug output. START exits.

Installation

All three ways need a 3DS running custom firmware with the Homebrew Launcher or FBI; see 3ds.hacks.guide if yours does not.

Universal-Updater (recommended)
  1. In Universal-Updater, open Settings → Select a UniStore → +.
  2. Scan the QR code, or type the store URL:
    https://3dsync.vllni.dev/unistore/3dsync.unistore
  3. Select the 3DSync store, open the 3DSync entry and pick a download: 3DSync.cia installs it to the HOME Menu, 3DSync.3dsx puts it in the Homebrew Launcher.

New releases show up in the store by themselves, and the entry is flagged when an update is available. Earlier versions stay listed as [v0.6.0] 3DSync.cia and so on, should you need to go back.

QR code of the 3DSync UniStore
FBI

Every release carries a QR code for its .cia. In FBI, choose Remote Install → Scan QR Code and scan it.

By hand

Download from the latest release:

  • 3DSync-<version>.cia — copy it to the SD card and install it with FBI.
  • 3DSync-<version>.3dsx — copy it to /3ds/3DSync/3DSync.3dsx and launch it from the Homebrew Launcher.

The .zip holds both, plus a .3ds for flashcarts.

Updating or reinstalling keeps your settings: 3DSync.ini, the sync manifest and the debug logs live in /3ds/3DSync/ on the SD card, not inside the app.


Configuration

The INI file lives at /3ds/3DSync/3DSync.ini.

Dropbox
[Dropbox]
AppKey=<app key from configurator>
RefreshToken=<refresh token from configurator>
Path=                     ; optional folder inside the account

Configs with a bare Token= still work, but Dropbox access tokens expire after about four hours — re-run the configurator to get a refresh token instead.

add_to_driveGoogle Drive
[GoogleDrive]
ClientId=<OAuth client ID>
ClientSecret=<OAuth client secret>
RefreshToken=<refresh token from configurator>
FolderId=<optional root folder ID>

FolderId restricts all Drive operations to one folder. Leave it out to use Drive root.

storageSMB file server (NAS, Windows share)
[SMB]
Server=192.168.1.10
Share=3ds
User=<username>
Password=<password>
Domain=                   ; optional, for a Windows domain
Path=                     ; optional folder inside the share

Speaks SMB2/SMB3, the dialects modern NAS boxes and Windows accept by default. SMB1 does not need to be re-enabled.

folder_sharedFTP / FTPS
[FTP]
Host=192.168.1.10
Port=                     ; optional, 21 (990 for TLS=implicit)
User=<username>
Password=<password>
Path=                     ; optional base directory
TLS=try                   ; none | try | require | implicit
Mode=passive              ; passive | active

TLS=try encrypts the session when the server offers AUTH TLS and falls back to plaintext otherwise — also for an anonymous login the server refuses over TLS (stock vsftpd does), but never for a named user, whose password would then travel unencrypted. SFTP is not supported — the 3DS build of libcurl has no libssh2.

cloud_queueWebDAV
[WebDAV]
Url=https://nas.example/remote.php/dav/files/username/
User=<username>
Password=<app password>

Works with Nextcloud/ownCloud and any NAS with WebDAV enabled. Prefer an app password over your account password.

Configure as many remotes as you like, several of one kind too: [SMB:backup] is a second SMB remote, labelled backup. A save changed in one place reaches all the others in the same sync. Role=backup makes a remote only receive copies, Role=source makes it only send them, and Only=saves, gba limits it to those folders. The configurator writes all of them into one file.

On startup 3DSync checks the file. If a remote is missing a setting or a login, a section has a misspelled type, Role= or Only= make no sense, or a [Filter] line names no configured path, it lists every problem with what to do, before the main menu. Press A to continue anyway or START to exit and fix the file.


Sync Paths

Four INI sections control which local paths to sync and how:

Section Direction Subdirs
[Paths] Bidirectional Yes
[ShallowPaths] Bidirectional No
[UploadPaths] Upload only Yes
[UploadShallowPaths] Upload only No

Each entry has the form RemoteName=LocalPath:

[Paths]
Checkpoint=/3ds/Checkpoint/saves
roms/nds/saves=/roms/nds/saves

[UploadPaths]
Backup=/3ds/MyGame
  • RemoteName is the folder on the remote (use / for nesting, e.g. roms/nds). It is also the name Only= refers to.
  • LocalPath is the absolute path on the SD card.
  • Bidirectional entries use a manifest (/3ds/3DSync/manifest.json) to detect which side changed.
  • Upload-only entries push the local file to every remote taking part and never download. A Role=source remote is left out of them: 3DSync never writes to it.
File type filter

By default every file under a sync path is synced. To sync only certain file types, list their extensions under [Filter]: Extensions= for every path, or a path's name for that path alone:

[Filter]
Extensions=sav, bin, dat   ; every path without a line of its own
roms/nds/saves=dsv         ; only .dsv in this one
screenshots=*              ; every file in this one
  • Separate extensions with commas, spaces or both — sav,bin, sav, bin and sav, bin,srm all work (not ;, which starts a comment). sav, .sav and *.sav all mean the same, and case does not matter.
  • Leave Extensions= empty, leave it out, or write * to sync all files.
  • A path's own line replaces Extensions= for that path rather than adding to it. A line naming a path that is not configured is reported when 3DSync starts.
  • Filters apply in both directions: a filtered-out file is neither uploaded nor downloaded.
  • The main menu on the 3DS shows the filter next to FILES (per path once a path has its own), and a sync prints it under each path it applies to.

Bidirectional Sync Logic

Bidirectional paths ([Paths] / [ShallowPaths]) compare each file's local modification time and size, and each remote's change tag, against the manifest from the last sync.

Remote Change tag
Google DriveMD5 checksum from the Drive API
Dropboxcontent_hash (SHA-256 over per-4 MiB-block SHA-256)
WebDAVETag, falling back to size + Last-Modified
SMBsize + modification time
FTPsize + MDTM

With several remotes configured, every file is decided once with all of them in view. The side that changed wins and its version is copied to all the others in the same run, whichever order the remotes are in. A remote that was unreachable while the file changed is simply brought up to date the next time.

Local changed? Remote changed? Action
No No Skip (up to date)
Yes No Upload local → remote, update manifest
No Yes Download from remote → local, update manifest
Yes Yes Conflict — the 3DS lists every side that changed. A keeps the highlighted one (tap a version, or move with R or D-pad ↑/↓), X skips the file, L applies the answer to every remaining conflict, START stops. The version kept is copied to the others.
Local missing Remote exists Download from remote
Local exists Remote missing Upload to remote
Both missing — Remove stale manifest entry

"Changed" means the local mtime/size, or the remote tag, differs from the last manifest entry. On the first sync of a file, a file on one side only is copied to the others; a file already on both is a conflict, unless Google Drive or Dropbox can prove it holds the same bytes as the 3DS. They can check contents on the 3DS, so they also catch a save whose modification time never moves; on SMB, FTP and WebDAV an unchanged time and size is taken as unchanged.

Manifest file

/3ds/3DSync/manifest.json is stored only on the SD card. It records the 3DS's copy and each remote's tag per file. Deleting it makes the next sync a first sync again: every file that exists both on the 3DS and on a remote becomes a conflict, unless Drive or Dropbox can prove the two are identical.

{
  "drive|/3ds/Checkpoint/saves/TitleA/001.sav": {"mtime": 1716905520, "size": 512, "tag": "abc123", "id": "driveFileId"},
  "local|/3ds/Checkpoint/saves/TitleA/001.sav": {"mtime": 1716905520, "size": 512, "tag": "", "id": "", "gen": 1}
}

add_to_driveGoogle Drive App Setup

  1. Go to Google Cloud Console.
  2. Create a project and enable the Google Drive API.
  3. Create an OAuth 2.0 Web application credential (Client ID + Client Secret).
  4. Add the redirect URI: https://3dsync.vllni.dev/configurator.html
  5. Use the configurator to authenticate; it performs PKCE and stores the refresh token.
Note: Apps in Testing mode issue refresh tokens that expire after 7 days. Publish the app or add your Google account as a test user to avoid frequent re-authentication.

Building

Requires devkitPro with 3DS support.

make

Output: output/3ds-arm/3DSync.cia and output/3ds-arm/3ds/3DSync/3DSync.3dsx.

Full credit to Kyraminol for the original project. Extended by michvllni with Google Drive support, refresh-token auth, and full bidirectional sync.