Etherdrive is a local-first organization layer on top of etherchunk. It groups uploaded files into collections: named sets you can list, extend and prune, without ever building a Swarm manifest of its own.
Etherchunk sees only individual files. Every file etherdrive uploads is a separate etherchunk upload <file> call with its own root hash, its own registry row, and its own reclaimable slots. Collections exist purely in etherdrive's SQLite database, so grouping and listing cost nothing on the network: there is nothing to re-upload and no manifest to rebuild.
npm install --global etherdriveEtherdrive shells out to the etherchunk binary, so install that too and make sure it is on your PATH:
npm install --global etherchunkEtherdrive does not define its own upload configuration. It inherits etherchunk's environment, which must be set up as described in etherchunk's Postage Batch Setup:
export ETHERCHUNK_UPLOAD_URL="http://localhost:1633/chunks"
export ETHERCHUNK_SIGNER="<private key hex>"
export ETHERCHUNK_BATCH_ID="<batch id hex>"
export ETHERCHUNK_BATCH_DEPTH=<depth of your batch>Etherdrive adds three of its own, all optional:
export ETHERDRIVE_ETHERCHUNK_BIN="etherchunk" # path to the etherchunk binary
export ETHERDRIVE_GATEWAY_URL="http://localhost:1633" # base URL used when printing browse links
export ETHERDRIVE_STATE_DIR="$HOME/.etherdrive" # where etherdrive.db livesIf ETHERDRIVE_GATEWAY_URL is unset, it is derived from ETHERCHUNK_UPLOAD_URL by stripping the trailing /chunks, falling back to http://localhost:1633.
# Upload files and put them in a collection (created on first use)
etherdrive add photos ~/pics/a.jpg ~/pics/b.jpg
# Give an entry a label other than its basename
etherdrive add docs ~/work/2024-final-v3.pdf --as spec.pdf
# Pass upload options straight through to etherchunk
etherdrive add docs ~/work/spec.pdf --encrypt --redundancy=2 --parallelism=64
# List the entries in a collection
etherdrive ls photos
# List every collection
etherdrive ls
# Remove an entry, reclaiming its slots
etherdrive rm photos b.jpg
# Delete a collection and everything in it
etherdrive drop photosetherdrive add <collection> <file...> [--as <name>] [--encrypt] [--redundancy=<0-4>] [--parallelism=<n>]
For each path, etherdrive runs etherchunk upload <file> with the given flags, reads the root hash from stdout, and records an entry in the named collection. The collection is created if it does not exist.
add always uploads. There is no deduplication of any kind: no content hashing, no path matching. Adding the same path twice produces two entries with two root hashes and two sets of slots, and adding a file to a second collection uploads it a second time.
If etherchunk exits non-zero for a file, nothing is recorded for that file. Remaining files in the same invocation are still attempted, and etherdrive exits non-zero at the end.
--as applies only when a single file is given; the default label is the file's basename. Labels are etherdrive's own bookkeeping — they are not written to Swarm and need not be unique.
etherdrive ls [collection]
With no argument, prints each collection with its entry count and total size. With a collection name, prints one line per entry:
3f9a1c04... a.jpg 4.2 MB uploaded=2026-07-29T10:14:03Z http://localhost:1633/bzz/3f9a1c04…9e2b/
b2c18475... b.jpg 11.8 MB uploaded=2026-07-29T10:14:19Z http://localhost:1633/bzz/b2c18475…c7d1/
The first column is abbreviated to eight characters for reading; the URL carries the full reference. Each entry is browsable on its own, because etherchunk wraps every single-file upload in a one-entry Mantaray manifest with a website-index-document pointer.
etherdrive rm <collection> <name|root-hash>
Runs etherchunk delete <root-hash> to reclaim the entry's slots, then deletes the entry row. Removal always purges. An entry belongs to exactly one collection, so there is no case where the data should outlive its membership.
An entry is addressed by its label or by any prefix of its root hash, so the abbreviated hash ls prints is enough. If several entries in the collection match:
- When the matches share a root hash, they hold identical bytes and occupy the same amount of batch space, so one is removed and the rest are left in place with a note. A root hash could not have told them apart.
- When the root hashes differ, etherdrive lists the matches and does nothing; disambiguate with a root hash.
If etherchunk has no record of the hash, etherdrive warns that there were no slots to reclaim and still drops the local entry. Any other etherchunk failure leaves the entry in place.
etherdrive drop <collection> [--yes]
Deletes the collection along with every entry in it, calling etherchunk delete for each. Because this can reclaim a large amount of data at once, etherdrive asks for confirmation when stdin is a TTY, and requires --yes when it is not.
$ etherdrive drop photos
Delete collection 'photos' — 12 entries, 148 MB? [y/N]If any entry cannot be deleted, that entry and the collection are kept so a retry can finish the job.
Etherdrive holds no chunk-level state. It does not stamp or split chunks and it never talks to Bee. The slot map and the per-file chunk lists stay entirely in etherchunk's hands, so the two tools cannot disagree about which slots are occupied; etherdrive's only network effect is invoking etherchunk upload and etherchunk delete.
A single SQLite database. Unlike etherchunk's state, it is not partitioned per postage batch: collections may span batches, so the batch each entry was uploaded under is recorded on the entry itself and passed along when deleting.
CREATE TABLE collections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL -- unix ms
);
CREATE TABLE entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
root_hash BLOB NOT NULL, -- 32 bytes, or 64 when encrypted (ref || key)
source_path TEXT NOT NULL, -- absolute path at upload time
name TEXT NOT NULL, -- label within the collection; defaults to basename
size INTEGER NOT NULL, -- bytes on disk at upload time
batch_id BLOB NOT NULL, -- 32 bytes; the batch holding these chunks
batch_depth INTEGER NOT NULL, -- that batch's depth at upload time
encrypted INTEGER NOT NULL DEFAULT 0,
redundancy_level INTEGER NOT NULL DEFAULT 0,
uploaded_at INTEGER NOT NULL -- unix ms
);
CREATE INDEX idx_entries_collection ON entries(collection_id);
CREATE INDEX idx_entries_root_hash ON entries(root_hash);An entry belongs to exactly one collection. Neither root_hash nor (collection_id, name) is unique: since add always uploads, the same bytes can legitimately appear as several entries under different root hashes, in the same collection or in different ones.
Etherchunk writes progress to stderr and the resulting root hash, as bare hex, to stdout. Etherdrive spawns it with stderr inherited, so upload progress appears live, and captures stdout, taking the last non-empty line and requiring it to match /^[0-9a-f]{64}([0-9a-f]{64})?$/ (64 hex characters, or 128 for an encrypted upload). Anything else is treated as a failed upload and nothing is recorded.
Deletion is etherchunk delete <root-hash-hex>, which returns the entry's slots to the batch's free bitmap. Because etherchunk registers each single-file upload as one row covering all of that file's chunks, a delete is exactly as granular as an etherdrive entry.
Both commands run with ETHERCHUNK_BATCH_ID and ETHERCHUNK_BATCH_DEPTH pinned to the batch the entry belongs to, so entries uploaded under a batch you are no longer using still get deleted from the right slot map. Pinning is reliable even when you keep configuration in a .env file: etherchunk calls loadEnvFile(), which does not overwrite variables already present in the environment it inherits. The one value etherdrive prefers from the current environment is the depth of the batch you are using now, since a dilution plus etherchunk migrate makes the stored depth stale.
A collection deliberately does not resolve to a Swarm address. Making it one would mean building a Mantaray trie over the collection's members, and since etherchunk exposes no way to assemble a trie from already-uploaded refs, every member would have to be rebuilt and re-uploaded on every change. Keeping collections local means add, rm and drop are all local writes plus at most one etherchunk call per file, and the price paid to the postage batch is one upload per add.
The trade-off is that a collection cannot be shared as a single link. Each of its entries has its own /bzz/<root>/ URL, and that is the unit of sharing.
pnpm install
pnpm buildMIT