//! The folder backend: a remote that is just a directory. //! //! Every sync test in the project runs against this backend, because it needs //! no network and no account. It is also a real backend in its own right — a //! sync folder on an external disk or a NAS is a perfectly good remote. //! //! Publish is a hard link, which fails when the name already exists: no partial //! file and no overwrite is ever visible to a reader. module remote import crypto.blake3 import time import os pub struct Folder { pub: base string } // new_folder points a backend at a directory, creating it when missing. pub fn new_folder(base string) !Folder { os.mkdir_all(base, os.MkdirParams{})! return Folder{ base: base } } pub fn (mut f Folder) get(key string) !Object { path := f.path_of(key) if !os.is_file(path) { return Object{} } body := os.read_bytes(path)! return Object{ found: true body: body tag: tag_of(body) } } pub fn (mut f Folder) list(prefix string) ![]string { dir := os.join_path(f.base, prefix) if !os.is_dir(dir) { return []string{} } mut keys := []string{} mut walk := [prefix] for walk.len > 0 { relative := walk.pop() for name in os.ls(f.path_of(relative))! { child := if relative == '' { name } else { '${relative}/${name}' } if os.is_dir(f.path_of(child)) { walk << child } else if !is_scratch(name) { keys << child } } } keys.sort() return keys } // put_if_absent writes a temp file in the target directory and hard-links it // into place. `link` fails when the name is taken, so a racing writer loses // cleanly instead of overwriting. pub fn (mut f Folder) put_if_absent(key string, body []u8) !bool { path := f.path_of(key) dir := os.dir(path) os.mkdir_all(dir, os.MkdirParams{})! temporary := '${path}.tmp-${os.getpid()}' os.write_file_array(temporary, body)! defer { if os.exists(temporary) { os.rm(temporary) or {} } } os.link(temporary, path) or { if os.exists(path) { return false } return err } return true } // put_if_match compares the current content's tag to the one the caller read // before writing. A lock file taken with an exclusive create serialises the // compare-and-swap, so two machines cannot both believe they won. pub fn (mut f Folder) put_if_match(key string, body []u8, tag string) !bool { path := f.path_of(key) os.mkdir_all(os.dir(path), os.MkdirParams{})! guard := '${path}.lock' if !take_lock(guard)! { clear_stale_lock(guard) if !take_lock(guard)! { return false } } defer { release_lock(guard) } if tag != '' { current := f.get(key)! if !current.found || current.tag != tag { return false } } else if f.get(key)!.found { return false } temporary := '${path}.tmp-${os.getpid()}' os.write_file_array(temporary, body)! // V's rename, like mv(1), would move the file INTO a directory there. if os.is_dir(path) { os.rm(temporary) or {} return error('${key} is a directory on the remote') } os.rename(temporary, path)! return true } // take_lock claims a lock path by hard-linking a fresh file into place. `link` // fails when the name exists, so exactly one writer can hold it. fn take_lock(path string) !bool { fresh := '${path}.${os.getpid()}.${time.now().unix_nano()}' os.write_file(fresh, '${os.getpid()}')! defer { os.rm(fresh) or {} } os.link(fresh, path) or { return false } return true } // is_scratch recognises the temp and lock files this backend writes beside a // key while publishing it. Keys themselves never contain a dot. fn is_scratch(name string) bool { return name.contains('.tmp-') || name.contains('.lock') } fn release_lock(path string) { os.rm(path) or {} } // clear_stale_lock removes a lock left behind by a writer that died mid-swap. // A lock a minute old is not a swap in progress; it is debris, and leaving it // would block this remote for ever. fn clear_stale_lock(path string) { info := os.stat(path) or { return } if time.now().unix() - info.mtime > 60 { os.rm(path) or {} } } pub fn (mut f Folder) delete(key string) ! { path := f.path_of(key) if os.exists(path) { os.rm(path)! } } fn (f &Folder) path_of(key string) string { return os.join_path(f.base, key) } // tag_of is the version tag of a folder object: the hash of its bytes. A // reader can compute it without asking anyone, which is what the lock-free // compare-and-swap needs. pub fn tag_of(body []u8) string { return blake3.sum256(body).hex() }