provcat.cache
The cache keeps a readable copy of remote objects, so a folder of it can be mounted into a container. The layout is::
<root>/<store>/<key...> the cached files
<root>/.provcat/meta/<hash>.json the version of one cached file
<root>/.provcat/done/<hash>.json a folder that was fetched whole
<root>/.provcat/refs/<hash> a moving name pinned to a version
<root>/.provcat/tmp/ files and folders being written
<root>/.provcat/locks/<hash>.lock one lock for each key being written
<root>/.provcat/snapshots/<kind>/<sha>/<name> a read-only copy, named by its contentA store decides the key. GitHub keys hold the commit, so they never change. S3 keys are bucket and object key, and the meta file holds the ETag.
The records under .provcat are named by a hash of store and key, so two keys
can never share or block a record. A meta record also holds the size, the
modification time, the inode and the SHA-256 of the file it describes. If the
time or the inode differs, as after the cache was copied, the file is hashed
again and kept if the hash matches. On Linux a time’s tick can be a few
milliseconds, so the inode tells apart two files of one size from one tick.
Writes of one key take its lock, so the last record describes the last file,
and a read that saw the record change while it looked returns nothing. If the
size differs, or the bytes differ from the hash, the file is fetched again. A
rewrite in place that keeps the size, the time and the inode, as
rsync --inplace -t makes, is not noticed. The locks need a filesystem with
flock, such as a local disk. On a share without it, every write fails.
Two names that differ only in case or in Unicode form land in one file on macOS and Windows. The stores refuse a folder that holds such names.
Every write goes to a temporary name under .provcat/tmp and then is renamed, so a reader never sees half a file.
A fetch can rewrite a cached file in place when a branch moves, so a cached
path is not safe to mount while something reads it. A snapshot, made by
Catalog.snapshot, copies a file or a folder into a folder named by the
SHA-256 of its content, and makes the copy read-only. Nothing in provcat writes to a snapshot again, so a mount of
one never changes.
class LocalCache
LocalCache(root: str | Path | None = None)The local cache of fetched files and folders, their versions, the pinned refs and the snapshots.
A store driver calls it to find and to write what it fetches. Each key is a list of path segments under the store’s name.
| Parameter | Type | Default | Description |
|---|---|---|---|
root |
str | Path | None | None |
The cache folder; None takes PROVCAT_CACHE, then XDG_CACHE_HOME/provcat, then ~/.cache/provcat |
LocalCache.path
LocalCache.path(store: str, key: Iterable[str]) -> PathReturn the cache path of a key, whether or not anything is there.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
Iterable[str] | required | The path segments under the store |
| returns | Path | Where the file or folder of the key lives in the cache; UriError for a bad segment |
LocalCache.get
LocalCache.get(store: str, key: list[str], version: str | None) -> Path | NoneReturn the cached file if it is there, unchanged, and has this version.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments under the store |
version |
str | None | required | The version wanted; None accepts any cached version, as offline mode does |
| returns | Path | None | The cached file; None when it is missing, changed, or of another version |
LocalCache.put
LocalCache.put(store: str, key: list[str], fill: Callable[[Path], str | None], version: str | None = None) -> PathWrite a file with fill, then record its version.
The version that fill returns wins over the version argument, so the record always describes the bytes on disk. With neither, StoreError.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments under the store |
fill |
Callable[[Path], str | None] | required | Writes the file at the temporary path it is given, and may return its version |
version |
str | None | None |
The version to record, when fill returns none |
| returns | Path | The cached file |
LocalCache.folder_done
LocalCache.folder_done(store: str, key: list[str], version: str | None) -> Path | NoneFind a folder that was fetched whole.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments of the folder |
version |
str | None | required | The version wanted; None accepts any |
| returns | Path | None | The cached folder when it was fetched whole at that version; else None |
LocalCache.covered
LocalCache.covered(store: str, key: list[str], version: str, min_len: int) -> Path | NoneReturn the cached file or folder if a folder at or above key was fetched whole.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments of the file or folder |
version |
str | required | The version the whole folder must have been fetched at |
min_len |
int | required | The shortest key prefix to look at, such as 1 to stop at the bucket |
| returns | Path | None | The cached file or folder; None when no such folder covers it |
LocalCache.mark_folder
LocalCache.mark_folder(store: str, key: list[str], version: str) -> NoneRecord that a folder was fetched whole at a version.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments of the folder |
version |
str | required | The version it was fetched at |
LocalCache.unmark_folder
LocalCache.unmark_folder(store: str, key: list[str]) -> NoneForget that a folder was fetched whole.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments of the folder |
LocalCache.prune
LocalCache.prune(store: str, key: list[str], keep: set[tuple[str, ...]]) -> NoneDelete cached files under key whose relative keys are not in keep.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments of the folder |
keep |
set[tuple[str, …]] | required | The paths under the folder, as tuples of segments, to keep |
LocalCache.lock
LocalCache.lock(store: str, key: list[str]) -> Iterator[None]Hold an exclusive lock for one key, across threads and processes.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments to lock |
| returns | Iterator[None] | A context manager that holds the lock while its block runs |
LocalCache.install_folder
LocalCache.install_folder(store: str, key: list[str], version: str, fill: Callable[[Path], None]) -> PathBuild a folder with fill(tmp_dir), then move it into place whole.
Installs of one key run one at a time. If another thread or process installed the same version first, its folder stays.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
key |
list[str] | required | The path segments of the folder |
version |
str | required | The version of the folder, recorded once it is in place |
fill |
Callable[[Path], None] | required | Writes the folder’s contents into the temporary folder it is given |
| returns | Path | The cached folder |
LocalCache.get_ref
LocalCache.get_ref(store: str, name: list[str]) -> str | NoneRead the version that a moving name was last pinned to, as offline mode does.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
name |
list[str] | required | The moving name, as segments, such as a repository, a branch and a time |
| returns | str | None | The version it was pinned to; None when it was never pinned |
LocalCache.set_ref
LocalCache.set_ref(store: str, name: list[str], value: str) -> NoneRecord the version that a moving name points at.
| Parameter | Type | Default | Description |
|---|---|---|---|
store |
str | required | The store’s name |
name |
list[str] | required | The moving name, as segments |
value |
str | required | The fixed version it points at now |
tree_digest
tree_digest(p: Path) -> strThe SHA-256 of a file, or of a folder.
A folder's digest covers every entry under it, folders included, so an
empty folder counts. Each entry is ``F�path�sha256for a file or D�path�
for a folder, in sorted order, after the line provcat-tree-v1``. A file name holds no NUL, so the form is unambiguous.
A file and a folder can still have equal digests, because a file can hold
any bytes; snapshots keep them apart by kind. A link or any other special
file is refused, so the digest always describes plain bytes.
| Parameter | Type | Default | Description |
|---|---|---|---|
p |
Path | required | A file or a folder |
| returns | str | The hex SHA-256; StoreError for a link or a special file |