Skip to content

midnightkoderr/adbsync

Repository files navigation

adbsync

Bidirectional file sync between a connected Android device and your machine over ADB.

adbsync is a CLI tool in the spirit of rsync — but for Android. It syncs files in either direction between your phone and your computer using only a USB cable and ADB, with no apps, no cloud, and no proprietary software required.

Why

Android's file transfer story over USB is broken by default. MTP (the protocol most phones expose) is slow, crashes frequently, and varies by kernel driver — plug in the same phone on two different Linux machines and you may get different results. GUI tools like KDE Connect or Android File Transfer are useful but require running a daemon or a separate app, and none of them let you script recurring syncs from the terminal.

ADB is different. It's the same protocol Android Studio uses to deploy apps — battle-tested, reliable over USB, and available on any device with developer options enabled. adbsync wraps adb push, adb pull, and adb shell into an rsync-style workflow so you can sync directories, skip already-identical files, back up orphans, and chain sync jobs in shell scripts.

Key things it handles that raw adb does not:

  • Skip unchanged files — compares by size + mtime (or by hash with --dedup-hash) so you don't re-push 10 GB of photos every run
  • Paths with spacesadb shell splits arguments on spaces; adbsync shell-quotes every device path before passing it in
  • Orphan backup — files present on one side but absent on the other can be moved to a local backup directory instead of deleted or ignored
  • Always-excluded directories.thumbnails (Android's thumbnail cache) is silently skipped in both directions so it never clutters your local tree

Requirements

  • Python 3.12+
  • adb (Android Debug Bridge) on your PATH, or set ANDROID_HOME

Installation

uv tool install .

Shell completion

Enable tab completion so that options appear when you press Tab:

bash — add to ~/.bashrc:

eval "$(adbsync completion bash)"

zsh — add to ~/.zshrc:

eval "$(adbsync completion zsh)"

fish — add to ~/.config/fish/config.fish:

adbsync completion fish | source

Or source once in the current session:

source <(adbsync completion bash)   # bash
source <(adbsync completion zsh)    # zsh
adbsync completion fish | source    # fish

Pressing Tab on an empty input after adbsync sync shows all available options, not just after typing -.

Usage

adbsync [--connect] <command> [options]

Commands

Command Description
sync Sync files between phone and host
completion Output shell completion script for bash, zsh, or fish

sync

adbsync [--connect] sync --src <PATH> --dest <PATH> [--no-recurse] [--dry-run]
                                              [--include PATTERN] [--exclude PATTERN]
                                              [--backup-orphans PATH] [--backup-excluded]
                                              [--force] [--dedup-hash MODE]
                                              [--min-size BYTES] [--max-size BYTES]
                                              [--verify] [--since DATETIME]

Paths must be prefixed with phone: (device) or host: (local machine). Either side can be source or destination.

Examples

Pull all files from the device to your computer:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music

Push local files to the device:

adbsync sync --src host:~/Music --dest phone:/sdcard/Music

Wait for a USB device to connect, then sync:

adbsync --connect sync --src phone:/sdcard/DCIM --dest host:~/Pictures

Preview what would be transferred without copying anything:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --dry-run

Sync only the top-level files (no subdirectories):

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --no-recurse

Skip files matching a glob pattern:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --exclude '*.mp4'

Multiple patterns are supported by repeating the flag:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --exclude '*.mp4' --exclude '*.tmp'

Back up orphaned files (present on one side but not the other) to a local directory:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --backup-orphans ~/Music-backup

Also back up files that match an exclude pattern:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music \
  --exclude '*.mp4' --backup-orphans ~/Music-backup --backup-excluded

Allow a file to overwrite an empty conflicting directory at the destination:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --force

Use a partial hash (4 KB head+tail) instead of mtime to decide whether a file has changed:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --dedup-hash partial

Use a full SHA-256 hash for the most accurate change detection:

adbsync sync --src phone:/sdcard/Music --dest host:~/Music --dedup-hash full

Options

Flag Description
--connect Block until a USB device is detected before running
--dry-run Print what would be synced without transferring any files
--no-recurse Only sync files directly inside the source directory
--include PATTERN Only sync files whose name matches the glob pattern (repeatable); files not matching any pattern are skipped
--exclude PATTERN Skip files whose name matches the glob pattern (repeatable)
--backup-orphans PATH Move files absent from the other side to host PATH instead of leaving them
--backup-excluded Also move files matching --exclude or --include filters to the backup path
--force Remove an empty conflicting directory before writing a file in its place
--dedup-hash MODE Compare files by hash instead of mtime: partial (4 KB head+tail) or full (entire file)
--min-size BYTES Skip files smaller than BYTES
--max-size BYTES Skip files larger than BYTES
--verify Hash-check each transferred file against its source after transfer
--since DATETIME Only sync files modified after DATETIME (ISO 8601, e.g. 2024-01-01 or 2024-06-15T14:30:00)
--version Print version and exit

How sync works

  • Pull (phone:host:): lists files on the device via adb shell find/stat, skips files already present at the destination, pulls changed or missing files, and restores mtime after transfer.
  • Push (host:phone:): walks the local source tree, creates directories on the device as needed, and pushes each file via adb push.

File comparison

By default, files are compared by size and modification time. If both match, the file is skipped.

With --dedup-hash, mtime is replaced by a SHA-256 hash comparison:

Mode How
partial Reads first 4 KB and last 4 KB, hashes them together. Fast; handles files ≤ 4 KB by reading the whole file.
full Reads the entire file. Use when you need certainty over speed.

In both modes the hash is computed on both sides independently — on the device via adb shell sha256sum, locally in Python — and the file is skipped only when they match.

--include and --exclude

Both flags match against the filename only, not the full path, using Python's fnmatch rules (same as shell glob). Either flag can be repeated for multiple patterns.

--include is a whitelist: if any include patterns are provided, only files whose name matches at least one of them are synced. --exclude is a blacklist: matching files are always skipped. Exclude takes precedence — a file matching both is skipped.

--backup-orphans and --backup-excluded

Without --backup-orphans, files that exist on one side but are absent from the other are left in place. With --backup-orphans PATH, those orphaned files are moved to PATH (always a local host directory) with their directory structure preserved relative to the sync root.

  • Pull (phone:host:): host files absent from the device are moved to PATH via shutil.move.
  • Push (host:phone:): device files absent from the host are pulled to PATH and then removed from the device.

--backup-excluded extends this to files that are skipped due to --exclude. Without it, excluded files are left untouched even when --backup-orphans is set.

Example: given a destination with Music/a.mp3, Music/b.mp3, and Music/cover.jpg, and a source that only contains a.mp3 with *.jpg excluded:

File --backup-orphans only --backup-orphans + --backup-excluded
Music/a.mp3 (in source) left in place left in place
Music/b.mp3 (orphan) moved to backup moved to backup
Music/cover.jpg (excluded) left in place moved to backup

Always-excluded paths

Some directories are always skipped, regardless of --include or --exclude settings:

Directory Reason
.thumbnails Android thumbnail cache — regenerated automatically by the gallery app; syncing it wastes time and space

Exclusion is depth-agnostic: a file at any nesting level inside a .thumbnails directory is skipped. The exclusion matches the exact directory name, so a file like thumbnail.png or a directory named .thumbnails_extra is not affected.

There is no flag to override this behaviour — if a path is in this list, it is never transferred.

--force

If a directory exists at the destination where a file needs to be written, --force attempts to remove it with rmdir before proceeding. rmdir only succeeds on empty directories — if the directory contains any files, the conflict is skipped and a warning is printed. This prevents accidentally destroying files nested inside a same-named directory.

Development

uv sync
uv run pytest

License

Apache 2.0 — see LICENSE.

About

adb sync files to-from device to host

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors