Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/app/src/runtime/persistence/drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,50 @@ describe("draft store text externalization", () => {
expect(new Uint8Array(await memory.blobs.get(image.id)!.arrayBuffer())).toEqual(new Uint8Array([1, 2, 3]))
})

test("a pasted file stays readable after its file on disk is deleted", async () => {
// Clipboard tools hand over temp files and delete them on the next copy; reads then fail.
class DiskFile extends File {
gone = false

private check() {
if (this.gone) throw new DOMException("file is gone", "NotReadableError")
}

override arrayBuffer() {
this.check()

return super.arrayBuffer()
}

override text() {
this.check()

return super.text()
}

override stream() {
this.check()

return super.stream()
}

override slice(...args: Parameters<File["slice"]>) {
this.check()

return super.slice(...args)
}
}

const memory = memoryDriver()
const store = createDraftStore(memory.driver)
const file = new DiskFile([new Uint8Array([1, 2, 3])], "1793877187508_d.png", { type: "image/png" })
const image = await store.putBlob(file)
file.gone = true
// happy-dom's FileReader reads a blob's internal buffer, so only a public read can notice the deleted file.
expect(new Uint8Array(await memory.blobs.get(image.id)!.arrayBuffer())).toEqual(new Uint8Array([1, 2, 3]))
expect(await blobDataUrl(image, "image/png")).toBe("data:image/png;base64,AQID")
})

test("the previous document stays visible until missing blobs are restored", async () => {
const memory = memoryDriver()
const store = createDraftStore(memory.driver)
Expand Down
13 changes: 11 additions & 2 deletions packages/app/src/runtime/persistence/drafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,15 @@ async function blobID(blob: Blob) {
return id
}

export async function createBlobReference(blob: Blob): Promise<BlobReference> {
// A pasted or dropped File can be backed by a file on disk that the browser reads lazily. Clipboard
// tools delete such temp files once something else is copied, and the preview and the send would
// then fail to read it, so keep the bytes in memory from the start.
async function detach(blob: Blob) {
return blob instanceof File ? new Blob([await blob.arrayBuffer()], { type: blob.type }) : blob
}

export async function createBlobReference(source: Blob): Promise<BlobReference> {
const blob = await detach(source)
const id = await blobID(blob)

return { id, url: blobUrl(id, blob) }
Expand Down Expand Up @@ -220,7 +228,8 @@ export function createDraftStore(driver: Driver, options: { grace?: number } = {

storeLoaders.set(storeId, loadBlobUrl)

const putBlob = async (blob: Blob) => {
const putBlob = async (source: Blob) => {
const blob = await detach(source)
const id = await driver.putBlob(blob)

return { id, url: blobUrl(id, blob, grace) }
Expand Down
Loading