Phase 6c fix: TagStore list/get see stamped tags #73
@@ -234,7 +234,15 @@ impl TagStore {
|
||||
let path = self.tag_path(key);
|
||||
let bytes = match tokio::fs::read(&path).await {
|
||||
Ok(b) => b,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
// Phase 6c fix (2026-07-14): legacy tag file absent
|
||||
// → fall through to the stamped store so modern pins
|
||||
// are visible via the plain `get` API.
|
||||
return Ok(self
|
||||
.get_stamped(key)
|
||||
.await?
|
||||
.map(|s| s.value));
|
||||
}
|
||||
Err(e) => return Err(anyhow::Error::from(e)),
|
||||
};
|
||||
let (parsed_key, value) = decode_record(&bytes).with_context(|| {
|
||||
@@ -284,9 +292,13 @@ impl TagStore {
|
||||
let tags_root = self.root.join("tags");
|
||||
let mut entries: Vec<TagEntry> = Vec::new();
|
||||
|
||||
let mut top = tokio::fs::read_dir(&tags_root)
|
||||
.await
|
||||
.with_context(|| format!("reading {}", tags_root.display()))?;
|
||||
let mut top = match tokio::fs::read_dir(&tags_root).await {
|
||||
Ok(t) => t,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return self.list_stamped_only().await
|
||||
}
|
||||
Err(e) => return Err(e).with_context(|| format!("reading {}", tags_root.display())),
|
||||
};
|
||||
while let Some(bucket) = top.next_entry().await? {
|
||||
if !bucket.file_type().await?.is_dir() {
|
||||
continue;
|
||||
@@ -305,10 +317,63 @@ impl TagStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Phase 6c fix (2026-07-14): fold in stamped tags too.
|
||||
// Pin/PutTagVersioned writes here; list() must see them or
|
||||
// downstream consumers (FUSE tag layer, humans running
|
||||
// `list-tags`) miss modern pins entirely.
|
||||
let stamped = self.list_stamped_only().await?;
|
||||
let seen: std::collections::HashSet<String> =
|
||||
entries.iter().map(|e| e.key.clone()).collect();
|
||||
for e in stamped {
|
||||
if !seen.contains(&e.key) {
|
||||
entries.push(e);
|
||||
}
|
||||
}
|
||||
entries.sort_by(|a, b| a.key.cmp(&b.key));
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn list_stamped_only(&self) -> Result<Vec<TagEntry>> {
|
||||
let stamped_root = self.root.join("tags-v2");
|
||||
let mut entries: Vec<TagEntry> = Vec::new();
|
||||
let mut top = match tokio::fs::read_dir(&stamped_root).await {
|
||||
Ok(t) => t,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(entries),
|
||||
Err(e) => {
|
||||
return Err(e).with_context(|| format!("reading {}", stamped_root.display()))
|
||||
}
|
||||
};
|
||||
while let Some(bucket) = top.next_entry().await? {
|
||||
if !bucket.file_type().await?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let mut inner = tokio::fs::read_dir(bucket.path()).await?;
|
||||
while let Some(entry) = inner.next_entry().await? {
|
||||
if !entry.file_type().await?.is_file() {
|
||||
continue;
|
||||
}
|
||||
if entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_none_or(|n| !n.ends_with(".svtag"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let bytes = match tokio::fs::read(entry.path()).await {
|
||||
Ok(b) => b,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Ok((key, stamped)) = decode_stamped_record(&bytes) {
|
||||
entries.push(TagEntry {
|
||||
key,
|
||||
value_hex: hex32(&stamped.value),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Phase 4 (2026-07-13): union of every 32-byte value referenced
|
||||
/// by a tag in either the legacy `tags/` or the Phase-3c
|
||||
/// `tags-v2/` namespace. Feeds pin-aware eviction: any blob whose
|
||||
|
||||
Reference in New Issue
Block a user