A structured, end-to-end explanation of rmapi’s Sync15 mirroring and filetree model.
This document is a structured, end-to-end explanation of how rmapi models
reMarkable data, how the Sync15 layer is mirrored locally, and how the
filetree abstraction is built and used by commands like ls, find,
stat, put, and rm.
The goal is to make the internal model understandable enough to implement new features or debug unexpected behavior without re-reading the code every time.
rmapi is built as a few layers that transform server state into a local, queryable tree:
Data flow looks like this:
remote reMarkable API
-> Sync15 mirror (hash tree + blob storage)
-> DocumentsFileTree (rmapi/filetree)
-> commands (ls/find/stat/put/...)
rmapi does not maintain a full relational database. The filetree is rebuilt from the Sync15 mirror and kept in memory.
rmapi models each entry (document or folder) as a model.Document and wraps
it in a model.Node inside a filetree.
The Document is a compact record of the fields rmapi cares about for most
operations:
type Document struct {
ID string
Name string
Version int
ModifiedClient string
Type string
CurrentPage int
Parent string
}
Important points:
Type is one of:
CollectionType (directory)DocumentType (file)TemplateType (template file)ModifiedClient is a string timestamp; Node.LastModified() parses it
into a time.Time.Document even
though they exist in metadata files. That data is not exposed via the
public API and is not in the filetree model.Node wraps a Document and adds tree structure:
type Node struct {
Document *Document
Children map[string]*Node
Parent *Node
}
Key helpers:
IsRoot() - ID == ""IsDirectory() - Type == CollectionTypeIsFile() - not a directoryFindByName(name) - exact match in childrenFindByPattern(pattern) - glob match on child names, case-insensitiveSync15 is the API and storage model used by the reMarkable cloud service. rmapi implements a client that:
Creating the API context does the heavy lifting:
rmapi/api/api.go defines the public ApiCtx interface.rmapi/api/sync15/apictx.go implements the Sync15 version.The sequence:
CreateCtx(http) loads a cached hash tree.Mirror(...) fetches updates from the remote storage.DocumentsFileTree(...) builds the filetree from the mirrored hash tree.This means CreateCtx(...) already performs a refresh-like sync.
The Sync15 tree stores documents as "blob docs". Each doc holds file entries
and a metadata file. In rmapi/api/sync15/blobdoc.go, ReadMetadata(...)
loads and parses the metadata file, then ToDocument() converts it into the
simple Document struct used by the filetree.
Important: this conversion discards extra metadata fields (pinned, deleted,
synced). Those values exist in archive.MetadataFile but are not surfaced in
the filetree model.
ApiCtx.Refresh() re-mirrors the hash tree and rebuilds the filetree. Commands
that read the filetree typically call CreateCtx(...) and then operate on the
freshly built tree.
The filetree is a lightweight representation of the document hierarchy.
Steps:
DocumentsFileTree(...) (in sync15 tree code) creates a FileTreeCtx.FileTreeCtx.AddDocument(...).pendingParent until they arrive.FileTreeCtx creates a root node with ID "" and a special trash node with
ID TrashID.
The root node is the anchor for path resolution and traversal. The trash node is attached as a child of root.
AddDocument(...) inserts a node into the idToNode map and then:
pendingParent until the parent arrives.FinishAdd() connects any remaining pending children to the root.
NodeByPath(path, current) is the "cd and stat" path resolver.
Rules:
. and .. are supported.NodesByPath(path, current, ignoreTrailingSlash) supports glob patterns on
the final path segment only.
Example:
/Books/*.pdf matches PDFs inside /Books./Books/*/notes does not glob at intermediate segments.Matching uses filepath.Match on lowercased names.
Pseudocode (simplified):
nodesByPath(path, current):
entries = split(path, "/")
for each entry in entries:
if entry is "." or "":
continue
if entry is "..":
current = current.Parent or root
continue
if entry is last:
return current.FindByPattern(entry) # glob, case-insensitive
else:
current = current.FindByName(entry) # exact match
WalkTree(start, visitor) traverses children recursively.
Important detail: children are stored in a map, so traversal order is not
deterministic. If you need stable output, collect matches and sort them.
Pseudocode (simplified):
WalkTree(node, visitor):
doWalk(node, [], visitor)
doWalk(node, path, visitor):
if visitor.Visit(node, path) == true:
return STOP
newPath = path + [node.Name()]
for child in node.Children: # map iteration, not stable order
if doWalk(child, newPath, visitor) == STOP:
return STOP
return CONTINUE
NodesByPath(...) to resolve patterns.WalkTree(...) to traverse from a start node.Pseudocode (simplified):
find(start, pattern, compact):
re = compile(pattern) if pattern else nil
WalkTree(start, visitor):
entry = formatEntry(node, compact) # includes [d]/[f] prefix if not compact
if re is nil or re matches entry:
print entry
return CONTINUE
NodeByPath(...) and prints the Document fields for one entry.WalkTree is map order and is not stable.ModifiedClient parsing can fail; handle errors if you
rely on timestamps.This diagram shows how rmapi moves from auth to a populated filetree, and how commands sit on top.
flowchart TD
A["User runs rmapi/remarquee command"] --> B["AuthHttpCtx (tokens)"]
B --> C["CreateApiCtx (sync15)"]
C --> D["Load cached hash tree"]
D --> E["Mirror remote hash tree + blobs"]
E --> F["DocumentsFileTree (build nodes)"]
F --> G["Filetree (Node graph)"]
G --> H["Command layer (ls/find/stat/put/rm)"]
The hash tree cache is a local JSON snapshot used to avoid full re-syncs on every command run.
os.UserCacheDir()/rmapi/tree.cache.getCachedTreePath() creates the cache directory if missing.cacheVersion = 3); mismatches trigger a resync.See rmapi/api/sync15/common.go.
CreateCtx(...) always loads the cache and then calls Mirror(...).ApiCtx.Refresh() explicitly re-mirrors and rebuilds the filetree.put, rm, mv, etc.) call Sync(...), which can re-mirror on
generation conflict and then saves the cache.See rmapi/api/sync15/apictx.go.
Mirror(...) always fetches the root index hash and generation. If the hash
matches the cached tree, it returns quickly without downloading per-document
indexes. If the hash differs, it downloads the root index and then pulls
doc-level indexes and metadata for new or changed documents. This is a global
sync, not directory-by-directory traversal.
See rmapi/api/sync15/tree.go and rmapi/api/sync15/blobdoc.go.
Node graph representing the folder/document
hierarchy.rmapi/api/api.go - public ApiCtx interface.rmapi/api/sync15/apictx.go - Sync15 ApiCtx implementation and mirror flow.rmapi/api/sync15/blobdoc.go - metadata parsing and Document conversion.rmapi/api/sync15/tree.go - hash tree and filetree construction helpers.rmapi/filetree/filetree.go - FileTreeCtx and path resolution.rmapi/filetree/treeutil.go - tree traversal utilities.rmapi/model/document.go - Document structure and types.rmapi/model/node.go - Node structure and helpers.rmapi/archive/file.go - metadata fields that are not surfaced in Document.