tensorfs 0.3.27__tar.gz → 0.3.30__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.
Potentially problematic release.
This version of tensorfs might be problematic. Click here for more details.
- {tensorfs-0.3.27 → tensorfs-0.3.30}/Cargo.lock +2 -2
- {tensorfs-0.3.27 → tensorfs-0.3.30}/Cargo.toml +1 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/PKG-INFO +1 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/fetch.rs +49 -6
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/main.rs +12 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/providers.rs +475 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/api.rs +7 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/http.rs +166 -16
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/hub.rs +16 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/ledger.rs +104 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/pull.rs +46 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/push.rs +4 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/tests.rs +261 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/store.rs +24 -3
- {tensorfs-0.3.27 → tensorfs-0.3.30}/pyproject.toml +1 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/_ext.pyi +1 -1
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/catalog.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/lib.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/py.typed +0 -0
|
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
392
392
|
|
|
393
393
|
[[package]]
|
|
394
394
|
name = "tensorfs-core"
|
|
395
|
-
version = "0.3.
|
|
395
|
+
version = "0.3.30"
|
|
396
396
|
dependencies = [
|
|
397
397
|
"fs2",
|
|
398
398
|
"minicbor",
|
|
@@ -404,7 +404,7 @@ dependencies = [
|
|
|
404
404
|
|
|
405
405
|
[[package]]
|
|
406
406
|
name = "tensorfs-py"
|
|
407
|
-
version = "0.3.
|
|
407
|
+
version = "0.3.30"
|
|
408
408
|
dependencies = [
|
|
409
409
|
"pyo3",
|
|
410
410
|
"tensorfs-core",
|
|
@@ -228,6 +228,34 @@ fn credential_of(
|
|
|
228
228
|
/// unbound Store is a pull with no cache, and a cache that is missing, cold, corrupt,
|
|
229
229
|
/// read-only or unmounted degrades to exactly that pull.
|
|
230
230
|
pub fn cmd_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
|
|
231
|
+
pull_with_budget(root, refspec, flags, None)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// A distinct verb makes an older CLI refuse instead of ignoring new quota flags.
|
|
235
|
+
pub fn cmd_bounded_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
|
|
236
|
+
let bound = |name: &str| {
|
|
237
|
+
let values = crate::flag_all(flags, name);
|
|
238
|
+
if values.len() != 1 {
|
|
239
|
+
return None;
|
|
240
|
+
}
|
|
241
|
+
values[0].parse::<u64>().ok()
|
|
242
|
+
};
|
|
243
|
+
let (Some(bytes), Some(objects)) = (bound("max-download-bytes"), bound("max-download-objects"))
|
|
244
|
+
else {
|
|
245
|
+
eprintln!(
|
|
246
|
+
"bounded fetch requires one unsigned --max-download-bytes and --max-download-objects"
|
|
247
|
+
);
|
|
248
|
+
return ExitCode::from(2);
|
|
249
|
+
};
|
|
250
|
+
pull_with_budget(root, refspec, flags, Some((bytes, objects)))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fn pull_with_budget(
|
|
254
|
+
root: &Path,
|
|
255
|
+
refspec: &str,
|
|
256
|
+
flags: &Flags,
|
|
257
|
+
budget: Option<(u64, u64)>,
|
|
258
|
+
) -> ExitCode {
|
|
231
259
|
let store = ok!(Store::open(root));
|
|
232
260
|
let Some(base) = flag(flags, "hub") else {
|
|
233
261
|
eprintln!("--hub <base-url> is required");
|
|
@@ -247,6 +275,7 @@ pub fn cmd_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
|
|
|
247
275
|
request.session = flag(flags, "session").unwrap_or("");
|
|
248
276
|
request.streams = crate::flag_num(flags, "streams", transport::STREAMS);
|
|
249
277
|
request.deadline = deadline_of(flags);
|
|
278
|
+
request.download_budget = budget;
|
|
250
279
|
// THE WALK REPORTS WHILE IT WALKS.
|
|
251
280
|
//
|
|
252
281
|
// Until now `tfs fetch` printed one report at the end, the pod's supervisor buffered
|
|
@@ -258,17 +287,18 @@ pub fn cmd_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
|
|
|
258
287
|
// What is printed is what was MEASURED — bytes through each door and the instant they
|
|
259
288
|
// landed — and nothing derived. A rate is the consumer's division of two of these
|
|
260
289
|
// samples, which is the only place it can be computed honestly: this walk has sixteen
|
|
261
|
-
// streams and no opinion about what interval anyone wants to average over.
|
|
262
|
-
// denominator
|
|
263
|
-
//
|
|
264
|
-
// side guessed.
|
|
290
|
+
// streams and no opinion about what interval anyone wants to average over. The
|
|
291
|
+
// denominator is the native FetchPlan's declared byte count. The plan callback also
|
|
292
|
+
// supplies already-held bytes, so a resumed or warm pull starts at its real position.
|
|
265
293
|
//
|
|
266
294
|
// The two doors are separate counters because they cost different things and only one
|
|
267
295
|
// of them is an invoice. A cache that has silently gone cold produces a complete
|
|
268
296
|
// closure, correct totals, and a bill — and looks exactly like a warm one unless the
|
|
269
297
|
// split is reported while it happens.
|
|
270
298
|
let progress = PullProgress::new(request.sample_seconds);
|
|
299
|
+
let planned = |plan: &FetchPlan| progress.planned(plan);
|
|
271
300
|
let observe = |_object: &ObjectRef, moved: u64, door: ObjectSource| progress.saw(moved, door);
|
|
301
|
+
request.on_plan = Some(&planned);
|
|
272
302
|
request.on_object = Some(&observe);
|
|
273
303
|
let report = ok!(transport::pull(&request));
|
|
274
304
|
progress.settle();
|
|
@@ -1282,6 +1312,8 @@ struct PullProgress {
|
|
|
1282
1312
|
}
|
|
1283
1313
|
|
|
1284
1314
|
struct PullProgressState {
|
|
1315
|
+
total_bytes: u64,
|
|
1316
|
+
held_bytes: u64,
|
|
1285
1317
|
origin_bytes: u64,
|
|
1286
1318
|
cache_bytes: u64,
|
|
1287
1319
|
origin_objects: u64,
|
|
@@ -1293,6 +1325,8 @@ impl PullProgress {
|
|
|
1293
1325
|
fn new(sample_seconds: f64) -> PullProgress {
|
|
1294
1326
|
PullProgress {
|
|
1295
1327
|
state: Mutex::new(PullProgressState {
|
|
1328
|
+
total_bytes: 0,
|
|
1329
|
+
held_bytes: 0,
|
|
1296
1330
|
origin_bytes: 0,
|
|
1297
1331
|
cache_bytes: 0,
|
|
1298
1332
|
origin_objects: 0,
|
|
@@ -1303,6 +1337,14 @@ impl PullProgress {
|
|
|
1303
1337
|
}
|
|
1304
1338
|
}
|
|
1305
1339
|
|
|
1340
|
+
fn planned(&self, plan: &FetchPlan) {
|
|
1341
|
+
let mut state = self.state.lock().unwrap();
|
|
1342
|
+
state.total_bytes = plan.declared_bytes();
|
|
1343
|
+
state.held_bytes = plan.held_bytes();
|
|
1344
|
+
state.last = Some(Instant::now());
|
|
1345
|
+
print_progress(&state);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1306
1348
|
fn saw(&self, moved: u64, door: ObjectSource) {
|
|
1307
1349
|
let mut state = self.state.lock().unwrap();
|
|
1308
1350
|
match door {
|
|
@@ -1364,7 +1406,8 @@ impl PullProgress {
|
|
|
1364
1406
|
/// door at all, and those are the two states worth telling apart.
|
|
1365
1407
|
fn print_progress(state: &PullProgressState) {
|
|
1366
1408
|
eprintln!(
|
|
1367
|
-
"{{\"event\":\"pull.progress\",\"origin_bytes\":{},\"cache_bytes\":{},\"origin_objects\":{},\"cache_objects\":{}}}",
|
|
1368
|
-
state.
|
|
1409
|
+
"{{\"event\":\"pull.progress\",\"total_bytes\":{},\"held_bytes\":{},\"origin_bytes\":{},\"cache_bytes\":{},\"origin_objects\":{},\"cache_objects\":{}}}",
|
|
1410
|
+
state.total_bytes, state.held_bytes, state.origin_bytes, state.cache_bytes,
|
|
1411
|
+
state.origin_objects, state.cache_objects
|
|
1369
1412
|
);
|
|
1370
1413
|
}
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
//! and the origin. It is bound by `tfs store ensure --repo-cache` and never by a flag here.
|
|
48
48
|
//!
|
|
49
49
|
//! tfs fetch <root> <ref> closure -> plan -> N-way walk (presign at use) -> proof
|
|
50
|
+
//! tfs fetch bounded <root> <ref> --max-download-bytes N --max-download-objects N
|
|
51
|
+
//! same pull, refused before body transfer if new objects exceed bounds
|
|
50
52
|
//! tfs fetch plan-objects <root> <session> --refs <jsonl> HELD/WANTED, no manifest
|
|
51
53
|
//! tfs fetch url <root> <sha256> <length> <url|-> [--progress] [--streams N]
|
|
52
54
|
//! one granted object, in parallel ranges
|
|
@@ -351,6 +353,7 @@ fn main() -> ExitCode {
|
|
|
351
353
|
["fetch", "plan-objects", r, s] => fetch::cmd_plan_objects(Path::new(r), s, &flags),
|
|
352
354
|
["fetch", "admit", r, p, i] => fetch::cmd_admit(Path::new(r), Path::new(p), i, &flags),
|
|
353
355
|
["fetch", "complete", r, p] => fetch::cmd_complete(Path::new(r), Path::new(p)),
|
|
356
|
+
["fetch", "bounded", r, refspec] => fetch::cmd_bounded_pull(Path::new(r), refspec, &flags),
|
|
354
357
|
["fetch", "url", r, i, l, u] => fetch::cmd_url(Path::new(r), i, l, u, &flags),
|
|
355
358
|
["staging", "fetch", r, i, l, u] => fetch::cmd_staging_fetch(Path::new(r), i, l, u, &flags),
|
|
356
359
|
["staging", "drop", r, i] => fetch::cmd_staging_drop(Path::new(r), i),
|
|
@@ -361,7 +364,15 @@ fn main() -> ExitCode {
|
|
|
361
364
|
["source", "plan", u] => fetch::cmd_source_plan(u, &flags),
|
|
362
365
|
["source", "pull", r, u] => fetch::cmd_source_pull(Path::new(r), u, &flags),
|
|
363
366
|
["fetch", sub, _t]
|
|
364
|
-
if [
|
|
367
|
+
if [
|
|
368
|
+
"plan",
|
|
369
|
+
"plan-objects",
|
|
370
|
+
"admit",
|
|
371
|
+
"complete",
|
|
372
|
+
"url",
|
|
373
|
+
"bounded",
|
|
374
|
+
]
|
|
375
|
+
.contains(sub) =>
|
|
365
376
|
{
|
|
366
377
|
eprintln!("usage: see the header of {}", file!());
|
|
367
378
|
ExitCode::from(2)
|
|
@@ -30,6 +30,8 @@ const API_PAGE_MAX: usize = 64;
|
|
|
30
30
|
/// A member the provider does not identify by digest is fetched whole and hashed; the
|
|
31
31
|
/// class is HuggingFace non-LFS files (configs, tokenizers), which git itself keeps small.
|
|
32
32
|
const UNPINNED_MEMBER_MAX_BYTES: u64 = 64 << 20;
|
|
33
|
+
/// An explicit ordinary-file selection is a bounded working set, not a weight download.
|
|
34
|
+
const FILE_SELECTION_MAX_BYTES: u64 = 64 << 20;
|
|
33
35
|
const MEMBER_MAX: usize = 4096;
|
|
34
36
|
/// A `*.safetensors.index.json` is a map from tensor name to shard file. It is JSON that
|
|
35
37
|
/// git itself keeps small, and the Go resolver bounded it at the same 32 MiB.
|
|
@@ -250,6 +252,17 @@ fn list_observed(
|
|
|
250
252
|
credentials: &dyn CredentialProvider,
|
|
251
253
|
deadline: Deadline,
|
|
252
254
|
ledger: &Ledger,
|
|
255
|
+
) -> Result<Vec<SourceMember>> {
|
|
256
|
+
list_selected_observed(uri, endpoints, credentials, deadline, ledger, None)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn list_selected_observed(
|
|
260
|
+
uri: &SourceUri,
|
|
261
|
+
endpoints: &Endpoints,
|
|
262
|
+
credentials: &dyn CredentialProvider,
|
|
263
|
+
deadline: Deadline,
|
|
264
|
+
ledger: &Ledger,
|
|
265
|
+
files: Option<&[String]>,
|
|
253
266
|
) -> Result<Vec<SourceMember>> {
|
|
254
267
|
let policy = endpoints.policy(uri);
|
|
255
268
|
let base = endpoints.base(uri);
|
|
@@ -287,6 +300,9 @@ fn list_observed(
|
|
|
287
300
|
let Some(path) = json_str(row, "path") else {
|
|
288
301
|
return refuse(Code::MISSING_FIELD, "a tree row names no path");
|
|
289
302
|
};
|
|
303
|
+
if files.is_some_and(|files| !files.iter().any(|file| file == path)) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
290
306
|
if !member_path(path) {
|
|
291
307
|
// Every HuggingFace repository carries `.gitattributes`, and a
|
|
292
308
|
// leading dot is outside the member grammar. Refusing the whole
|
|
@@ -345,7 +361,10 @@ fn list_observed(
|
|
|
345
361
|
member: path.to_string(),
|
|
346
362
|
length,
|
|
347
363
|
sha256,
|
|
348
|
-
url: format!(
|
|
364
|
+
url: format!(
|
|
365
|
+
"{base}/{org}/{repo}/resolve/{revision}/{}",
|
|
366
|
+
path.replace(' ', "%20")
|
|
367
|
+
),
|
|
349
368
|
primary: false,
|
|
350
369
|
});
|
|
351
370
|
}
|
|
@@ -1342,6 +1361,145 @@ pub fn resolve(
|
|
|
1342
1361
|
})
|
|
1343
1362
|
}
|
|
1344
1363
|
|
|
1364
|
+
/// Resolve only explicitly named ordinary files at one immutable HF revision.
|
|
1365
|
+
/// No suffix has special meaning here: selecting Python bytes never executes them,
|
|
1366
|
+
/// and selecting an index does not expand it into tensor shards. Tensor carriers
|
|
1367
|
+
/// continue to use `resolve` and reviewed profile/carrier selection.
|
|
1368
|
+
pub fn resolve_files(
|
|
1369
|
+
uri: &SourceUri,
|
|
1370
|
+
files: &[String],
|
|
1371
|
+
endpoints: &Endpoints,
|
|
1372
|
+
credentials: &dyn CredentialProvider,
|
|
1373
|
+
deadline: Deadline,
|
|
1374
|
+
) -> Result<Resolution> {
|
|
1375
|
+
let SourceUri::HuggingFace {
|
|
1376
|
+
org,
|
|
1377
|
+
repo,
|
|
1378
|
+
revision,
|
|
1379
|
+
} = uri
|
|
1380
|
+
else {
|
|
1381
|
+
return refuse(
|
|
1382
|
+
Code::SOURCE_NOT_ALLOWED,
|
|
1383
|
+
"ordinary file selection requires HuggingFace",
|
|
1384
|
+
);
|
|
1385
|
+
};
|
|
1386
|
+
if files.is_empty() || files.len() > MEMBER_MAX {
|
|
1387
|
+
return refuse(
|
|
1388
|
+
Code::COUNT_CAP,
|
|
1389
|
+
"ordinary file selection count is outside the bound",
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
let mut names = files.to_vec();
|
|
1393
|
+
names.sort();
|
|
1394
|
+
if names
|
|
1395
|
+
.iter()
|
|
1396
|
+
.any(|name| !member_path(name) || crate::manifest::check_path(name).is_err())
|
|
1397
|
+
{
|
|
1398
|
+
return refuse(
|
|
1399
|
+
Code::PATH_ILLEGAL,
|
|
1400
|
+
"ordinary file selection requires exact member paths",
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
if names.windows(2).any(|pair| pair[0] == pair[1]) {
|
|
1404
|
+
return refuse(
|
|
1405
|
+
Code::DUPLICATE_KEY,
|
|
1406
|
+
"ordinary file selection names one member twice",
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
let ledger = Ledger::new();
|
|
1410
|
+
let policy = endpoints.policy(uri);
|
|
1411
|
+
let listed =
|
|
1412
|
+
list_selected_observed(uri, endpoints, credentials, deadline, &ledger, Some(&names))?;
|
|
1413
|
+
// Establish the whole roster before fetching any body. A missing later member
|
|
1414
|
+
// must not turn a partial requested tree into a successful smaller selection.
|
|
1415
|
+
let selected = names
|
|
1416
|
+
.iter()
|
|
1417
|
+
.map(|name| {
|
|
1418
|
+
listed
|
|
1419
|
+
.iter()
|
|
1420
|
+
.find(|member| &member.member == name)
|
|
1421
|
+
.ok_or_else(|| Refusal {
|
|
1422
|
+
code: Code::MISSING_FIELD,
|
|
1423
|
+
detail: format!("the source does not contain requested file {name:?}"),
|
|
1424
|
+
})
|
|
1425
|
+
})
|
|
1426
|
+
.collect::<Result<Vec<_>>>()?;
|
|
1427
|
+
let listed_bytes = selected.iter().try_fold(0u64, |total, member| {
|
|
1428
|
+
total.checked_add(member.length.unwrap_or(0))
|
|
1429
|
+
});
|
|
1430
|
+
if listed_bytes.is_none_or(|bytes| bytes > FILE_SELECTION_MAX_BYTES) {
|
|
1431
|
+
return refuse(
|
|
1432
|
+
Code::SIZE_CAP,
|
|
1433
|
+
"ordinary file selection exceeds its byte bound",
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
let mut remaining = FILE_SELECTION_MAX_BYTES;
|
|
1437
|
+
let mut resolved = Vec::with_capacity(selected.len());
|
|
1438
|
+
for member in selected {
|
|
1439
|
+
if member
|
|
1440
|
+
.length
|
|
1441
|
+
.map_or(remaining == 0, |length| length > remaining)
|
|
1442
|
+
{
|
|
1443
|
+
return refuse(
|
|
1444
|
+
Code::SIZE_CAP,
|
|
1445
|
+
"ordinary file selection exceeds its byte bound",
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
let (object, provenance) = if let Some(sha256) = &member.sha256 {
|
|
1449
|
+
let length = match member.length {
|
|
1450
|
+
Some(length) => length,
|
|
1451
|
+
None => probe_length(&member.url, &policy, credentials, deadline, &ledger)?,
|
|
1452
|
+
};
|
|
1453
|
+
(
|
|
1454
|
+
ObjectRef {
|
|
1455
|
+
sha256: sha256.clone(),
|
|
1456
|
+
length,
|
|
1457
|
+
},
|
|
1458
|
+
Provenance::Declared,
|
|
1459
|
+
)
|
|
1460
|
+
} else {
|
|
1461
|
+
let (body, _) = api_get(
|
|
1462
|
+
&member.url,
|
|
1463
|
+
&policy,
|
|
1464
|
+
credentials,
|
|
1465
|
+
remaining,
|
|
1466
|
+
deadline,
|
|
1467
|
+
&ledger,
|
|
1468
|
+
)?;
|
|
1469
|
+
if member
|
|
1470
|
+
.length
|
|
1471
|
+
.is_some_and(|length| length != body.len() as u64)
|
|
1472
|
+
{
|
|
1473
|
+
return refuse(
|
|
1474
|
+
Code::LENGTH_MISMATCH,
|
|
1475
|
+
"ordinary file body disagrees with listed length",
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
(ObjectRef::of(&body), Provenance::Computed)
|
|
1479
|
+
};
|
|
1480
|
+
remaining = remaining
|
|
1481
|
+
.checked_sub(object.length)
|
|
1482
|
+
.ok_or_else(|| Refusal {
|
|
1483
|
+
code: Code::SIZE_CAP,
|
|
1484
|
+
detail: "ordinary file selection exceeds its byte bound".into(),
|
|
1485
|
+
})?;
|
|
1486
|
+
resolved.push(ResolvedMember {
|
|
1487
|
+
member: member.member.clone(),
|
|
1488
|
+
object,
|
|
1489
|
+
url: member.url.clone(),
|
|
1490
|
+
provenance,
|
|
1491
|
+
carrier: false,
|
|
1492
|
+
requires: Vec::new(),
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
let canonical = format!("hf://{org}/{repo}@{revision}");
|
|
1496
|
+
Ok(Resolution {
|
|
1497
|
+
selection_sha256: selection_digest(&canonical, &resolved),
|
|
1498
|
+
canonical,
|
|
1499
|
+
members: resolved,
|
|
1500
|
+
})
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1345
1503
|
#[cfg(test)]
|
|
1346
1504
|
mod tests {
|
|
1347
1505
|
use super::*;
|
|
@@ -2231,4 +2389,320 @@ mod tests {
|
|
|
2231
2389
|
handle.join().unwrap();
|
|
2232
2390
|
let _ = std::fs::remove_dir_all(root);
|
|
2233
2391
|
}
|
|
2392
|
+
|
|
2393
|
+
#[test]
|
|
2394
|
+
fn explicit_files_resolve_only_named_bytes_and_retain_across_restart() {
|
|
2395
|
+
let config = b"{\"model_type\":\"test\"}".to_vec();
|
|
2396
|
+
let tokenizer = b"these are tokenizer bytes".to_vec();
|
|
2397
|
+
let expected = vec![ObjectRef::of(&config), ObjectRef::of(&tokenizer)];
|
|
2398
|
+
let listing = format!(
|
|
2399
|
+
r#"[
|
|
2400
|
+
{{"type":"file","path":"model.safetensors","size":100000000000,"lfs":{{"oid":"{}","size":100000000000}}}},
|
|
2401
|
+
{{"type":"file","path":"config.json","size":{}}},
|
|
2402
|
+
{{"type":"file","path":"tokenizer files/tokenizer.json","size":{}}},
|
|
2403
|
+
{{"type":"file","path":"unused.py","size":99}}
|
|
2404
|
+
]"#,
|
|
2405
|
+
"*".repeat(64),
|
|
2406
|
+
config.len(),
|
|
2407
|
+
tokenizer.len()
|
|
2408
|
+
);
|
|
2409
|
+
// Two order-independent resolutions each read list + two small bodies;
|
|
2410
|
+
// materialization reads the bodies once, and restart/warm admission reads none.
|
|
2411
|
+
let (base, seen, handle) = origin(8, move |request| {
|
|
2412
|
+
if request.path.contains("/tree/") {
|
|
2413
|
+
ok_body(listing.as_bytes(), "application/json")
|
|
2414
|
+
} else if request.path.ends_with("/config.json") {
|
|
2415
|
+
ok_body(&config, "application/json")
|
|
2416
|
+
} else if request.path.ends_with("/tokenizer.json") {
|
|
2417
|
+
ok_body(&tokenizer, "application/json")
|
|
2418
|
+
} else {
|
|
2419
|
+
panic!("unselected file requested: {}", request.path);
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
|
|
2423
|
+
let endpoints = Endpoints {
|
|
2424
|
+
huggingface: base,
|
|
2425
|
+
allow_local: true,
|
|
2426
|
+
..Endpoints::default()
|
|
2427
|
+
};
|
|
2428
|
+
let mut names = vec![
|
|
2429
|
+
"config.json".into(),
|
|
2430
|
+
"tokenizer files/tokenizer.json".into(),
|
|
2431
|
+
];
|
|
2432
|
+
let selected =
|
|
2433
|
+
resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none()).unwrap();
|
|
2434
|
+
names.reverse();
|
|
2435
|
+
assert_eq!(
|
|
2436
|
+
selected,
|
|
2437
|
+
resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none()).unwrap()
|
|
2438
|
+
);
|
|
2439
|
+
assert_eq!(
|
|
2440
|
+
selected
|
|
2441
|
+
.members
|
|
2442
|
+
.iter()
|
|
2443
|
+
.map(|m| m.object.clone())
|
|
2444
|
+
.collect::<Vec<_>>(),
|
|
2445
|
+
expected
|
|
2446
|
+
);
|
|
2447
|
+
assert!(selected
|
|
2448
|
+
.members
|
|
2449
|
+
.iter()
|
|
2450
|
+
.all(|m| m.provenance == Provenance::Computed && !m.carrier && m.requires.is_empty()));
|
|
2451
|
+
let root = temporary("selected-files");
|
|
2452
|
+
let store = Store::init(&root).unwrap();
|
|
2453
|
+
let producer = ObjectRef::of(b"files-producer").id();
|
|
2454
|
+
let recipient = ObjectRef::of(b"files-recipient").id();
|
|
2455
|
+
let (cold, transfers) = materialize_selected(
|
|
2456
|
+
&store,
|
|
2457
|
+
&producer,
|
|
2458
|
+
&selected,
|
|
2459
|
+
&endpoints.policy(&uri),
|
|
2460
|
+
&Anonymous,
|
|
2461
|
+
Deadline::none(),
|
|
2462
|
+
&mut |_, _| {},
|
|
2463
|
+
)
|
|
2464
|
+
.unwrap();
|
|
2465
|
+
assert_eq!(
|
|
2466
|
+
transfers.iter().map(|row| row.transferred).sum::<u64>(),
|
|
2467
|
+
selected.member_bytes()
|
|
2468
|
+
);
|
|
2469
|
+
crate::source_artifact::retain(&store, &producer, &recipient).unwrap();
|
|
2470
|
+
drop(store);
|
|
2471
|
+
let store = Store::open(&root).unwrap();
|
|
2472
|
+
let (warm, transfers) = materialize_selected(
|
|
2473
|
+
&store,
|
|
2474
|
+
&producer,
|
|
2475
|
+
&selected,
|
|
2476
|
+
&endpoints.policy(&uri),
|
|
2477
|
+
&Anonymous,
|
|
2478
|
+
Deadline::none(),
|
|
2479
|
+
&mut |_, _| {},
|
|
2480
|
+
)
|
|
2481
|
+
.unwrap();
|
|
2482
|
+
assert_eq!(warm, cold);
|
|
2483
|
+
assert!(transfers.iter().all(|row| row.held && row.transferred == 0));
|
|
2484
|
+
crate::source_artifact::release(&store, &producer).unwrap();
|
|
2485
|
+
crate::gc::collect(&root, false).unwrap();
|
|
2486
|
+
let retained = crate::source_artifact::read(&store, &recipient)
|
|
2487
|
+
.unwrap()
|
|
2488
|
+
.unwrap();
|
|
2489
|
+
assert!(retained.complete && !retained.released);
|
|
2490
|
+
for object in &expected {
|
|
2491
|
+
assert!(store.record_valid(&object.sha256).is_ok());
|
|
2492
|
+
}
|
|
2493
|
+
crate::source_artifact::release(&store, &recipient).unwrap();
|
|
2494
|
+
crate::gc::collect(&root, false).unwrap();
|
|
2495
|
+
for object in expected {
|
|
2496
|
+
assert!(!store.contains(&object.sha256));
|
|
2497
|
+
}
|
|
2498
|
+
handle.join().unwrap();
|
|
2499
|
+
assert_eq!(seen.lock().unwrap().len(), 8);
|
|
2500
|
+
assert!(seen
|
|
2501
|
+
.lock()
|
|
2502
|
+
.unwrap()
|
|
2503
|
+
.iter()
|
|
2504
|
+
.any(|(path, _)| path.contains("tokenizer%20files/")));
|
|
2505
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
#[test]
|
|
2509
|
+
fn explicit_files_refuse_missing_and_over_budget_before_fetching_bodies() {
|
|
2510
|
+
for (names, listing, code) in [
|
|
2511
|
+
(
|
|
2512
|
+
vec!["absent.json".into(), "config.json".into()],
|
|
2513
|
+
r#"[{"type":"file","path":"config.json","size":2}]"#.to_string(),
|
|
2514
|
+
Code::MISSING_FIELD,
|
|
2515
|
+
),
|
|
2516
|
+
(
|
|
2517
|
+
vec!["config.json".into()],
|
|
2518
|
+
format!(
|
|
2519
|
+
r#"[{{"type":"file","path":"config.json","size":{}}}]"#,
|
|
2520
|
+
FILE_SELECTION_MAX_BYTES + 1
|
|
2521
|
+
),
|
|
2522
|
+
Code::SIZE_CAP,
|
|
2523
|
+
),
|
|
2524
|
+
] {
|
|
2525
|
+
let (base, _, handle) =
|
|
2526
|
+
origin(1, move |_| ok_body(listing.as_bytes(), "application/json"));
|
|
2527
|
+
let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
|
|
2528
|
+
let endpoints = Endpoints {
|
|
2529
|
+
huggingface: base,
|
|
2530
|
+
allow_local: true,
|
|
2531
|
+
..Endpoints::default()
|
|
2532
|
+
};
|
|
2533
|
+
assert_eq!(
|
|
2534
|
+
resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none())
|
|
2535
|
+
.unwrap_err()
|
|
2536
|
+
.code,
|
|
2537
|
+
code
|
|
2538
|
+
);
|
|
2539
|
+
handle.join().unwrap();
|
|
2540
|
+
}
|
|
2541
|
+
let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
|
|
2542
|
+
for names in [
|
|
2543
|
+
vec!["../config.json".into()],
|
|
2544
|
+
vec!["*.json".into()],
|
|
2545
|
+
vec!["/config.json".into()],
|
|
2546
|
+
] {
|
|
2547
|
+
assert_eq!(
|
|
2548
|
+
resolve_files(
|
|
2549
|
+
&uri,
|
|
2550
|
+
&names,
|
|
2551
|
+
&Endpoints::default(),
|
|
2552
|
+
&Anonymous,
|
|
2553
|
+
Deadline::none()
|
|
2554
|
+
)
|
|
2555
|
+
.unwrap_err()
|
|
2556
|
+
.code,
|
|
2557
|
+
Code::PATH_ILLEGAL
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
assert_eq!(
|
|
2561
|
+
resolve_files(
|
|
2562
|
+
&uri,
|
|
2563
|
+
&["config.json".into(), "config.json".into()],
|
|
2564
|
+
&Endpoints::default(),
|
|
2565
|
+
&Anonymous,
|
|
2566
|
+
Deadline::none()
|
|
2567
|
+
)
|
|
2568
|
+
.unwrap_err()
|
|
2569
|
+
.code,
|
|
2570
|
+
Code::DUPLICATE_KEY
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
#[test]
|
|
2575
|
+
fn explicit_files_refuse_a_changed_listing_length() {
|
|
2576
|
+
let (base, _, handle) = origin(2, move |request| {
|
|
2577
|
+
if request.path.contains("/tree/") {
|
|
2578
|
+
ok_body(
|
|
2579
|
+
br#"[{"type":"file","path":"config.json","size":2}]"#,
|
|
2580
|
+
"application/json",
|
|
2581
|
+
)
|
|
2582
|
+
} else {
|
|
2583
|
+
ok_body(b"changed", "application/json")
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
|
|
2587
|
+
let endpoints = Endpoints {
|
|
2588
|
+
huggingface: base,
|
|
2589
|
+
allow_local: true,
|
|
2590
|
+
..Endpoints::default()
|
|
2591
|
+
};
|
|
2592
|
+
assert_eq!(
|
|
2593
|
+
resolve_files(
|
|
2594
|
+
&uri,
|
|
2595
|
+
&["config.json".into()],
|
|
2596
|
+
&endpoints,
|
|
2597
|
+
&Anonymous,
|
|
2598
|
+
Deadline::none()
|
|
2599
|
+
)
|
|
2600
|
+
.unwrap_err()
|
|
2601
|
+
.code,
|
|
2602
|
+
Code::LENGTH_MISMATCH
|
|
2603
|
+
);
|
|
2604
|
+
handle.join().unwrap();
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
#[test]
|
|
2608
|
+
fn explicit_files_changed_after_resolution_never_finish_a_native_root() {
|
|
2609
|
+
let replies = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
2610
|
+
let (base, _, handle) = origin(3, move |request| {
|
|
2611
|
+
if request.path.contains("/tree/") {
|
|
2612
|
+
ok_body(
|
|
2613
|
+
br#"[{"type":"file","path":"config.json","size":2}]"#,
|
|
2614
|
+
"application/json",
|
|
2615
|
+
)
|
|
2616
|
+
} else {
|
|
2617
|
+
let attempt = replies.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
2618
|
+
ok_body(if attempt == 0 { b"AA" } else { b"BB" }, "application/json")
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
|
|
2622
|
+
let endpoints = Endpoints {
|
|
2623
|
+
huggingface: base,
|
|
2624
|
+
allow_local: true,
|
|
2625
|
+
..Endpoints::default()
|
|
2626
|
+
};
|
|
2627
|
+
let selected = resolve_files(
|
|
2628
|
+
&uri,
|
|
2629
|
+
&["config.json".into()],
|
|
2630
|
+
&endpoints,
|
|
2631
|
+
&Anonymous,
|
|
2632
|
+
Deadline::none(),
|
|
2633
|
+
)
|
|
2634
|
+
.unwrap();
|
|
2635
|
+
let root = temporary("changed-files");
|
|
2636
|
+
let store = Store::init(&root).unwrap();
|
|
2637
|
+
let producer = ObjectRef::of(b"changed-files-producer").id();
|
|
2638
|
+
let error = materialize_selected(
|
|
2639
|
+
&store,
|
|
2640
|
+
&producer,
|
|
2641
|
+
&selected,
|
|
2642
|
+
&endpoints.policy(&uri),
|
|
2643
|
+
&Anonymous,
|
|
2644
|
+
Deadline::none(),
|
|
2645
|
+
&mut |_, _| {},
|
|
2646
|
+
)
|
|
2647
|
+
.unwrap_err();
|
|
2648
|
+
assert_eq!(error.code, Code::OBJECT_ID_MISMATCH);
|
|
2649
|
+
assert!(!store.contains(&selected.members[0].object.sha256));
|
|
2650
|
+
assert!(
|
|
2651
|
+
!crate::source_artifact::read(&store, &producer)
|
|
2652
|
+
.unwrap()
|
|
2653
|
+
.unwrap()
|
|
2654
|
+
.complete
|
|
2655
|
+
);
|
|
2656
|
+
handle.join().unwrap();
|
|
2657
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
#[test]
|
|
2661
|
+
fn explicit_files_can_retain_an_empty_file() {
|
|
2662
|
+
let (base, _, handle) = origin(2, move |request| {
|
|
2663
|
+
if request.path.contains("/tree/") {
|
|
2664
|
+
ok_body(
|
|
2665
|
+
br#"[{"type":"file","path":"empty.py","size":0}]"#,
|
|
2666
|
+
"application/json",
|
|
2667
|
+
)
|
|
2668
|
+
} else {
|
|
2669
|
+
ok_body(b"", "text/plain")
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
|
|
2673
|
+
let endpoints = Endpoints {
|
|
2674
|
+
huggingface: base,
|
|
2675
|
+
allow_local: true,
|
|
2676
|
+
..Endpoints::default()
|
|
2677
|
+
};
|
|
2678
|
+
let selected = resolve_files(
|
|
2679
|
+
&uri,
|
|
2680
|
+
&["empty.py".into()],
|
|
2681
|
+
&endpoints,
|
|
2682
|
+
&Anonymous,
|
|
2683
|
+
Deadline::none(),
|
|
2684
|
+
)
|
|
2685
|
+
.unwrap();
|
|
2686
|
+
assert_eq!(selected.members[0].object, ObjectRef::of(b""));
|
|
2687
|
+
let root = temporary("empty-files");
|
|
2688
|
+
let store = Store::init(&root).unwrap();
|
|
2689
|
+
let producer = ObjectRef::of(b"empty-files-producer").id();
|
|
2690
|
+
let (retained, transfers) = materialize_selected(
|
|
2691
|
+
&store,
|
|
2692
|
+
&producer,
|
|
2693
|
+
&selected,
|
|
2694
|
+
&endpoints.policy(&uri),
|
|
2695
|
+
&Anonymous,
|
|
2696
|
+
Deadline::none(),
|
|
2697
|
+
&mut |_, _| {},
|
|
2698
|
+
)
|
|
2699
|
+
.unwrap();
|
|
2700
|
+
assert!(retained.complete && !retained.released);
|
|
2701
|
+
assert_eq!(transfers[0].transferred, 0);
|
|
2702
|
+
assert!(store
|
|
2703
|
+
.record_valid(&selected.members[0].object.sha256)
|
|
2704
|
+
.is_ok());
|
|
2705
|
+
handle.join().unwrap();
|
|
2706
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
2707
|
+
}
|
|
2234
2708
|
}
|