web: fix pdf exports

This commit is contained in:
Abdullah Atta
2024-01-17 18:26:33 +05:00
committed by Abdullah Atta
parent a20a683b17
commit 05cc947cb4
6 changed files with 131 additions and 112 deletions

View File

@@ -21,6 +21,7 @@ import { TaskManager } from "./task-manager";
import { ExportStream } from "../utils/streams/export-stream"; import { ExportStream } from "../utils/streams/export-stream";
import { createZipStream } from "../utils/streams/zip-stream"; import { createZipStream } from "../utils/streams/zip-stream";
import { createWriteStream } from "../utils/stream-saver"; import { createWriteStream } from "../utils/stream-saver";
import { showToast } from "../utils/toast";
export async function exportToPDF( export async function exportToPDF(
title: string, title: string,
@@ -74,12 +75,17 @@ export async function exportNotes(
title: "Exporting notes", title: "Exporting notes",
subtitle: "Please wait while your notes are exported.", subtitle: "Please wait while your notes are exported.",
action: async (report) => { action: async (report) => {
await new ExportStream(noteIds, format, undefined, (c, text) => try {
report({ total: noteIds.length, current: c, text }) await new ExportStream(noteIds, format, undefined, (c, text) =>
) report({ total: noteIds.length, current: c, text })
.pipeThrough(createZipStream()) )
.pipeTo(await createWriteStream("notes.zip")); .pipeThrough(createZipStream())
return true; .pipeTo(await createWriteStream("notes.zip"));
return true;
} catch (e) {
if (e instanceof Error) showToast("error", e.message);
}
return false;
} }
}); });
} }

View File

@@ -40,7 +40,6 @@ export class ExportStream extends ReadableStream<ZipFile> {
signal?: AbortSignal, signal?: AbortSignal,
onProgress?: (current: number, text: string) => void onProgress?: (current: number, text: string) => void
) { ) {
const textEncoder = new TextEncoder();
let index = 0; let index = 0;
const counters: Record<string, number> = {}; const counters: Record<string, number> = {};
let vaultUnlocked = false; let vaultUnlocked = false;
@@ -60,103 +59,99 @@ export class ExportStream extends ReadableStream<ZipFile> {
} }
}, },
async pull(controller) { async pull(controller) {
if (signal?.aborted) { try {
controller.close(); if (signal?.aborted) {
return; controller.close();
} return;
}
const note = db.notes?.note(noteIds[index++]); const note = db.notes?.note(noteIds[index++]);
if (!note) return; if (!note) return;
if (!vaultUnlocked && note.data.locked) 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 rawContent = await db.content?.raw(note.data.contentId);
const content = note.data.locked const content = note.data.locked
? await db.vault?.decryptContent(rawContent) ? await db.vault?.decryptContent(rawContent)
: rawContent; : rawContent;
const exported = await note const exported = await note
.export(format === "pdf" ? "html" : format, content) .export(format === "pdf" ? "html" : format, content)
.catch((e: Error) => { .catch((e: Error) => {
console.error(note.data, e); console.error(note.data, e);
showToast( showToast(
"error", "error",
`Failed to export note "${note.title}": ${e.message}` `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<string> =
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)
});
}); });
} catch (e) {
if (typeof exported !== "string") { controller.error(e);
showToast("error", `Failed to export note "${note.title}"`); } finally {
return; if (index === noteIds.length) {
} controller.close();
}
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<string> };
});
const notebooksWithTopics: Array<{
id: string;
topics: Array<string>;
}> = 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<string> = [];
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();
} }
} }
}); });

View File

@@ -17,15 +17,19 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { path } from "@notesnook-importer/core/dist/src/utils/path";
export function makeUniqueFilename( export function makeUniqueFilename(
filePath: string, filePath: string,
counters: Record<string, number> counters: Record<string, number>
) { ) {
counters[filePath] = (counters[filePath] || 0) + 1; const matchablePath = filePath.toLowerCase();
if (counters[filePath] === 1) return filePath; const count = (counters[matchablePath] = (counters[matchablePath] || 0) + 1);
if (count === 1) return filePath;
const parts = filePath.split("."); const ext = path.extname(filePath);
return `${parts.slice(0, -1).join(".")}-${counters[filePath]}.${ const basename = ext
parts[parts.length - 1] ? `${path.basename(filePath, ext)}-${count}${ext}`
}`; : `${path.basename(filePath)}-${count}`;
return path.join(path.dirname(filePath), basename);
} }

View File

@@ -18,13 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Deflate, Inflate } from "./fflate-shim"; 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 }); configure({ Deflate, Inflate });
export type ZipFile = { export type ZipFile = {
path: string; path: string;
data: Uint8Array; data: string | Uint8Array;
mtime?: Date; mtime?: Date;
ctime?: Date; ctime?: Date;
}; };
@@ -43,10 +48,16 @@ export function createZipStream(signal?: AbortSignal) {
if (written.has(chunk.path)) return; if (written.has(chunk.path)) return;
await writer await writer
.add(chunk.path, new Uint8ArrayReader(chunk.data), { .add(
creationDate: chunk.ctime, chunk.path,
lastModDate: chunk.mtime typeof chunk.data === "string"
}) ? new TextReader(chunk.data)
: new Uint8ArrayReader(chunk.data),
{
creationDate: chunk.ctime,
lastModDate: chunk.mtime
}
)
.catch(async (e) => { .catch(async (e) => {
await ts.writable.abort(e); await ts.writable.abort(e);
await ts.readable.cancel(e); await ts.readable.cancel(e);

View File

@@ -75,7 +75,7 @@ function sanitize(input: string, replacement: string) {
.replace(windowsReservedRe, replacement) .replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement); .replace(windowsTrailingRe, replacement);
return sanitized.slice(0, 254).toLowerCase(); return sanitized.slice(0, 254);
} }
export function sanitizeFilename( export function sanitizeFilename(

View File

@@ -83,6 +83,8 @@ export default class Content extends Collection {
} }
async raw(id) { async raw(id) {
if (!id) return;
const content = await this._collection.getItem(id); const content = await this._collection.getItem(id);
if (!content) return; if (!content) return;
return content; return content;
@@ -118,6 +120,7 @@ export default class Content extends Collection {
async downloadMedia(groupId, contentItem, notify = true) { async downloadMedia(groupId, contentItem, notify = true) {
if (!contentItem) return contentItem; if (!contentItem) return contentItem;
const content = getContentFromData(contentItem.type, contentItem.data); const content = getContentFromData(contentItem.type, contentItem.data);
if (!content) console.log(contentItem);
contentItem.data = await content.insertMedia(async (hashes) => { contentItem.data = await content.insertMedia(async (hashes) => {
const attachments = hashes.map((h) => this._db.attachments.attachment(h)); const attachments = hashes.map((h) => this._db.attachments.attachment(h));
await this._db.fs.queueDownloads( await this._db.fs.queueDownloads(