Keep a mirror in sync with Dropbox's folder cursors: take a cursor,
read every change since it (list_folder/continue), store the new
cursor. Deltas are metadata entries: files and folders that were
added or changed, and deleted entries for removals.
Sync loop
- Once:
cursor ::sync/get-latest-cursor("/Docs", {recursive: true})("changes from now on"), orstart-syncfor a full snapshot plus its cursor. Store the cursor. - Each run (schedule, webhook, or after
longpollsays so):set ::sync/sync-changes(cursor, {path: "/Docs", recursive: true}), applychanged-files(set)anddeleted-paths(set), storeset.cursor.
Reset. A cursor can be invalidated (409 reset, e.g. after a
large server-side change). When opts.path is given, sync-changes
then relists the folder from scratch and returns reset: true with
the full listing in entries: replace your mirror with it rather
than merging. Without opts.path the reset Err is returned.
Longpoll. longpoll holds one HTTP request open until something
changes or the timeout passes; see its doc. Webhooks (see
::dropbox::webhooks) are the push alternative.
Scopes. files.metadata.read.
Values
MAX_LONGPOLL_TIMEOUT
MAX_LONGPOLL_TIMEOUT: Int 480
MIN_LONGPOLL_TIMEOUT
MIN_LONGPOLL_TIMEOUT: Int 30
SYNC_LOCAL_KEYS
SYNC_LOCAL_KEYS: Vec [
"path",
"cursor"
]
Functions
changed-files
fn (changes: Any): Vec
The entries in a ChangeSet (or a Vec of entries) that exist (files and folders, not deletions), as DocFiles, the latest entry per path. A path deleted later in the same set is left out.
deleted-paths
fn (changes: Any): Vec
The lowercased paths a ChangeSet (or Vec of entries) deleted, de-duplicated. Deleting a folder deletes everything under it (Dropbox reports only the folder). A path re-created later in the same set is not included.
get-latest-cursor
fn (path: Str?): Str
fn (path: Str?, opts: Map): Str
POST files/list_folder/get_latest_cursor - a cursor for "changes to path from now on" (a Str), without listing. Options as for ::dropbox::files/list-folder (recursive, include_deleted, ...); use the same ones for every sync of this cursor.
is-under
fn (path: Str, folder: Str): Bool
True when lowercased path is folder or inside it (pure): use it to drop a deleted folder's descendants from a mirror.
longpoll
fn (cursor: Str): Map
fn (cursor: Str, timeout: Int?): Map
fn (cursor: Str, timeout: Int?, opts: Map): Map
POST files/list_folder/longpoll (notify host, no authentication) -
wait until something under cursor changes or timeout seconds pass
(30..480, default 30; Dropbox adds up to 90 s of jitter). Returns
{changes, backoff?}: when changes is true, call sync-changes;
when backoff is set, wait that many seconds before the next
longpoll (schedule it, do not spin).
This is a single blocking HTTP call, not a sleep, but it holds the
run open for up to timeout + 90 seconds. The runtime's HTTP timeout
(120 s by default) cuts longer waits off with a transport Err, so
keep timeout at 30 unless your HTTP timeout is raised, and treat a
timeout as "no news". For many accounts, webhooks scale better.
poll ::dropbox::sync/longpoll(saved-cursor)
if(poll.changes, ::dropbox::sync/sync-changes(saved-cursor), null)
longpoll-timeout
fn (timeout: Int?): Int
Clamp a longpoll timeout to Dropbox's 30..480 seconds (pure; null gives 30).
start-sync
fn (path: Str?): ChangeSet
fn (path: Str?, opts: Map): ChangeSet
A full listing of path and the cursor to sync from afterwards, as a ChangeSet (reset: true, since it is a snapshot). Options as for ::dropbox::files/list-folder (recursive: true for the tree).
sync-changes
fn (cursor: Str): ChangeSet
fn (cursor: Str, opts: Map): ChangeSet
Read every page of changes since cursor and return a ChangeSet
with the cursor to use next time. On a 409 reset, relists
opts.path (with the other list-folder options in opts) and
returns reset: true; without opts.path the Err is returned.
::sync ::dropbox::sync
set ::sync/sync-changes(saved-cursor, {path: "/Docs", recursive: true})
upserts ::sync/changed-files(set) // DocFiles to (re)index
deletes ::sync/deleted-paths(set) // lowercased paths to drop
saved-cursor set.cursor
sync-loop
fn (page: Result, opts: Map, acc: Vec, pages: Int, reset: Bool): ChangeSet
Types
ChangeSet
ChangeSet type {
entries: Vec,
cursor: Str,
pages: Int,
reset: Bool
}
Everything that changed since a cursor.
entries- every metadata entry in order (files, folders, deleted)cursor- store it and pass it to the next syncpages- how many pages were readreset- true when the old cursor was invalidated andentriesis a fresh full listing (replace the mirror; do not merge)