feat(server): blob reconciliation (#15165)

#### PR Dependency Tree


* **PR #15165** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added automated backend maintenance for missing blob metadata
backfill, document-to-blob reference rebuilding, and unreferenced blob
cleanup planning/execution.
* Introduced scheduled batch processing (workspace-paged) and paginated
object-storage listing.
* **Bug Fixes**
* Improved reliability of object-storage reads by treating expected “not
found” results as non-errors.
* Strengthened blob/expired cleanup flows with runtime-driven batching
and reduced coupling to metadata synchronization.
* **Tests**
* Expanded unit and e2e coverage for partial blob metadata and updated
runtime/job cleanup test assertions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-06-29 00:02:38 +08:00
committed by GitHub
parent 4a7c931eca
commit 0a422aa158
42 changed files with 2494 additions and 264 deletions
@@ -105,31 +105,6 @@ test('should list blobs', async t => {
t.is(blobs[1].key, blob2.key);
});
test('should list deleted blobs', async t => {
const workspace = await module.create(Mockers.Workspace);
const blob = await models.blob.upsert({
workspaceId: workspace.id,
key: 'test-key',
mime: 'text/plain',
size: 100,
});
await models.blob.delete(workspace.id, blob.key);
const blobs = await models.blob.listDeleted(workspace.id);
t.is(blobs.length, 1);
t.is(blobs[0].key, blob.key);
t.truthy(blobs[0].deletedAt);
// delete permanently
await models.blob.delete(workspace.id, blob.key, true);
const blobs2 = await models.blob.listDeleted(workspace.id);
t.is(blobs2.length, 0);
});
test('should get blob', async t => {
const workspace = await module.create(Mockers.Workspace);
const blob = await models.blob.upsert({