mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
[monorepo] read empty content from its hash, in a shared blob store base (#36831)
The hash of empty content is a property of the content rather than of anything stored: every project's empty file has it, whether or not a blob was ever written under it, and a file may reference it before anything has. Eight blob stores were each paying a lookup -- a database query, an HTTP request, a read of a cache that offline mode may be the only thing standing in for -- to be told what the hash already says, and clsi had already reinvented the answer on its own. overleaf-editor-core gains BlobStoreBase, which answers getString for that hash and delegates the rest to the fetchString its subclasses provide. getObject comes with it: four stores spelled out the same JSON.parse of getString, and a fifth wrapped it in a log line. Adopted by history-v1's BlobStore, BackupBlobStore, HashCheckBlobStore and recover_zip's RecoveryBlobStore, by project-history's BlobStore, by clsi's BlobStore, and by web's HistoryBlobStore, SimpleBlobStore and FrontendBlobStore. history-v1 keeps its own answers in getStream, getBlob and getBlobs, which no other store implements. Writing and copying a blob are untouched, so a project that wrote empty content still has a blob for it and a read answers the same either way. Two paths could not handle a project holding a tracked change or a comment at all. Loading such a file needs the ranges blob its rangesHash points at, and hashing one again needs to write it back, so a store missing getObject or putObject fails outright: - recover_zip died on `blobStore.getObject is not a function`, making every such project unrecoverable. The test covering it asserts a tracked delete is absent from the zip, which the script already asked for and never reached. - GET /projects/:project_id/latest/hashed_content returned 500 from the same cause, then would have hit a missing putObject in snapshot.store. HashCheckBlobStore becomes a class over the base and gains putObject. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> GitOrigin-RevId: c84f62465beff6cf77ffac26aa772984e1b554cb
This commit is contained in:
committed by
Copybot
co-authored by
Claude Opus 5
parent
7608d738a6
commit
6caa5fc3a3
@@ -0,0 +1,95 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
|
||||
const { Blob, BlobStoreBase } = require('../..')
|
||||
|
||||
class TestBlobStore extends BlobStoreBase {
|
||||
constructor(contentByHash = {}) {
|
||||
super()
|
||||
this.contentByHash = contentByHash
|
||||
this.fetched = []
|
||||
}
|
||||
|
||||
async fetchString(hash) {
|
||||
this.fetched.push(hash)
|
||||
if (!(hash in this.contentByHash)) {
|
||||
throw new Error(`nothing stored under ${hash}`)
|
||||
}
|
||||
return this.contentByHash[hash]
|
||||
}
|
||||
}
|
||||
|
||||
const HASH = 'a1b2c3d4e5f60718293a4b5c6d7e8f9012345678'
|
||||
|
||||
describe('BlobStoreBase', function () {
|
||||
describe('getString', function () {
|
||||
it('fetches the content of a hash something stored', async function () {
|
||||
const blobStore = new TestBlobStore({ [HASH]: 'hello world' })
|
||||
|
||||
expect(await blobStore.getString(HASH)).to.equal('hello world')
|
||||
expect(blobStore.fetched).to.deep.equal([HASH])
|
||||
})
|
||||
|
||||
it('answers the hash of empty content without fetching', async function () {
|
||||
// The store below has nothing stored under it and throws when asked, which is
|
||||
// the point: the hash says what the content is, so no store has to be asked.
|
||||
const blobStore = new TestBlobStore()
|
||||
|
||||
expect(await blobStore.getString(Blob.EMPTY_HASH)).to.equal('')
|
||||
expect(blobStore.fetched).to.have.length(0)
|
||||
})
|
||||
|
||||
it('passes on what the implementation throws', async function () {
|
||||
const blobStore = new TestBlobStore()
|
||||
|
||||
try {
|
||||
await blobStore.getString(HASH)
|
||||
expect.fail('should have thrown')
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal(`nothing stored under ${HASH}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getObject', function () {
|
||||
it('deserializes the content of the hash', async function () {
|
||||
const blobStore = new TestBlobStore({
|
||||
[HASH]: '{"comments":[],"trackedChanges":[]}',
|
||||
})
|
||||
|
||||
expect(await blobStore.getObject(HASH)).to.deep.equal({
|
||||
comments: [],
|
||||
trackedChanges: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('fails on the hash of empty content, which is not JSON', async function () {
|
||||
// Nothing writes an empty ranges blob -- JSON.stringify never yields empty
|
||||
// content -- so a hash reaching here is one something stored.
|
||||
const blobStore = new TestBlobStore()
|
||||
|
||||
try {
|
||||
await blobStore.getObject(Blob.EMPTY_HASH)
|
||||
expect.fail('should have thrown')
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(SyntaxError)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchString', function () {
|
||||
it('names the store that has not implemented it', async function () {
|
||||
class IncompleteBlobStore extends BlobStoreBase {}
|
||||
|
||||
try {
|
||||
await new IncompleteBlobStore().getString(HASH)
|
||||
expect.fail('should have thrown')
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal(
|
||||
'IncompleteBlobStore does not implement fetchString(hash)'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user