diff --git a/apps/web/src/common/export.ts b/apps/web/src/common/export.ts index b3b31160c..79a7a1b11 100644 --- a/apps/web/src/common/export.ts +++ b/apps/web/src/common/export.ts @@ -21,15 +21,18 @@ import { TaskManager } from "./task-manager"; import { ExportStream } from "../utils/streams/export-stream"; import { createZipStream } from "../utils/streams/zip-stream"; import { createWriteStream } from "../utils/stream-saver"; -import { showToast } from "../utils/toast"; -import Vault from "./vault"; -import { db } from "./db"; -import { sanitizeFilename } from "@notesnook/common"; -import { Note, isDeleted } from "@notesnook/core/dist/types"; import { isEncryptedContent, isUnencryptedContent } from "@notesnook/core/dist/collections/content"; +import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection"; +import { Note, isDeleted } from "@notesnook/core"; +import { fromAsyncIterator } from "../utils/stream"; +import { db } from "./db"; +import { ExportOptions } from "@notesnook/core/dist/collections/notes"; +import { showToast } from "../utils/toast"; +import { sanitizeFilename } from "@notesnook/common"; +import Vault from "./vault"; export async function exportToPDF( title: string, @@ -76,51 +79,23 @@ export async function exportToPDF( export async function exportNotes( format: "pdf" | "md" | "txt" | "html" | "md-frontmatter", - noteIds: string[] + notes: FilteredSelector ): Promise { return await TaskManager.startTask({ type: "modal", title: "Exporting notes", subtitle: "Please wait while your notes are exported.", action: async (report) => { - try { - let progress = 0; - await createNoteStream(noteIds) - .pipeThrough( - new TransformStream({ - transform(note, controller) { - controller.enqueue(note); - report({ - total: noteIds.length, - current: progress++, - text: `Exporting "${note?.title || "Unknown note"}"` - }); - } - }) + const noteStream = fromAsyncIterator(notes[Symbol.asyncIterator]()); + await noteStream + .pipeThrough( + new ExportStream(format, undefined, (c, text) => + report({ current: c, text }) ) - .pipeThrough(new ExportStream(format)) - .pipeThrough(createZipStream()) - .pipeTo(await createWriteStream("notes.zip")); - return true; - } catch (e) { - if (e instanceof Error) showToast("error", e.message); - } - return false; - } - }); -} - -function createNoteStream(noteIds: string[]) { - let i = 0; - return new ReadableStream({ - start() {}, - async pull(controller) { - const noteId = noteIds[i++]; - if (!noteId) controller.close(); - else controller.enqueue(db.notes?.note(noteId)?.data); - }, - async cancel(reason) { - throw new Error(reason); + ) + .pipeThrough(createZipStream()) + .pipeTo(await createWriteStream("notes.zip")); + return true; } }); } @@ -135,35 +110,32 @@ const FORMAT_TO_EXT = { export async function exportNote( note: Note, - format: keyof typeof FORMAT_TO_EXT, - disableTemplate = false + options: Omit & { + format: keyof typeof FORMAT_TO_EXT; + } ) { - if (!db.vault?.unlocked && note.locked && !(await Vault.unlockVault())) { + if (!db.vault.unlocked && note.locked && !(await Vault.unlockVault())) { showToast("error", `Skipping note "${note.title}" as it is locked.`); return false; } - const rawContent = note.contentId - ? await db.content.raw(note.contentId) - : null; + ? await db.content.get(note.contentId) + : undefined; const content = - !rawContent || isDeleted(rawContent) - ? undefined - : isEncryptedContent(rawContent) - ? await db.vault.decryptContent(rawContent) - : isUnencryptedContent(rawContent) - ? rawContent - : undefined; + rawContent && + !isDeleted(rawContent) && + (rawContent.locked + ? await db.vault.decryptContent(rawContent, note.id) + : rawContent); const exported = await db.notes .export(note.id, { - format: format === "pdf" ? "html" : format, - contentItem: content, - disableTemplate + ...options, + format: options.format === "pdf" ? "html" : options.format, + contentItem: content || undefined }) .catch((e: Error) => { - console.error(note, e); showToast("error", `Failed to export note "${note.title}": ${e.message}`); return false as const; }); @@ -171,7 +143,7 @@ export async function exportNote( if (typeof exported === "boolean" && !exported) return false; const filename = sanitizeFilename(note.title, { replacement: "-" }); - const ext = FORMAT_TO_EXT[format]; + const ext = FORMAT_TO_EXT[options.format]; return { filename: [filename, ext].join("."), content: exported diff --git a/apps/web/src/components/note/index.tsx b/apps/web/src/components/note/index.tsx index 00ef54d70..68e28771b 100644 --- a/apps/web/src/components/note/index.tsx +++ b/apps/web/src/components/note/index.tsx @@ -95,7 +95,7 @@ import { TagsWithDateEdited } from "../list-container/types"; import { SchemeColors } from "@notesnook/theme"; -import Vault from "../../common/vault"; +import FileSaver from "file-saver"; type NoteProps = { tags?: TagsWithDateEdited; @@ -432,10 +432,7 @@ const menuItems: ( //isDisabled: !isSynced, icon: Print.path, onClick: async () => { - const item = db.notes?.note(note); - if (!item) return; - - const result = await exportNote(item, "pdf"); + const result = await exportNote(note, { format: "pdf" }); if (!result) return; await exportToPDF(note.title, result.content); } @@ -474,10 +471,7 @@ const menuItems: ( isPro: format.type !== "txt", onClick: async () => { if (ids.length === 1) { - const item = db.notes?.note(note); - if (!item) return; - - const result = await exportNote(item, format.type); + const result = await exportNote(note, { format: format.type }); if (!result) return; if (format.type === "pdf") return exportToPDF(note.title, result.content); @@ -488,7 +482,10 @@ const menuItems: ( ); } - await exportNotes(format.type, ids); + await exportNotes( + format.type, + db.notes.all.where((eb) => eb("id", "in", ids)) + ); } })) }, @@ -766,8 +763,8 @@ async function copyNote(noteId: string, format: "md" | "txt") { const note = await db.notes?.note(noteId); if (!note) throw new Error("No note with this id exists."); - const result = await exportNote(note, format, true); - if (!result) return; + const result = await exportNote(note, { format, disableTemplate: true }); + if (!result) throw new Error(`Could not convert note to ${format}.`); await navigator.clipboard.writeText(result.content); showToast("success", "Copied!"); diff --git a/apps/web/src/dialogs/settings/backup-export-settings.ts b/apps/web/src/dialogs/settings/backup-export-settings.ts index 85a823bac..48b02f01e 100644 --- a/apps/web/src/dialogs/settings/backup-export-settings.ts +++ b/apps/web/src/dialogs/settings/backup-export-settings.ts @@ -183,7 +183,7 @@ export const BackupExportSettings: SettingsGroup[] = [ if (await verifyAccount()) await exportNotes( value as "txt" | "md" | "html" | "md-frontmatter", - await db.notes.all.ids() + db.notes.all ); } } diff --git a/apps/web/src/utils/stream.ts b/apps/web/src/utils/stream.ts index 67dc98ee6..f34f23378 100644 --- a/apps/web/src/utils/stream.ts +++ b/apps/web/src/utils/stream.ts @@ -27,3 +27,20 @@ export async function consumeReadableStream( } return chunks; } + +export function fromAsyncIterator( + iterator: AsyncIterableIterator +): ReadableStream { + return new ReadableStream({ + start() {}, + async pull(controller) { + const result = await iterator.next(); + if (result.done) controller.close(); + else if (result.value) controller.enqueue(result.value); + }, + async cancel(reason) { + if (iterator.throw) await iterator.throw(reason); + else throw new Error(reason); + } + }); +} diff --git a/apps/web/src/utils/streams/export-stream.ts b/apps/web/src/utils/streams/export-stream.ts index 6df872d71..642c7f673 100644 --- a/apps/web/src/utils/streams/export-stream.ts +++ b/apps/web/src/utils/streams/export-stream.ts @@ -17,11 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { Note } from "@notesnook/core/dist/types"; -import { db } from "../../common/db"; import { exportNote } from "../../common/export"; import { makeUniqueFilename } from "./utils"; import { ZipFile } from "./zip-stream"; +import { Note } from "@notesnook/core"; export class ExportStream extends TransformStream { constructor( diff --git a/packages/core/src/api/vault.ts b/packages/core/src/api/vault.ts index 08d428d5a..c28c3e2bc 100644 --- a/packages/core/src/api/vault.ts +++ b/packages/core/src/api/vault.ts @@ -37,9 +37,9 @@ export const VAULT_ERRORS = { const ERASE_TIME = 1000 * 60 * 30; export default class Vault { - vaultPassword?: string; - erasureTimeout = 0; - key = "svvaads1212#2123"; + private vaultPassword?: string; + private erasureTimeout = 0; + private key = "svvaads1212#2123"; private get password() { return this.vaultPassword; @@ -212,6 +212,10 @@ export default class Vault { return !!vaultKey && isCipher(vaultKey); } + get unlocked() { + return !!this.vaultPassword; + } + // Private & internal methods private async getVaultPassword() { diff --git a/packages/core/src/collections/notes.ts b/packages/core/src/collections/notes.ts index 4649626a1..f13372d2a 100644 --- a/packages/core/src/collections/notes.ts +++ b/packages/core/src/collections/notes.ts @@ -32,7 +32,7 @@ import { NoteContent } from "./session-content"; import { SQLCollection } from "../database/sql-collection"; import { isFalse } from "../database"; -type ExportOptions = { +export type ExportOptions = { format: "html" | "md" | "txt" | "md-frontmatter"; contentItem?: NoteContent; rawContent?: string;