diff --git a/packages/core/__mocks__/node-storage.mock.ts b/packages/core/__mocks__/node-storage.mock.ts index a986866f1..0677e125d 100644 --- a/packages/core/__mocks__/node-storage.mock.ts +++ b/packages/core/__mocks__/node-storage.mock.ts @@ -24,6 +24,7 @@ import { SerializedKeyPair } from "@notesnook/crypto"; import { IStorage } from "../src/interfaces.js"; +import { randomBytes } from "crypto"; export class NodeStorageInterface implements IStorage { storage = {}; @@ -112,7 +113,7 @@ export class NodeStorageInterface implements IStorage { password: string, salt?: string | undefined ): Promise { - return { password, salt }; + return { password, salt: salt || randomBytes(16).toString("base64") }; } generateCryptoKeyPair(): Promise { diff --git a/packages/core/__tests__/trash.test.ts b/packages/core/__tests__/trash.test.ts index 699a88f8f..36267d045 100644 --- a/packages/core/__tests__/trash.test.ts +++ b/packages/core/__tests__/trash.test.ts @@ -23,7 +23,8 @@ import { notebookTest, TEST_NOTE, TEST_NOTEBOOK, - databaseTest + databaseTest, + loginFakeUser } from "./utils/index.js"; import { test, expect } from "vitest"; @@ -384,3 +385,20 @@ test("permanently deleted note should not have note fields", () => "synced" ]); })); + +test("trash cleanup should remove orphaned attachments", () => + databaseTest().then(async (db) => { + await loginFakeUser(db); + + const hash = await db.attachments.save( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "image/png", + "test.png" + ); + if (!hash) throw new Error("Failed to create attachment"); + + await db.trash.cleanup(); + + expect(await db.attachments.exists(hash)).toBe(false); + expect(await db.attachments.orphaned.count()).toBe(0); + })); diff --git a/packages/core/__tests__/utils/index.ts b/packages/core/__tests__/utils/index.ts index 8f3a2fe1b..5cea88224 100644 --- a/packages/core/__tests__/utils/index.ts +++ b/packages/core/__tests__/utils/index.ts @@ -109,18 +109,17 @@ function delay(ms: number) { async function loginFakeUser(db) { const email = "johndoe@example.com"; + const password = "password"; const userSalt = randomBytes(16).toString("base64"); await db.storage().deriveCryptoKey({ - password: "password", + password, salt: userSalt }); - const userEncryptionKey = await db.storage().getCryptoKey(`_uk_@${email}`); - const key = await db.crypto().generateRandomKey(); const attachmentsKey = await db .storage() - .encrypt({ password: userEncryptionKey }, JSON.stringify(key)); + .encrypt({ password, salt: userSalt }, JSON.stringify(key)); await db.user.setUser({ email, diff --git a/packages/core/src/collections/attachments.ts b/packages/core/src/collections/attachments.ts index 0a966ccf5..0f6a82e80 100644 --- a/packages/core/src/collections/attachments.ts +++ b/packages/core/src/collections/attachments.ts @@ -574,6 +574,14 @@ export class Attachments implements ICollection { ); return this.key; } + + async removeOrphaned() { + for await (const attachment of this.db.attachments.orphaned.iterate()) { + try { + await this.db.attachments.remove(attachment.hash, false); + } catch (error) {} + } + } } export function getOutputType(attachment: Attachment): DataFormat { diff --git a/packages/core/src/collections/trash.ts b/packages/core/src/collections/trash.ts index afbc0a286..73915e302 100644 --- a/packages/core/src/collections/trash.ts +++ b/packages/core/src/collections/trash.ts @@ -121,8 +121,10 @@ export default class Trash { }, { noteIds: [] as string[], notebookIds: [] as string[] } ); - await this._delete(noteIds, notebookIds); + + await this.db.attachments.removeOrphaned(); + await this.buildCache(); }