A Rust crate that handles Google Drives API and offers an interface that makes sense for FUSE file systems. Support for Gnome Online Accounts is built in, but applications using this crate can also provide their own sources of tokens.
Find a file
2026-09-06 11:40:05 +00:00
examples Performance improvements 2026-09-06 13:10:24 +02:00
src More performance improvements 2026-09-06 13:39:26 +02:00
tests Implement M15 2026-09-05 11:03:50 +02:00
.gitignore Initial commit 2026-09-04 21:40:07 +00:00
Cargo.toml Support large files and resolve race-condition 2026-09-06 12:12:09 +02:00
LICENSE Initial commit 2026-09-04 21:40:07 +00:00
notes.txt More performance improvements 2026-09-06 13:39:26 +02:00
performance.md More performance improvements 2026-09-06 13:39:26 +02:00
PLAN.md More performance improvements 2026-09-06 13:39:26 +02:00
README.md Support large files and resolve race-condition 2026-09-06 12:12:09 +02:00
summary.md More performance improvements 2026-09-06 13:39:26 +02:00
USER_GUIDE.md More performance improvements 2026-09-06 13:39:26 +02:00

gdrive4fs

A Rust crate that exposes Google Drive as a filesystem-flavoured API, designed to be wrapped by a FUSE filesystem (and friends). It wraps the Google Drive HTTP API (google-drive3) and presents a POSIX-like, async interface — getattr/lookup/readdir/open/read/write/flush/unlink/mkdir/rename/statfs — so FUSE glue stays trivial. The full design and milestone plan live in PLAN.md.

Status

Implemented (M1–M17). v1 is complete; M11 adds the optional "Shared with me" virtual folder; M12 adds an executable-bit policy (exec_policy); M13 adds the freedesktop /.Trash namespace over Drive's own trash; M14 replaces google-drive3's single-shot resumable upload with a chunked, byte-contiguous resumable uploader (2 MiB bands, reseeded from the server's Range header on 308 and from transfer-status queries after failures — the bandwidth milestone §7.1, re-scoped since Drive sessions can only start at byte 0); M15 implements duplicate_policy=RenameRemote (name collisions actually rename the Drive holder out of the way, race-guarded by a version compare, with a graceful EACCES fallback when the holder isn't owned) and extended attributes over Drive appProperties under a gdrive4fs. namespace (getxattr/setxattr/listxattr/removexattr, coexisting with the M12 exec bit in the same property map); M16 is the v2 polish + release pass (FUSE example now configures and walks every v2 feature — see Examples and Feature build-out below); M17 adds session logging & metrics — every op logs a per-op line (getattr, cache.open, cache.flush, drive.upload.resumable, …) and each GDrive session collects a shared metrics ledger (op timings, byte totals, cache hit/reuse/eviction counters, retries, resumable band/stall/conflict counters) that is written out as one info! summary when the mount drops — enough to diagnose performance and corrupt-file issues from a plain log (RUST_LOG=debug for per-op tracing, default info). 196 unit tests (174 without the goa feature) — zero warnings on cargo build/cargo test/cargo doc in both feature configurations. cargo clippy is not installable in this environment (no rustup), so the zero-warning build/test/doc gate stands in its place. Two runnable examples ship with the crate; live end-to-end smoke tests (basic + GOA round-trips, a multi-band ~9 MiB resumable round-trip, a real files.update(name) rename-remote, an appProperties xattr round-trip, trash walk + empty_trash, and the shared feed) are opt-in behind the integration feature — 7/7 green against a live GOA account.

Feature build-out

Feature Milestone Status
Namespace + content ops (getattr/lookup/readdir/open/read/write/flush/create/mkdir/unlink/rmdir/rename/truncate/statfs) M9 ✓
Whole-file on-disk content cache (export/import of Google-native files) M8 ✓
Chunked resumable uploads (≥5 MiB; banded, byte-contiguous) M14 ✓
Optimistic conflict handling (FailOnRemoteChange) M9 ✓
"Shared with me" virtual folder (mount_shared) M11 ✓
Executable bit — exec_policy (Property/Sidecar/Heuristic) M12 ✓
freedesktop /.Trash (walk, trash/restore, permanent delete in-trash, empty_trash) M13 ✓
duplicate_policy=RenameRemote (colliding create renames the holder away) M15 ✓
xattr over appProperties (gdrive4fs. namespace) M15 ✓
GOA token provider (optional goa feature) M5 ✓
FUSE bridge example (examples/fuse-mount.rs) + v2 ops M10 / M16 ✓
Session logging & metrics (log/Metrics, RUST_LOG-tunable) M17 ✓
Live-Drive smoke tests (optional integration feature) M10+ ✓

Usage

[dependencies]
gdrive4fs = { version = "0.1", default-features = false }

Connect with a token you manage yourself,

use gdrive4fs::config::GDriveConfig;
use gdrive4fs::fs::GDrive;

let drive = GDrive::connect_with_token(token.into(), GDriveConfig::default()).await?;

or reuse an existing token source (GDrive::connect_with_source). With the default goa feature (Linux), a ready-made Gnome Online Accounts provider picks the Drive-enabled Google account for you.

Examples

  • examples/basic.rs — a credential + feature walk over a live account: stat /, mkdir, create, open→write→flush, read, readdir, then the v2 ops — an xattr round-trip, an exec-bit chmod, a freedesktop trash walk (trash a file, list /.Trash/<uid>/files/, read its synthesized .trashinfo, rename it back out to restore), a slice of the /Shared with me feed (mount_shared on), and cleanup. Run with GDRIVE_TOKEN=<token> cargo run --example basic.
  • examples/fuse-mount.rs — the FUSE bridge described below. Run with cargo run --example fuse-mount -- <mountpoint> [--shared] [--delete=trash|permanent] [--exec=property|sidecar|heuristic] [--duplicates=suffix|first-wins|rename-remote] (unmount: fusermount -u <mountpoint>). The flags configure the v2 features the mount exposes: --shared shows the /Shared with me view, --delete selects the /.Trash presentation or permanent deletes, --exec picks the executable-bit policy (the setattr(mode) callback is the exec-bit chmod path), and --duplicates selects the collision policy. The four xattr callbacks (get/set/list/removexattr) are wired unconditionally. fuser exposes no empty_trash op, so emptying the trash uses the API (the basic example calls it after its walk).

Both connect from GDRIVE_TOKEN if set, otherwise from the GOA account.

A FUSE mount in one file

gdrive4fs is deliberately FUSE-agnostic — it does not depend on fuser. The bridge lives in the consumer (examples/fuse-mount.rs shows the whole thing):

  • GDrive is async and path/handle-based; fuser is a synchronous, inode-based kernel protocol. The glue holds a tokio::runtime::Handle, an inode → path map (inodes are deterministic — the crate's inode table is stable per Drive id), and an fh → OpenFile map; every fuser callback simply block_ons the matching GDrive call.
  • Because inodes are deterministic and paths stable, the inode → path map fills up naturally as the kernel walks lookup/readdir.
  • Every v2 feature rides the same glue: .Trash and /Shared with me are ordinary virtual directories from the kernel's point of view (walk, restore and in-trash delete are plain readdir/rename/unlink), chmod is exec-bit-only, and xattrs map to Drive appProperties.

Feature flags

Feature Default Purpose
goa on GOA/D-Bus token provider (dbus dependency, Linux only)
integration off live-Drive + live-D-Bus smoke tests (needs credentials)

Use default-features = false for a portable, dependency-light build.

Token sources

The crate accepts a TokenSource (a cloneable, Send + Sync provider of fresh tokens), or a plain token for short-lived sessions:

  • connect_with_token(token, cfg) — fixed token (tests / quick experiments).
  • connect_with_source(source, cfg) — application-owned acquisition and refresh.
  • CallableTokenSource — wrap a closure so the crate re-fetches on 401.

Design notes

  • Whole-file caching: reads/writes go through a local on-disk cache; modified files are re-uploaded whole on flush/close. No partial-write emulation.
  • Resumable uploads (M14): payloads above 5 MiB bypass the simple multipart upload and go through a banded resumable session (2 MiB PUT bands with Content-Range, the cursor reseeded from the server's Range header on 308 and from transfer-status queries after any failed band). Drive sessions are byte-contiguous from offset 0 — there is no way to start a session at a mid-file offset — so "upload only the dirty range" is impossible and the whole file is always re-sent; the bands merely bound memory and resume state.
  • Deterministic inodes: stable FNV-1a-64 of the Drive file id.
  • Google-native files (Docs/Sheets/Slides): shown as editable regular files — exported on open and re-imported on flush.
  • Errno-flavored errors (Error::to_errno()) for FUSE glue.
  • Conflicts: optimistic — the files.version token is replayed on upload; fail_on_remote_change surfaces a stale copy instead of clobbering silently.
  • "Shared with me" (config.mount_shared, default off): a virtual /Shared with me folder at the mount root lists Drive's shared feed (sharedWithMe = true). Its items are keyed by file id, not path, so content, export and mutation ops work unchanged once an id is resolved; a file you also own under "My Drive" keeps a single inode in both views.
  • freedesktop Trash (delete_mode=Trash, the default): a virtual /.Trash folder at the mount root presents Drive's own trash. FM trash actions map onto it — renaming a file into /.Trash/<uid>/files/ trashes it, the /.Trash/<uid>/info/<name>.trashinfo write is accepted-and-dropped, mkdir of the uid/files/info scaffolding is ignored, renaming a file out of the trash restores it, and unlinking inside the trash permanently deletes it. .trashinfo records are synthesized (freedesktop [Trash Info]; Path from the last-known location, DeletionDate from trashedTime, falling back to modifiedTime). empty_trash calls files.emptyTrash. delete_mode=Permanently omits /.Trash entirely.
  • Executable bit (config.exec_policy, default Property): Drive has no perms, so mounts expose an exec bit per file and honour exec-bit-only chmods. Property persists it in the file's appProperties (gdrive4fs.exec — 15 bytes, well under Drive's 124-byte cap) for files you own; for shared/read-only files the chmod falls back to a machine-local manifest. Sidecar stores every chmod in that manifest; Heuristic (the default look-on-a-mount) just marks .sh/.py/.exe/… by extension and rejects chmods (ENOTSUP).
  • RenameRemote (config.duplicate_policy, default Suffix): instead of presenting duplicates with (N) suffixes, a colliding create/mkdir/ rename actually renames the Drive holder to the next free name (N) (files.update(name)) so the caller can claim the bare name — consistent for every client and persistent beyond this mount. Opt-in for a reason: it mutates Drive. Two safety rails: the freed slot is picked against a fresh listing, and the rename replays the holder's version so two racing renames can't both win (a lost race surfaces as EEXIST; a holder this account can't rename — e.g. a file only shared with you — degrades to EACCES, nothing moves).
  • Extended attributes (M15): getxattr/setxattr/listxattr/ removexattr read and write Drive appProperties under a gdrive4fs. namespace — the xattr name is the property key, so setfattr -n gdrive4fs.user.color -v blue f.txt stores appProperties["gdrive4fs.user.color"] = "blue". Names must match Drive's property-key charset (A-Za-z0-9.!@$%^&*()-_/ — Drive rejects : and friends with a 403; discovered live, hence the . namespace). Outside the namespace: ENOTSUP; missing attribute: ENODATA; non-UTF-8 value: EINVAL; key+value over Drive's 124-byte budget: EFBIG. xattrs share the map with the exec bit, so chmod and xattrs coexist. FUSE glue for all four ops ships in examples/fuse-mount.rs. Removal caveat (M15, live-verified): Drive's files.update merges appProperties, and google-drive3 cannot express the null value Drive needs to truly delete a key — so removexattr persists an empty-string tombstone that getxattr/listxattr report as absent; the property lingers in the Drive metadata (a documented trade-off, consistent across remounts).
  • Logging & metrics (M17): the crate logs through the standard log façade — RUST_LOG controls verbosity (both examples initialize env_logger: basic defaults to debug for full per-op tracing, fuse-mount to info). Every public op reports one foreground line (e.g. getattr /a/b, cache.open <id> with the stale-reason and fetched bytes, cache.flush <id> with the re-upload size and conflict guard, drive.upload.resumable <size> with per-band progress and running MiB/s). Each GDrive session also keeps a per-instance Metrics ledger — shared with the cache and drive connection — of op timings, byte totals, cache hits/reuses/evictions, 401 replays, pagination and resumable band/stall/conflict counters; when the mount drops, the whole ledger is written as one info! summary (with derived MiB/s and the cache.reuse.ratio). Size-mismatch warn!s inside the cache (slot filled ≠ metadata size; upload reported ≠ media sent) flag the truncated/corrupt-file class with the file id attached.

Limitations

Per feature (each maps to a "Design notes" bullet above):

  • Permissions — no chown/mknod/symlink (ENOTSUP); chmod is exec-bit-only (under Heuristic the chmod itself fails with ENOTSUP).
  • Shared with me (mount_shared) — a filter view, so you cannot create files directly inside /Shared with me (create inside a shared folder instead), shared-drive full listing is out of scope, and mutating a shared file requires the owner's edit permission (otherwise the write fails with a permission error).
  • Slash/backslash/dot names (§7.9) — Drive allows / in file names and exact ./.., neither of which is a valid POSIX path component. Such names are presented C-style-escaped (/→\/, \→\\, exact ./.. get a leading \), consistently across lookup/readdir (one pipe in children_of), and decoded back only at the Drive write (create/mkdir/rename). Names without / or \ pass through unchanged. Caveats: an escaped \/ still contains a / byte, so a kernel FUSE mount still drops those entries from readdir and no Path can name them — slash-names are addressable through the crate's OsStr-name ops (lookup/create/mkdir/unlink/rmdir) in escaped form, while \- and dot-names work everywhere including FUSE; presented names over 255 bytes are skipped (with a warning) as unlistable via getdents.
  • Duplicates — Drive's duplicate-name allowance is reconciled by presented (N) suffixes (deterministic by createdTime); under the default policy the duplicates stay on Drive. RenameRemote fixes Drive itself (opt-in, mutates your data), and a holder this account can't rename degrades to EACCES.
  • Trash — rmdir removes a Drive folder regardless of contents (Drive has folder-level granularity, no ENOTEMPTY); the trash feed is keyed by file id, and fuser exposes no empty_trash op on Linux (empty via GDrive::empty_trash() — see examples/basic.rs).
  • Writes — whole-file: large edits upload the entire file on flush (the

    5 MiB resumable path sends it in 2 MiB bands — configurable — from the on-disk slot, so RAM stays flat; no partial-Drive-write exists, so whole-file re-upload stays — see the M14 design note above). Single files are capped by max_file_bytes (unlimited by default, EFBIG when set); uploads are MD5-verified against the server checksum. Drive caps single files at 5 TB and idle resumable sessions expire after ~1 week.

  • xattr removal — removexattr persists an "" tombstone (see the design note) that reads as absent; the property lingers in the Drive metadata.
  • Conflicts — optimistic (files.version replayed on upload); fail_on_remote_change surfaces a stale copy instead of clobbering silently.
  • GOA — needs a running GNOME session D-Bus; connect_goa picks the single Drive-enabled Google account and errors helpfully when ambiguous.

Live smoke tests

GDRIVE_TOKEN=<token> cargo test --features integration live_

These hit the real Drive API (and a live D-Bus session for the GOA test); they skip at runtime when credentials are absent, so they never fail a CI run. The suite (tests/live_drive.rs, all green against a live GOA account):

  • live_drive_basic_round_trip — mkdir/create/write/flush/read/unlink/rmdir on a real Drive tree.
  • live_goa_smoke — GOA token acquisition + connect (+ filter/Scopes).
  • live_shared_with_me_smoke — the /Shared with me feed counts and lists.
  • live_trash_smoke — trash a file, walk /.Trash, read the synthesized .trashinfo (Path + DeletionDate), permanently delete, empty_trash.
  • live_resumable_upload_smoke — ~9 MiB payload, 5 × 2 MiB bands, byte-exact read-back round-trip.
  • live_rename_remote_smoke — a colliding create actually renames the Drive holder to name (1).
  • live_xattr_smoke — setxattr/getxattr/listxattr/removexattr against real Drive appProperties.