tensorfs 0.3.37__tar.gz → 0.3.38__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {tensorfs-0.3.37 → tensorfs-0.3.38}/Cargo.lock +32 -2
  2. {tensorfs-0.3.37 → tensorfs-0.3.38}/Cargo.toml +1 -1
  3. {tensorfs-0.3.37 → tensorfs-0.3.38}/PKG-INFO +1 -1
  4. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/Cargo.toml +1 -0
  5. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/repo_cache.rs +347 -62
  6. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/repo_cache.rs +88 -4
  7. {tensorfs-0.3.37 → tensorfs-0.3.38}/pyproject.toml +1 -1
  8. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/build.rs +0 -0
  9. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  10. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  11. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  12. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  13. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/b64.rs +0 -0
  14. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  15. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  16. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  17. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  18. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  19. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  20. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  21. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  22. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  23. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  24. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/canon.rs +0 -0
  25. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/capability.rs +0 -0
  26. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/catalog.rs +0 -0
  27. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/cbor.rs +0 -0
  28. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  29. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/corpus.rs +0 -0
  30. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  31. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  32. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  33. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  34. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  35. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  36. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived.rs +0 -0
  37. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/dtype.rs +0 -0
  38. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/durability.rs +0 -0
  39. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/err.rs +0 -0
  40. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/fetch.rs +0 -0
  41. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/fill.rs +0 -0
  42. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/fit.rs +0 -0
  43. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/gc.rs +0 -0
  44. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/header.rs +0 -0
  45. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/home.rs +0 -0
  46. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ids.rs +0 -0
  47. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  48. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  49. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  50. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  51. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  52. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  53. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  54. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  55. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  56. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  57. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/jcs.rs +0 -0
  58. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/lib.rs +0 -0
  59. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/limits.rs +0 -0
  60. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/machine.rs +0 -0
  61. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/manifest.rs +0 -0
  62. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/meta.rs +0 -0
  63. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/project.rs +0 -0
  64. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/providers.rs +0 -0
  65. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/read.rs +0 -0
  66. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/receipt.rs +0 -0
  67. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/reclaim.rs +0 -0
  68. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/registry.rs +0 -0
  69. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/relation.rs +0 -0
  70. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/repository.rs +0 -0
  71. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/sha256.rs +0 -0
  72. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  73. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/spec.rs +0 -0
  74. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/staging.rs +0 -0
  75. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/storage.rs +0 -0
  76. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/store.rs +0 -0
  77. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/api.rs +0 -0
  78. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/http.rs +0 -0
  79. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  80. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  81. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  82. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  83. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  84. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  85. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/push.rs +0 -0
  86. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  87. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  88. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/src/vectors.rs +0 -0
  89. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  90. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fit.rs +0 -0
  91. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  92. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  93. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  94. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  95. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  96. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  97. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  98. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  99. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/home.rs +0 -0
  100. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  101. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  102. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/liveness.rs +0 -0
  103. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  104. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  105. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  106. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  107. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  108. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  109. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  110. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  111. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  112. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  113. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  114. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/staging.rs +0 -0
  115. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  116. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  117. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/version.rs +0 -0
  118. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-core/tests/write_through.rs +0 -0
  119. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-py/Cargo.toml +0 -0
  120. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-py/src/docs.rs +0 -0
  121. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-py/src/errors.rs +0 -0
  122. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-py/src/lib.rs +0 -0
  123. {tensorfs-0.3.37 → tensorfs-0.3.38}/crates/tensorfs-py/src/store.rs +0 -0
  124. {tensorfs-0.3.37 → tensorfs-0.3.38}/python/tensorfs/__init__.py +0 -0
  125. {tensorfs-0.3.37 → tensorfs-0.3.38}/python/tensorfs/_ext.pyi +0 -0
  126. {tensorfs-0.3.37 → tensorfs-0.3.38}/python/tensorfs/errors.py +0 -0
  127. {tensorfs-0.3.37 → tensorfs-0.3.38}/python/tensorfs/py.typed +0 -0
@@ -48,6 +48,16 @@ version = "0.5.8"
48
48
  source = "registry+https://github.com/rust-lang/crates.io-index"
49
49
  checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
50
50
 
51
+ [[package]]
52
+ name = "errno"
53
+ version = "0.3.14"
54
+ source = "registry+https://github.com/rust-lang/crates.io-index"
55
+ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
56
+ dependencies = [
57
+ "libc",
58
+ "windows-sys",
59
+ ]
60
+
51
61
  [[package]]
52
62
  name = "fallible-iterator"
53
63
  version = "0.3.0"
@@ -128,6 +138,12 @@ dependencies = [
128
138
  "vcpkg",
129
139
  ]
130
140
 
141
+ [[package]]
142
+ name = "linux-raw-sys"
143
+ version = "0.12.1"
144
+ source = "registry+https://github.com/rust-lang/crates.io-index"
145
+ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
146
+
131
147
  [[package]]
132
148
  name = "minicbor"
133
149
  version = "2.3.0"
@@ -290,6 +306,19 @@ dependencies = [
290
306
  "smallvec",
291
307
  ]
292
308
 
309
+ [[package]]
310
+ name = "rustix"
311
+ version = "1.1.4"
312
+ source = "registry+https://github.com/rust-lang/crates.io-index"
313
+ checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
314
+ dependencies = [
315
+ "bitflags",
316
+ "errno",
317
+ "libc",
318
+ "linux-raw-sys",
319
+ "windows-sys",
320
+ ]
321
+
293
322
  [[package]]
294
323
  name = "rustls"
295
324
  version = "0.23.43"
@@ -392,19 +421,20 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
421
 
393
422
  [[package]]
394
423
  name = "tensorfs-core"
395
- version = "0.3.37"
424
+ version = "0.3.38"
396
425
  dependencies = [
397
426
  "fs2",
398
427
  "minicbor",
399
428
  "rcgen",
400
429
  "rusqlite",
430
+ "rustix",
401
431
  "rustls",
402
432
  "webpki-roots 0.26.11",
403
433
  ]
404
434
 
405
435
  [[package]]
406
436
  name = "tensorfs-py"
407
- version = "0.3.37"
437
+ version = "0.3.38"
408
438
  dependencies = [
409
439
  "pyo3",
410
440
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.37"
6
+ version = "0.3.38"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.37
3
+ Version: 0.3.38
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -11,6 +11,7 @@ path = "src/bin/tfs/main.rs"
11
11
  fs2 = "0.4"
12
12
  minicbor = { version = "2.3.0", default-features = false, features = ["alloc"] }
13
13
  rusqlite = { version = "0.32", features = ["bundled"] }
14
+ rustix = { version = "1.1.4", default-features = false, features = ["fs", "std"] }
14
15
  # tfs-049: the ONE transport (src/transport/). rustls is the one TLS stack, used only
15
16
  # there; scripts/fence.py refuses it anywhere else so a second mover cannot grow.
16
17
  rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
@@ -5,11 +5,15 @@
5
5
  //! the caller's exact digest and length while bytes move into an ordinary local Store.
6
6
  //! Cache failure is weather, never authority.
7
7
 
8
+ use std::ffi::OsStr;
8
9
  use std::fs::{self, File, OpenOptions};
9
10
  use std::io::{Read, Write};
10
- use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
11
+ use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
12
+
11
13
  use std::path::{Path, PathBuf};
12
14
 
15
+ use rustix::fs::{self as unix, AtFlags, Mode, OFlags};
16
+
13
17
  use crate::err::{Code, Result};
14
18
  use crate::ids::{hex64, Doc, ObjectRef};
15
19
  use crate::limits;
@@ -241,27 +245,49 @@ impl RepoObjectCache {
241
245
  progress: Option<&dyn store::Progress>,
242
246
  ) -> Result<CacheWrite> {
243
247
  let destination = self.path(kind, &want.sha256)?;
244
- if destination.exists() {
245
- return Ok(match verify_path(&destination, kind, want, progress) {
246
- Ok(()) => CacheWrite::Present,
247
- Err(_) => CacheWrite::Unavailable,
248
- });
249
- }
248
+ let directory = match cache_parent(&self.root, &destination) {
249
+ Ok(directory) => directory,
250
+ Err(_) => return Ok(CacheWrite::Unavailable),
251
+ };
252
+ let name = destination.file_name().expect("digest path has a filename");
253
+ let repair = match open_at(&directory, name) {
254
+ Ok(file) => {
255
+ let metadata = match file.metadata() {
256
+ Ok(metadata) if metadata.is_file() => metadata,
257
+ _ => return Ok(CacheWrite::Unavailable),
258
+ };
259
+ match verify_file(file, kind, want, progress) {
260
+ Ok(()) => return Ok(CacheWrite::Present),
261
+ Err(SoftFailure::Corrupt) => Some(metadata),
262
+ Err(_) => return Ok(CacheWrite::Unavailable),
263
+ }
264
+ }
265
+ Err(SoftFailure::Missing) => None,
266
+ Err(_) => return Ok(CacheWrite::Unavailable),
267
+ };
250
268
  let result = match kind {
251
269
  CacheKind::Blob => match source.open_verified(&want.sha256) {
252
270
  Ok(mut file) if file.len() == want.length => {
253
- publish_stream(&mut file, &destination, want, progress)
271
+ publish_stream(&mut file, &directory, name, want, progress, repair.as_ref())
254
272
  }
255
273
  Ok(_) | Err(_) => Err(SoftFailure::Unavailable),
256
274
  },
257
275
  CacheKind::Manifest => match source.read_manifest(want) {
258
- Ok(manifest) => publish_bytes(&manifest.canonical_bytes(), &destination, progress),
276
+ Ok(manifest) => publish_bytes(
277
+ &manifest.canonical_bytes(),
278
+ &directory,
279
+ name,
280
+ progress,
281
+ repair.as_ref(),
282
+ ),
259
283
  Err(_) => Err(SoftFailure::Unavailable),
260
284
  },
261
285
  };
262
286
  Ok(match result {
263
287
  Ok(true) => CacheWrite::Stored,
264
- Ok(false) => match verify_path(&destination, kind, want, progress) {
288
+ Ok(false) => match open_at(&directory, name)
289
+ .and_then(|file| verify_file(file, kind, want, progress))
290
+ {
265
291
  Ok(()) => CacheWrite::Present,
266
292
  Err(_) => CacheWrite::Unavailable,
267
293
  },
@@ -451,80 +477,181 @@ fn read_manifest(path: &Path, want: &ObjectRef) -> std::result::Result<Manifest,
451
477
  Manifest::parse(&bytes).map_err(|_| SoftFailure::Corrupt)
452
478
  }
453
479
 
480
+ #[cfg(test)]
454
481
  fn verify_path(
455
482
  path: &Path,
456
483
  kind: CacheKind,
457
484
  want: &ObjectRef,
458
485
  progress: Option<&dyn store::Progress>,
459
486
  ) -> std::result::Result<(), SoftFailure> {
487
+ verify_file(open_exact(path, want.length)?, kind, want, progress)
488
+ }
489
+
490
+ fn verify_file(
491
+ mut file: File,
492
+ kind: CacheKind,
493
+ want: &ObjectRef,
494
+ progress: Option<&dyn store::Progress>,
495
+ ) -> std::result::Result<(), SoftFailure> {
496
+ let metadata = file.metadata().map_err(|_| SoftFailure::Unavailable)?;
497
+ if !metadata.is_file() {
498
+ return Err(SoftFailure::Unavailable);
499
+ }
500
+ if metadata.len() != want.length {
501
+ return Err(SoftFailure::Corrupt);
502
+ }
460
503
  match kind {
461
- CacheKind::Blob => {
462
- let mut file = open_exact(path, want.length)?;
463
- let mut sink = std::io::sink();
464
- stream_exact(&mut file, &mut sink, want, progress)
504
+ CacheKind::Blob => stream_exact(&mut file, &mut std::io::sink(), want, progress),
505
+ CacheKind::Manifest => {
506
+ if want.length > Manifest::MAX_BYTES as u64 {
507
+ return Err(SoftFailure::Corrupt);
508
+ }
509
+ let mut bytes = Vec::with_capacity(want.length as usize);
510
+ file.take(want.length + 1)
511
+ .read_to_end(&mut bytes)
512
+ .map_err(|_| SoftFailure::Unavailable)?;
513
+ if bytes.len() as u64 != want.length || sha256::hex_digest(&bytes) != want.sha256 {
514
+ return Err(SoftFailure::Corrupt);
515
+ }
516
+ Manifest::parse(&bytes)
517
+ .map(|_| ())
518
+ .map_err(|_| SoftFailure::Corrupt)
465
519
  }
466
- CacheKind::Manifest => read_manifest(path, want).map(|_| ()),
467
520
  }
468
521
  }
469
522
 
523
+ // The configured root is the caller's location authority. Pin it once, then refuse
524
+ // symlinks in every cache-owned fanout directory and publish relative to the pinned fd.
525
+ fn cache_parent(root: &Path, destination: &Path) -> std::result::Result<File, SoftFailure> {
526
+ fs::create_dir_all(root).map_err(|_| SoftFailure::Unavailable)?;
527
+ let mut directory = File::from(
528
+ unix::open(
529
+ root,
530
+ OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
531
+ Mode::empty(),
532
+ )
533
+ .map_err(|_| SoftFailure::Unavailable)?,
534
+ );
535
+ let relative = destination
536
+ .parent()
537
+ .and_then(|parent| parent.strip_prefix(root).ok())
538
+ .ok_or(SoftFailure::Unavailable)?;
539
+ for component in relative.components() {
540
+ let std::path::Component::Normal(name) = component else {
541
+ return Err(SoftFailure::Unavailable);
542
+ };
543
+ match unix::mkdirat(&directory, name, Mode::from_raw_mode(0o755)) {
544
+ Ok(()) | Err(rustix::io::Errno::EXIST) => {}
545
+ Err(_) => return Err(SoftFailure::Unavailable),
546
+ }
547
+ directory = File::from(
548
+ unix::openat(
549
+ &directory,
550
+ name,
551
+ OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
552
+ Mode::empty(),
553
+ )
554
+ .map_err(|_| SoftFailure::Unavailable)?,
555
+ );
556
+ }
557
+ Ok(directory)
558
+ }
559
+
560
+ fn open_at(directory: &File, name: &OsStr) -> std::result::Result<File, SoftFailure> {
561
+ unix::openat(
562
+ directory,
563
+ name,
564
+ OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
565
+ Mode::empty(),
566
+ )
567
+ .map(File::from)
568
+ .map_err(|error| {
569
+ if error == rustix::io::Errno::NOENT {
570
+ SoftFailure::Missing
571
+ } else {
572
+ SoftFailure::Unavailable
573
+ }
574
+ })
575
+ }
576
+
470
577
  fn publish_bytes(
471
578
  bytes: &[u8],
472
- destination: &Path,
579
+ directory: &File,
580
+ name: &OsStr,
473
581
  progress: Option<&dyn store::Progress>,
582
+ repair: Option<&fs::Metadata>,
474
583
  ) -> std::result::Result<bool, SoftFailure> {
475
- let mut source = bytes;
476
- let want = ObjectRef::of(bytes);
477
- publish_stream(&mut source, destination, &want, progress)
584
+ publish_stream(
585
+ &mut &bytes[..],
586
+ directory,
587
+ name,
588
+ &ObjectRef::of(bytes),
589
+ progress,
590
+ repair,
591
+ )
478
592
  }
479
593
 
480
594
  fn publish_stream(
481
595
  source: &mut impl Read,
482
- destination: &Path,
596
+ directory: &File,
597
+ name: &OsStr,
483
598
  want: &ObjectRef,
484
599
  progress: Option<&dyn store::Progress>,
600
+ repair: Option<&fs::Metadata>,
485
601
  ) -> std::result::Result<bool, SoftFailure> {
486
- let Some(directory) = destination.parent() else {
487
- return Err(SoftFailure::Unavailable);
488
- };
489
- fs::create_dir_all(directory).map_err(|_| SoftFailure::Unavailable)?;
490
- let name = destination
491
- .file_name()
492
- .and_then(|value| value.to_str())
493
- .unwrap_or("object");
494
- let temporary = directory.join(format!(
495
- ".{name}.{}.{}.tmp",
602
+ let temporary = format!(
603
+ ".{}.{}.{}.tmp",
604
+ name.to_string_lossy(),
496
605
  std::process::id(),
497
606
  store::now_nanos_unique()
498
- ));
499
- let mut output = OpenOptions::new()
500
- .write(true)
501
- .create_new(true)
502
- .mode(0o600)
503
- .open(&temporary)
504
- .map_err(|_| SoftFailure::Unavailable)?;
505
- if let Err(error) = stream_exact(source, &mut output, want, progress) {
506
- let _ = fs::remove_file(&temporary);
507
- return Err(error);
508
- }
509
- if output.sync_all().is_err()
510
- || fs::set_permissions(&temporary, fs::Permissions::from_mode(0o444)).is_err()
511
- {
512
- let _ = fs::remove_file(&temporary);
513
- return Err(SoftFailure::Unavailable);
514
- }
515
- let admitted = match fs::hard_link(&temporary, destination) {
516
- Ok(()) => true,
517
- Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => false,
518
- Err(_) => {
519
- let _ = fs::remove_file(&temporary);
520
- return Err(SoftFailure::Unavailable);
607
+ );
608
+ let mut output = File::from(
609
+ unix::openat(
610
+ directory,
611
+ temporary.as_str(),
612
+ OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
613
+ Mode::from_raw_mode(0o600),
614
+ )
615
+ .map_err(|_| SoftFailure::Unavailable)?,
616
+ );
617
+ let publish = (|| {
618
+ stream_exact(source, &mut output, want, progress)?;
619
+ output.sync_all().map_err(|_| SoftFailure::Unavailable)?;
620
+ output
621
+ .set_permissions(fs::Permissions::from_mode(0o444))
622
+ .map_err(|_| SoftFailure::Unavailable)?;
623
+ let admitted = if let Some(previous) = repair {
624
+ let current = unix::statat(directory, name, AtFlags::SYMLINK_NOFOLLOW)
625
+ .map_err(|_| SoftFailure::Unavailable)?;
626
+ if current.st_dev != previous.dev() || current.st_ino != previous.ino() {
627
+ false
628
+ } else {
629
+ // Both racing successful repairs publish exactly the same verified object.
630
+ // renameat replaces the directory entry, never follows a changed symlink.
631
+ unix::renameat(directory, temporary.as_str(), directory, name)
632
+ .map_err(|_| SoftFailure::Unavailable)?;
633
+ true
634
+ }
635
+ } else {
636
+ match unix::linkat(
637
+ directory,
638
+ temporary.as_str(),
639
+ directory,
640
+ name,
641
+ AtFlags::empty(),
642
+ ) {
643
+ Ok(()) => true,
644
+ Err(rustix::io::Errno::EXIST) => false,
645
+ Err(_) => return Err(SoftFailure::Unavailable),
646
+ }
647
+ };
648
+ if admitted {
649
+ directory.sync_all().map_err(|_| SoftFailure::Unavailable)?;
521
650
  }
522
- };
523
- let _ = fs::remove_file(&temporary);
524
- if admitted && store::fsync_dir(directory).is_err() {
525
- return Err(SoftFailure::Unavailable);
526
- }
527
- Ok(admitted)
651
+ Ok(admitted)
652
+ })();
653
+ let _ = unix::unlinkat(directory, temporary.as_str(), AtFlags::empty());
654
+ publish
528
655
  }
529
656
 
530
657
  fn stream_exact(
@@ -1040,7 +1167,14 @@ mod backfill_tests {
1040
1167
  length: object.length,
1041
1168
  };
1042
1169
  assert_eq!(
1043
- publish_stream(&mut input, &destination, &object, None),
1170
+ publish_stream(
1171
+ &mut input,
1172
+ &cache_parent(&root, &destination).unwrap(),
1173
+ destination.file_name().unwrap(),
1174
+ &object,
1175
+ None,
1176
+ None
1177
+ ),
1044
1178
  Ok(true)
1045
1179
  );
1046
1180
  assert!(!source.contains(&object.sha256));
@@ -1077,7 +1211,14 @@ mod backfill_tests {
1077
1211
  }
1078
1212
  }
1079
1213
  assert_eq!(
1080
- publish_stream(&mut Fails(false), &destination, &object, None),
1214
+ publish_stream(
1215
+ &mut Fails(false),
1216
+ &cache_parent(&root, &destination).unwrap(),
1217
+ destination.file_name().unwrap(),
1218
+ &object,
1219
+ None,
1220
+ None
1221
+ ),
1081
1222
  Err(SoftFailure::Unavailable)
1082
1223
  );
1083
1224
  assert!(!destination.exists());
@@ -1153,7 +1294,14 @@ mod exit_wait_tests {
1153
1294
  let wanted = object.clone();
1154
1295
  let (finish, finishing) = mpsc::channel();
1155
1296
  let copy = std::thread::spawn(move || {
1156
- let result = publish_stream(&mut Paced(&data), &target, &wanted, Some(&observed));
1297
+ let result = publish_stream(
1298
+ &mut Paced(&data),
1299
+ &cache_parent(target.parent().unwrap(), &target).unwrap(),
1300
+ target.file_name().unwrap(),
1301
+ &wanted,
1302
+ Some(&observed),
1303
+ None,
1304
+ );
1157
1305
  finishing.recv().unwrap();
1158
1306
  observed.activity.finished();
1159
1307
  result
@@ -1185,3 +1333,140 @@ mod exit_wait_tests {
1185
1333
  fs::remove_dir_all(root).unwrap();
1186
1334
  }
1187
1335
  }
1336
+
1337
+ #[cfg(test)]
1338
+ mod repair_tests {
1339
+ use super::*;
1340
+ use std::sync::atomic::{AtomicBool, Ordering};
1341
+
1342
+ struct Swap {
1343
+ once: AtomicBool,
1344
+ path: PathBuf,
1345
+ replacement: PathBuf,
1346
+ outside: Option<PathBuf>,
1347
+ }
1348
+ impl store::Progress for Swap {
1349
+ fn admitted(&self) {}
1350
+ fn moved(&self, _: u64) {
1351
+ if self.once.swap(true, Ordering::SeqCst) {
1352
+ return;
1353
+ }
1354
+ if let Some(outside) = &self.outside {
1355
+ fs::rename(&self.path, &self.replacement).unwrap();
1356
+ std::os::unix::fs::symlink(outside, &self.path).unwrap();
1357
+ } else {
1358
+ fs::rename(&self.replacement, &self.path).unwrap();
1359
+ }
1360
+ }
1361
+ }
1362
+
1363
+ #[test]
1364
+ fn publication_stays_in_its_open_directory_after_a_fanout_swap() {
1365
+ let root = std::env::temp_dir().join(format!(
1366
+ "tfs-parent-swap-{}-{}",
1367
+ std::process::id(),
1368
+ store::now_nanos_unique()
1369
+ ));
1370
+ let source = Store::init(&root.join("source")).unwrap();
1371
+ let data = vec![9; 4096];
1372
+ let object = source
1373
+ .put_stream(&mut data.as_slice(), None, &Fault::default())
1374
+ .unwrap()
1375
+ .obj;
1376
+ let cache = RepoObjectCache::new(root.join("cache"));
1377
+ let path = cache.path(CacheKind::Blob, &object.sha256).unwrap();
1378
+ let outside = root.join("outside");
1379
+ fs::create_dir_all(&outside).unwrap();
1380
+ let parked = root.join("parked");
1381
+ let swap = Swap {
1382
+ once: AtomicBool::new(false),
1383
+ path: path.parent().unwrap().to_path_buf(),
1384
+ replacement: parked.clone(),
1385
+ outside: Some(outside.clone()),
1386
+ };
1387
+ assert_eq!(
1388
+ cache
1389
+ .backfill_observed(&source, CacheKind::Blob, &object, Some(&swap))
1390
+ .unwrap(),
1391
+ CacheWrite::Stored
1392
+ );
1393
+ assert_eq!(fs::read_dir(outside).unwrap().count(), 0);
1394
+ assert_eq!(
1395
+ fs::read(parked.join(path.file_name().unwrap())).unwrap(),
1396
+ data
1397
+ );
1398
+ fs::remove_dir_all(root).unwrap();
1399
+ }
1400
+
1401
+ #[test]
1402
+ fn repair_adopts_a_competing_verified_final_instead_of_replacing_it() {
1403
+ let root = std::env::temp_dir().join(format!(
1404
+ "tfs-repair-winner-{}-{}",
1405
+ std::process::id(),
1406
+ store::now_nanos_unique()
1407
+ ));
1408
+ let source = Store::init(&root.join("source")).unwrap();
1409
+ let data = vec![17; 4096];
1410
+ let object = source
1411
+ .put_stream(&mut data.as_slice(), None, &Fault::default())
1412
+ .unwrap()
1413
+ .obj;
1414
+ let cache = RepoObjectCache::new(root.join("cache"));
1415
+ let path = cache.path(CacheKind::Blob, &object.sha256).unwrap();
1416
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
1417
+ fs::write(&path, b"bad").unwrap();
1418
+ let winner = root.join("winner");
1419
+ fs::write(&winner, &data).unwrap();
1420
+ let winner_inode = fs::metadata(&winner).unwrap().ino();
1421
+ let swap = Swap {
1422
+ once: AtomicBool::new(false),
1423
+ path: path.clone(),
1424
+ replacement: winner,
1425
+ outside: None,
1426
+ };
1427
+ assert_eq!(
1428
+ cache
1429
+ .backfill_observed(&source, CacheKind::Blob, &object, Some(&swap))
1430
+ .unwrap(),
1431
+ CacheWrite::Present
1432
+ );
1433
+ assert_eq!(fs::metadata(&path).unwrap().ino(), winner_inode);
1434
+ assert_eq!(fs::read(path).unwrap(), data);
1435
+ fs::remove_dir_all(root).unwrap();
1436
+ }
1437
+
1438
+ #[test]
1439
+ fn interrupted_repair_keeps_the_previous_final_and_removes_its_temporary() {
1440
+ struct Broken;
1441
+ impl Read for Broken {
1442
+ fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
1443
+ Err(std::io::Error::other("copy interrupted"))
1444
+ }
1445
+ }
1446
+ let root = std::env::temp_dir().join(format!(
1447
+ "tfs-repair-interrupted-{}-{}",
1448
+ std::process::id(),
1449
+ store::now_nanos_unique()
1450
+ ));
1451
+ fs::create_dir_all(&root).unwrap();
1452
+ let object = ObjectRef::of(b"right");
1453
+ let path = root.join(&object.sha256);
1454
+ fs::write(&path, b"wrong").unwrap();
1455
+ let previous = fs::metadata(&path).unwrap();
1456
+ let directory = cache_parent(&root, &path).unwrap();
1457
+ assert_eq!(
1458
+ publish_stream(
1459
+ &mut Broken,
1460
+ &directory,
1461
+ path.file_name().unwrap(),
1462
+ &object,
1463
+ None,
1464
+ Some(&previous)
1465
+ ),
1466
+ Err(SoftFailure::Unavailable)
1467
+ );
1468
+ assert_eq!(fs::read(&path).unwrap(), b"wrong");
1469
+ assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
1470
+ fs::remove_dir_all(root).unwrap();
1471
+ }
1472
+ }
@@ -5,7 +5,7 @@ use std::sync::{Arc, Barrier};
5
5
  use std::thread;
6
6
  use std::time::{SystemTime, UNIX_EPOCH};
7
7
 
8
- use tensorfs_core::ids::ObjectRef;
8
+ use tensorfs_core::ids::{Doc, ObjectRef};
9
9
  use tensorfs_core::manifest::Manifest;
10
10
  use tensorfs_core::repo_cache::{CacheKind, CacheRead, CacheWrite, RepoObjectCache};
11
11
  use tensorfs_core::store::{Fault, Store};
@@ -105,6 +105,27 @@ fn blobs_and_manifests_round_trip_through_normal_store_admission() {
105
105
  );
106
106
  assert_eq!(destination.read_manifest(&manifest_ref).unwrap(), manifest);
107
107
 
108
+ // Manifest repairs obey the same complete-object publication rule as blobs.
109
+ let manifest_path = cache
110
+ .path(CacheKind::Manifest, &manifest_ref.sha256)
111
+ .unwrap();
112
+ fs::set_permissions(
113
+ &manifest_path,
114
+ std::os::unix::fs::PermissionsExt::from_mode(0o600),
115
+ )
116
+ .unwrap();
117
+ fs::write(&manifest_path, b"damaged manifest").unwrap();
118
+ assert_eq!(
119
+ cache
120
+ .backfill_store(&source, CacheKind::Manifest, &manifest_ref)
121
+ .unwrap(),
122
+ CacheWrite::Stored
123
+ );
124
+ assert_eq!(
125
+ fs::read(&manifest_path).unwrap(),
126
+ manifest.canonical_bytes()
127
+ );
128
+
108
129
  let cache_root = cache.root();
109
130
  assert!(!cache_root.join("tensorfs.sqlite").exists());
110
131
  assert!(!cache_root.join("repos").exists());
@@ -203,10 +224,10 @@ fn missing_and_corrupt_cache_objects_fall_through_without_local_admission() {
203
224
  cache
204
225
  .backfill_store(&source, CacheKind::Blob, &wanted)
205
226
  .unwrap(),
206
- CacheWrite::Unavailable,
207
- "no-clobber leaves repair of a corrupt final entry explicit"
227
+ CacheWrite::Stored,
228
+ "verified local bytes atomically repair a corrupt regular cache entry"
208
229
  );
209
- assert_eq!(fs::read(&cache_path).unwrap(), b"wrong");
230
+ assert_eq!(fs::read(&cache_path).unwrap(), b"right");
210
231
  fs::remove_file(&cache_path).unwrap();
211
232
  assert_eq!(
212
233
  cache
@@ -568,3 +589,66 @@ fn concurrent_offers_keep_the_offer_list_and_the_sequence_in_step() {
568
589
  assert_eq!(mirror.report().durable.objects, objects.len() as u64);
569
590
  let _ = fs::remove_dir_all(root);
570
591
  }
592
+
593
+ #[test]
594
+ fn a_symlinked_fanout_never_redirects_cache_publication() {
595
+ use std::os::unix::fs::symlink;
596
+ let root = temporary("fanout-symlink");
597
+ let source = Store::init(&root.join("source")).unwrap();
598
+ let object = put_blob(&source, b"verified");
599
+ let cache = RepoObjectCache::new(root.join("cache"));
600
+ let outside = root.join("outside");
601
+ fs::create_dir_all(&outside).unwrap();
602
+ fs::create_dir_all(cache.root()).unwrap();
603
+ symlink(&outside, cache.root().join("blobs")).unwrap();
604
+ assert_eq!(
605
+ cache
606
+ .backfill_store(&source, CacheKind::Blob, &object)
607
+ .unwrap(),
608
+ CacheWrite::Unavailable
609
+ );
610
+ assert_eq!(fs::read_dir(outside).unwrap().count(), 0);
611
+ fs::remove_dir_all(root).unwrap();
612
+ }
613
+
614
+ #[test]
615
+ fn concurrent_repairs_publish_only_verified_complete_bytes() {
616
+ let root = temporary("repairs");
617
+ let source = Arc::new(Store::init(&root.join("source")).unwrap());
618
+ let data = vec![73; 2 << 20];
619
+ let object = put_blob(&source, &data);
620
+ let cache = Arc::new(RepoObjectCache::new(root.join("cache")));
621
+ let path = cache.path(CacheKind::Blob, &object.sha256).unwrap();
622
+ write(&path, &vec![0; data.len()]);
623
+ let barrier = Arc::new(Barrier::new(4));
624
+ let workers = (0..4)
625
+ .map(|_| {
626
+ let (source, cache, object, barrier) = (
627
+ source.clone(),
628
+ cache.clone(),
629
+ object.clone(),
630
+ barrier.clone(),
631
+ );
632
+ thread::spawn(move || {
633
+ barrier.wait();
634
+ cache
635
+ .backfill_store(&source, CacheKind::Blob, &object)
636
+ .unwrap()
637
+ })
638
+ })
639
+ .collect::<Vec<_>>();
640
+ for worker in workers {
641
+ assert!(matches!(
642
+ worker.join().unwrap(),
643
+ CacheWrite::Stored | CacheWrite::Present
644
+ ));
645
+ }
646
+ assert_eq!(fs::read(path).unwrap(), data);
647
+ assert_eq!(
648
+ cache
649
+ .backfill_store(&source, CacheKind::Blob, &object)
650
+ .unwrap(),
651
+ CacheWrite::Present
652
+ );
653
+ fs::remove_dir_all(root).unwrap();
654
+ }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.37"
7
+ version = "0.3.38"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [