diff --git a/apps/web/src/common/export.ts b/apps/web/src/common/export.ts index ee102e882..5d6a996d0 100644 --- a/apps/web/src/common/export.ts +++ b/apps/web/src/common/export.ts @@ -21,6 +21,7 @@ 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"; export async function exportToPDF( title: string, @@ -74,12 +75,17 @@ export async function exportNotes( title: "Exporting notes", subtitle: "Please wait while your notes are exported.", action: async (report) => { - await new ExportStream(noteIds, format, undefined, (c, text) => - report({ total: noteIds.length, current: c, text }) - ) - .pipeThrough(createZipStream()) - .pipeTo(await createWriteStream("notes.zip")); - return true; + try { + await new ExportStream(noteIds, format, undefined, (c, text) => + report({ total: noteIds.length, current: c, text }) + ) + .pipeThrough(createZipStream()) + .pipeTo(await createWriteStream("notes.zip")); + return true; + } catch (e) { + if (e instanceof Error) showToast("error", e.message); + } + return false; } }); } diff --git a/apps/web/src/utils/streams/export-stream.ts b/apps/web/src/utils/streams/export-stream.ts index 4ac0befa4..8619a68aa 100644 --- a/apps/web/src/utils/streams/export-stream.ts +++ b/apps/web/src/utils/streams/export-stream.ts @@ -40,7 +40,6 @@ export class ExportStream extends ReadableStream { signal?: AbortSignal, onProgress?: (current: number, text: string) => void ) { - const textEncoder = new TextEncoder(); let index = 0; const counters: Record = {}; let vaultUnlocked = false; @@ -60,103 +59,99 @@ export class ExportStream extends ReadableStream { } }, async pull(controller) { - if (signal?.aborted) { - controller.close(); - return; - } + try { + if (signal?.aborted) { + controller.close(); + return; + } - const note = db.notes?.note(noteIds[index++]); - if (!note) return; - if (!vaultUnlocked && note.data.locked) return; + const note = db.notes?.note(noteIds[index++]); + if (!note) return; + if (!vaultUnlocked && note.data.locked) return; - onProgress && onProgress(index, `Exporting "${note.title}"...`); + onProgress && onProgress(index, `Exporting "${note.title}"...`); - const rawContent = await db.content?.raw(note.data.contentId); - const content = note.data.locked - ? await db.vault?.decryptContent(rawContent) - : rawContent; + const rawContent = await db.content?.raw(note.data.contentId); + const content = note.data.locked + ? await db.vault?.decryptContent(rawContent) + : rawContent; - const exported = await note - .export(format === "pdf" ? "html" : format, content) - .catch((e: Error) => { - console.error(note.data, e); - showToast( - "error", - `Failed to export note "${note.title}": ${e.message}` - ); + const exported = await note + .export(format === "pdf" ? "html" : format, content) + .catch((e: Error) => { + console.error(note.data, e); + showToast( + "error", + `Failed to export note "${note.title}": ${e.message}` + ); + }); + + if (typeof exported !== "string") { + showToast("error", `Failed to export note "${note.title}"`); + return; + } + + if (format === "pdf") { + await exportToPDF(note.title, exported); + controller.error("PDF export."); + return; + } + + const filename = sanitizeFilename(note.title, { replacement: "-" }); + const ext = FORMAT_TO_EXT[format]; + const filenameWithExtension = [filename, ext].join("."); + + const notebooks = [ + ...(db.relations?.to({ id: note.id, type: "note" }, "notebook") || + []), + ...(note.notebooks || []).map( + (ref: { id: string; topics: string[] }) => { + const notebook = db.notebooks?.notebook(ref.id); + const topics: any[] = notebook?.topics.all || []; + + return { + title: notebook?.title, + topics: ref.topics + .map((topicId: string) => + topics.find((topic) => topic.id === topicId) + ) + .filter(Boolean) + }; + } + ) + ]; + + const filePaths: Array = + notebooks.length > 0 + ? notebooks + .map((notebook) => { + if (notebook.topics.length > 0) + return notebook.topics.map((topic: { title: string }) => + [ + notebook.title, + topic.title, + filenameWithExtension + ].join("/") + ); + return [notebook.title, filenameWithExtension].join("/"); + }) + .flat() + : [filenameWithExtension]; + + filePaths.forEach((filePath) => { + controller.enqueue({ + path: makeUniqueFilename(filePath, counters), + data: exported, + mtime: new Date(note.data.dateEdited), + ctime: new Date(note.data.dateCreated) + }); }); - - if (typeof exported !== "string") { - showToast("error", `Failed to export note "${note.title}"`); - return; - } - - if (format === "pdf") { - await exportToPDF(note.title, exported); - controller.close(); - return; - } - - const filename = sanitizeFilename(note.title, { replacement: "-" }); - const ext = FORMAT_TO_EXT[format]; - const notebooks = db.relations - ?.to({ id: note.id, type: "note" }, "notebook") - .map((notebook) => { - return { title: notebook.title, topics: Array }; - }); - const notebooksWithTopics: Array<{ - id: string; - topics: Array; - }> = note?.notebooks; - - if (notebooksWithTopics) - notebooks?.push( - ...notebooksWithTopics.map((_notebook) => { - const notebook = db.notebooks?.notebook(_notebook.id); - const _topics = notebook?.topics.all; - let topics: any; - - _notebook.topics.map((topicId: string) => { - topics = _topics?.filter((topic) => { - return topic.id === topicId; - }); - }); - - return { - title: notebook?.title, - topics: topics?.map((topic) => topic.title) - }; - }) - ); - - const filenameWithExtension = [filename, ext].join(".").toLowerCase(); - const filePaths: Array = []; - - if (notebooks && notebooks.length > 0) { - notebooks.forEach((notebook) => { - if (notebook.topics.length > 0) - notebook.topics.forEach((topic) => { - filePaths.push( - `/${notebook.title}/${topic}/${filenameWithExtension}` - ); - }); - else filePaths.push(`/${notebook.title}/${filenameWithExtension}`); - }); - } else { - filePaths.push(filenameWithExtension); - } - - filePaths.forEach((filePath) => { - controller.enqueue({ - path: makeUniqueFilename(filePath, counters), - data: textEncoder.encode(exported), - mtime: new Date(note.data.dateEdited), - ctime: new Date(note.data.dateCreated) - }); - }); - - if (index === noteIds.length) { - controller.close(); + } catch (e) { + controller.error(e); + } finally { + if (index === noteIds.length) { + controller.close(); + } } } }); diff --git a/apps/web/src/utils/streams/utils.ts b/apps/web/src/utils/streams/utils.ts index 350915c0d..306fc8de7 100644 --- a/apps/web/src/utils/streams/utils.ts +++ b/apps/web/src/utils/streams/utils.ts @@ -17,15 +17,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +import { path } from "@notesnook-importer/core/dist/src/utils/path"; + export function makeUniqueFilename( filePath: string, counters: Record ) { - counters[filePath] = (counters[filePath] || 0) + 1; - if (counters[filePath] === 1) return filePath; + const matchablePath = filePath.toLowerCase(); + const count = (counters[matchablePath] = (counters[matchablePath] || 0) + 1); + if (count === 1) return filePath; - const parts = filePath.split("."); - return `${parts.slice(0, -1).join(".")}-${counters[filePath]}.${ - parts[parts.length - 1] - }`; + const ext = path.extname(filePath); + const basename = ext + ? `${path.basename(filePath, ext)}-${count}${ext}` + : `${path.basename(filePath)}-${count}`; + return path.join(path.dirname(filePath), basename); } diff --git a/apps/web/src/utils/streams/zip-stream.ts b/apps/web/src/utils/streams/zip-stream.ts index e32b49098..54809cf5b 100644 --- a/apps/web/src/utils/streams/zip-stream.ts +++ b/apps/web/src/utils/streams/zip-stream.ts @@ -18,13 +18,18 @@ along with this program. If not, see . */ import { Deflate, Inflate } from "./fflate-shim"; -import { Uint8ArrayReader, ZipWriter, configure } from "@zip.js/zip.js"; +import { + Uint8ArrayReader, + TextReader, + ZipWriter, + configure +} from "@zip.js/zip.js"; configure({ Deflate, Inflate }); export type ZipFile = { path: string; - data: Uint8Array; + data: string | Uint8Array; mtime?: Date; ctime?: Date; }; @@ -43,10 +48,16 @@ export function createZipStream(signal?: AbortSignal) { if (written.has(chunk.path)) return; await writer - .add(chunk.path, new Uint8ArrayReader(chunk.data), { - creationDate: chunk.ctime, - lastModDate: chunk.mtime - }) + .add( + chunk.path, + typeof chunk.data === "string" + ? new TextReader(chunk.data) + : new Uint8ArrayReader(chunk.data), + { + creationDate: chunk.ctime, + lastModDate: chunk.mtime + } + ) .catch(async (e) => { await ts.writable.abort(e); await ts.readable.cancel(e); diff --git a/packages/common/src/utils/file.ts b/packages/common/src/utils/file.ts index c9c183fd7..29103471b 100644 --- a/packages/common/src/utils/file.ts +++ b/packages/common/src/utils/file.ts @@ -75,7 +75,7 @@ function sanitize(input: string, replacement: string) { .replace(windowsReservedRe, replacement) .replace(windowsTrailingRe, replacement); - return sanitized.slice(0, 254).toLowerCase(); + return sanitized.slice(0, 254); } export function sanitizeFilename( diff --git a/packages/core/src/collections/content.js b/packages/core/src/collections/content.js index 3823fd59a..564c17540 100644 --- a/packages/core/src/collections/content.js +++ b/packages/core/src/collections/content.js @@ -83,6 +83,8 @@ export default class Content extends Collection { } async raw(id) { + if (!id) return; + const content = await this._collection.getItem(id); if (!content) return; return content; @@ -118,6 +120,7 @@ export default class Content extends Collection { async downloadMedia(groupId, contentItem, notify = true) { if (!contentItem) return contentItem; const content = getContentFromData(contentItem.type, contentItem.data); + if (!content) console.log(contentItem); contentItem.data = await content.insertMedia(async (hashes) => { const attachments = hashes.map((h) => this._db.attachments.attachment(h)); await this._db.fs.queueDownloads(