folder.v

4.2 KB · raw

1//! The folder backend: a remote that is just a directory.2//!3//! Every sync test in the project runs against this backend, because it needs4//! no network and no account. It is also a real backend in its own right — a5//! sync folder on an external disk or a NAS is a perfectly good remote.6//!7//! Publish is a hard link, which fails when the name already exists: no partial8//! file and no overwrite is ever visible to a reader.910module remote1112import crypto.blake313import time14import os1516pub struct Folder {17pub:18	base string19}2021// new_folder points a backend at a directory, creating it when missing.22pub fn new_folder(base string) !Folder {23	os.mkdir_all(base, os.MkdirParams{})!24	return Folder{25		base: base26	}27}2829pub fn (mut f Folder) get(key string) !Object {30	path := f.path_of(key)31	if !os.is_file(path) {32		return Object{}33	}34	body := os.read_bytes(path)!35	return Object{36		found: true37		body:  body38		tag:   tag_of(body)39	}40}4142pub fn (mut f Folder) list(prefix string) ![]string {43	dir := os.join_path(f.base, prefix)44	if !os.is_dir(dir) {45		return []string{}46	}47	mut keys := []string{}48	mut walk := [prefix]49	for walk.len > 0 {50		relative := walk.pop()51		for name in os.ls(f.path_of(relative))! {52			child := if relative == '' { name } else { '${relative}/${name}' }53			if os.is_dir(f.path_of(child)) {54				walk << child55			} else if !is_scratch(name) {56				keys << child57			}58		}59	}60	keys.sort()61	return keys62}6364// put_if_absent writes a temp file in the target directory and hard-links it65// into place. `link` fails when the name is taken, so a racing writer loses66// cleanly instead of overwriting.67pub fn (mut f Folder) put_if_absent(key string, body []u8) !bool {68	path := f.path_of(key)69	dir := os.dir(path)70	os.mkdir_all(dir, os.MkdirParams{})!71	temporary := '${path}.tmp-${os.getpid()}'72	os.write_file_array(temporary, body)!73	defer {74		if os.exists(temporary) {75			os.rm(temporary) or {}76		}77	}78	os.link(temporary, path) or {79		if os.exists(path) {80			return false81		}82		return err83	}84	return true85}8687// put_if_match compares the current content's tag to the one the caller read88// before writing. A lock file taken with an exclusive create serialises the89// compare-and-swap, so two machines cannot both believe they won.90pub fn (mut f Folder) put_if_match(key string, body []u8, tag string) !bool {91	path := f.path_of(key)92	os.mkdir_all(os.dir(path), os.MkdirParams{})!93	guard := '${path}.lock'94	if !take_lock(guard)! {95		clear_stale_lock(guard)96		if !take_lock(guard)! {97			return false98		}99	}100	defer {101		release_lock(guard)102	}103	if tag != '' {104		current := f.get(key)!105		if !current.found || current.tag != tag {106			return false107		}108	} else if f.get(key)!.found {109		return false110	}111	temporary := '${path}.tmp-${os.getpid()}'112	os.write_file_array(temporary, body)!113	os.mv(temporary, path, os.MvParams{})!114	return true115}116117// take_lock claims a lock path by hard-linking a fresh file into place. `link`118// fails when the name exists, so exactly one writer can hold it.119fn take_lock(path string) !bool {120	fresh := '${path}.${os.getpid()}.${time.now().unix_nano()}'121	os.write_file(fresh, '${os.getpid()}')!122	defer {123		os.rm(fresh) or {}124	}125	os.link(fresh, path) or { return false }126	return true127}128129// is_scratch recognises the temp and lock files this backend writes beside a130// key while publishing it. Keys themselves never contain a dot.131fn is_scratch(name string) bool {132	return name.contains('.tmp-') || name.contains('.lock')133}134135fn release_lock(path string) {136	os.rm(path) or {}137}138139// clear_stale_lock removes a lock left behind by a writer that died mid-swap.140// A lock a minute old is not a swap in progress; it is debris, and leaving it141// would block this remote for ever.142fn clear_stale_lock(path string) {143	info := os.stat(path) or { return }144	if time.now().unix() - info.mtime > 60 {145		os.rm(path) or {}146	}147}148149pub fn (mut f Folder) delete(key string) ! {150	path := f.path_of(key)151	if os.exists(path) {152		os.rm(path)!153	}154}155156fn (f &Folder) path_of(key string) string {157	return os.join_path(f.base, key)158}159160// tag_of is the version tag of a folder object: the hash of its bytes. A161// reader can compute it without asking anyone, which is what the lock-free162// compare-and-swap needs.163pub fn tag_of(body []u8) string {164	return blake3.sum256(body).hex()165}