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({
@@ -96,32 +96,6 @@ export class BlobModel extends BaseModel {
return count > 0;
}
async listPendingExpired(before: Date) {
return await this.db.blob.findMany({
where: {
status: 'pending',
deletedAt: null,
createdAt: {
lt: before,
},
},
select: {
workspaceId: true,
key: true,
uploadId: true,
},
});
}
async listDeleted(workspaceId: string) {
return await this.db.blob.findMany({
where: {
workspaceId,
deletedAt: { not: null },
},
});
}
async totalSize(workspaceId: string) {
const sum = await this.db.blob.aggregate({
where: {
@@ -162,21 +162,4 @@ export class HistoryModel extends BaseModel {
editor: row.createdByUser,
};
}
/**
* Clean expired histories.
*/
async cleanExpired() {
const { count } = await this.db.snapshotHistory.deleteMany({
where: {
expiredAt: {
lte: new Date(),
},
},
});
if (count > 0) {
this.logger.log(`Deleted ${count} expired histories`);
}
return count;
}
}
@@ -148,17 +148,4 @@ export class SessionModel extends BaseModel {
}
return count;
}
async cleanExpiredUserSessions() {
const { count } = await this.db.userSession.deleteMany({
where: {
expiresAt: {
lte: new Date(),
},
},
});
if (count > 0) {
this.logger.log(`Cleaned ${count} expired user sessions`);
}
}
}