53def _object_remote(profile: dict, archive_id: str, *parts: str) -> str:
55 @brief Return the remote path for one immutable archive object.
56 @param[in] profile Value supplied through the `profile` argument.
57 @param[in] archive_id Value supplied through the `archive_id` argument.
58 @param[in] parts Value supplied through the `parts` argument.
59 @return Result produced by this operation.
61 return _remote_join(profile[
"remote"], REMOTE_OBJECTS_DIRECTORY, archive_id, *parts)
74def _chunk_remote_path(profile: dict, archive_id: str, chunk: dict) -> str:
76 @brief Resolve where one manifest chunk's payload lives on the remote.
78 @details Every chunk this schema writes is blob-addressed, resolved out of the
79 shared content-addressed store. `_load_remote_manifest` already refuses
80 any manifest whose schema version does not match the current one, so a
81 non-blob chunk cannot reach this function from a manifest this build
83 @param[in] profile Active storage profile.
84 @param[in] archive_id Owning archive id.
85 @param[in] chunk Manifest chunk entry.
86 @return Remote path of the chunk payload.
89 return _blob_remote(profile, chunk[
"sha256"])
90 return _object_remote(profile, archive_id,
"chunks", chunk[
"name"])
144def _upload_verified(local_path: str, remote_path: str) -> dict:
146 @brief Upload one file, then verify its remote SHA-256.
147 @param[in] local_path Value supplied through the `local_path` argument.
148 @param[in] remote_path Value supplied through the `remote_path` argument.
149 @return Result produced by this operation.
151 local_digest = _sha256_file(local_path)
152 _run_rclone([
"copyto", local_path, remote_path])
153 remote_digest = _remote_sha256(remote_path)
154 if remote_digest != local_digest:
156 f
"Remote checksum mismatch after upload: {remote_path} "
157 f
"(local {local_digest}, remote {remote_digest})."
159 return {
"sha256": local_digest,
"stored_bytes": os.path.getsize(local_path)}
180def _load_remote_manifest(profile: dict, archive_id: str, require_complete: bool =
True) -> dict:
182 @brief Fetch and validate one versioned remote storage manifest.
183 @param[in] profile Value supplied through the `profile` argument.
184 @param[in] archive_id Value supplied through the `archive_id` argument.
185 @param[in] require_complete Value supplied through the `require_complete` argument.
186 @return Result produced by this operation.
188 if not ARCHIVE_ID_PATTERN.fullmatch(str(archive_id)):
189 raise StorageError(f
"Invalid archive ID: {archive_id!r}.")
190 manifest_bytes = _read_remote_bytes(_object_remote(profile, archive_id, REMOTE_MANIFEST_FILENAME))
192 complete = _read_remote_bytes(_object_remote(profile, archive_id, REMOTE_COMPLETE_FILENAME))
193 recorded = complete.decode(
"ascii",
"replace").strip().lower()
194 actual = hashlib.sha256(manifest_bytes).hexdigest()
195 if recorded != actual:
196 raise StorageError(f
"Archive {archive_id} has no valid completion marker.")
198 manifest = json.loads(manifest_bytes.decode(
"utf-8"))
199 except (UnicodeDecodeError, ValueError)
as exc:
200 raise StorageError(f
"Archive {archive_id} has an invalid manifest.")
from exc
201 if not isinstance(manifest, dict)
or manifest.get(
"storage_schema_version") != STORAGE_SCHEMA_VERSION:
203 f
"Archive {archive_id} uses unsupported storage schema "
204 f
"{manifest.get('storage_schema_version') if isinstance(manifest, dict) else 'unknown'}."