Files
notesnook/apps/mobile/app/services/exporter.ts
2025-04-10 10:28:59 +05:00

373 lines
9.6 KiB
TypeScript

/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
//@ts-ignore
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
import * as ScopedStorage from "react-native-scoped-storage";
import { zip } from "react-native-zip-archive";
import { DatabaseLogger } from "../common/database/index";
import {
exportNote as _exportNote,
ExportableAttachment,
ExportableNote,
exportNotes
} from "@notesnook/common";
import { FilteredSelector, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { basename, dirname, extname, join } from "pathe";
import filesystem from "../common/filesystem";
import downloadAttachment from "../common/filesystem/download-attachment";
import { cacheDir } from "../common/filesystem/utils";
import { unlockVault } from "../utils/unlock-vault";
import { useUserStore } from "../stores/use-user-store";
const FolderNames: { [name: string]: string } = {
txt: "Text",
pdf: "PDF",
md: "Markdown",
html: "Html"
};
async function getPath(type: string) {
let path =
Platform.OS === "ios" &&
(await filesystem.checkAndCreateDir(`/exported/${type}/`));
if (Platform.OS === "android") {
useUserStore.setState({
disableAppLockRequests: true
});
const file = await ScopedStorage.openDocumentTree(true);
setTimeout(() => {
useUserStore.setState({
disableAppLockRequests: false
});
}, 1000);
if (!file) return;
path = file.uri;
}
return path;
}
async function unlockVaultForNoteExport() {
return await unlockVault({
title: strings.unlockNotes(),
paragraph: strings.exportedNotesLocked(),
context: "export-notes"
});
}
function copyFileAsync(source: string, dest: string) {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, () => {
resolve(true);
});
});
}
async function resolveFileFunctions(
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter"
) {
const path = await getPath(FolderNames[type]);
if (!path) return;
const exportCacheFolder = join(
RNFetchBlob.fs.dirs.CacheDir,
`/export_${Date.now()}`
);
await RNFetchBlob.fs.mkdir(exportCacheFolder).catch((e) => {});
const mkdir = async (dir: string) => {
const folder = join(exportCacheFolder, dir);
if (!(await RNFetchBlob.fs.exists(folder))) {
await RNFetchBlob.fs.mkdir(folder);
}
};
const writeFile = async (path: string, result: string) => {
const cacheFilePath = join(exportCacheFolder, path);
await RNFetchBlob.fs.writeFile(
cacheFilePath,
result,
type === "pdf" ? "base64" : "utf8"
);
};
return {
path,
cacheFolder: exportCacheFolder,
mkdir,
writeFile
};
}
async function exportNoteToFile(
item: ExportableNote,
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
mkdir: (dir: string) => Promise<void>,
writeFile: (path: string, result: string) => Promise<void>
) {
const dir = dirname(item.path);
const filename = basename(item.path);
await mkdir(dir);
if (type === "pdf") {
const options = {
html: item.data,
fileName: Platform.OS === "ios" ? "/exported/PDF/" + filename : filename,
width: 595,
height: 852,
bgColor: "#FFFFFF",
padding: 30,
base64: true
} as { [name: string]: any };
if (Platform.OS === "ios") {
options.directory = "Documents";
}
const res = await RNHTMLtoPDF.convert(options);
if (res.filePath) {
RNFetchBlob.fs.unlink(res.filePath);
}
await writeFile(item.path, res.base64);
} else {
await writeFile(item.path, item.data);
}
}
async function exportAttachmentToFile(
item: ExportableAttachment,
mkdir: (dir: string) => Promise<void>,
cacheFolder: string
) {
const dir = dirname(item.path);
await mkdir(dir);
const cacheFileName = await downloadAttachment(item.data.hash, true, {
silent: true,
cache: true,
throwError: false
} as any);
await RNFetchBlob.fs.mv(
join(cacheDir, cacheFileName as string),
join(cacheFolder, item.path)
);
}
async function bulkExport(
notes: FilteredSelector<Note>,
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
callback: (progress?: string) => void
) {
const totalNotes = await notes.count();
const fileFuncions = await resolveFileFunctions(type);
if (!fileFuncions) return;
const path = fileFuncions.path;
const { mkdir, writeFile, cacheFolder } = fileFuncions;
let currentProgress = 0;
let currentAttachmentProgress = 0;
for await (const item of exportNotes(notes, {
format: type,
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) {
if (item instanceof Error) {
DatabaseLogger.error(item);
continue;
}
if (item.type === "note") {
currentProgress += 1;
callback(`Exporting notes (${currentProgress}/${totalNotes})`);
try {
await exportNoteToFile(item, type, mkdir, writeFile);
} catch (e) {
/* empty */
}
} else if (item.type === "attachment") {
currentAttachmentProgress += 1;
callback(`Downloading attachments (${currentAttachmentProgress})`);
try {
await exportAttachmentToFile(item, mkdir, cacheFolder);
} catch (e) {
/* empty */
}
}
}
return createZip(totalNotes, cacheFolder, type, path, callback);
}
async function exportNote(
note: Note,
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
callback: (progress?: string) => void
) {
const fileFuncions = await resolveFileFunctions(type);
if (!fileFuncions) return;
const path = fileFuncions.path;
const { mkdir, writeFile, cacheFolder } = fileFuncions;
let currentAttachmentProgress = 0;
let hasAttachments = false;
let noteItem;
for await (const item of _exportNote(note, {
format: type,
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) {
if (item instanceof Error) {
DatabaseLogger.error(item);
continue;
}
if (item.type === "note") {
callback(`Exporting note`);
try {
noteItem = item;
await exportNoteToFile(item, type, mkdir, writeFile);
} catch (e) {
/* empty */
}
} else if (item.type === "attachment") {
currentAttachmentProgress += 1;
callback(`Downloading attachments (${currentAttachmentProgress})`);
try {
hasAttachments = true;
await exportAttachmentToFile(item, mkdir, cacheFolder);
} catch (e) {
/* empty */
}
}
}
if (!hasAttachments) {
return createFile(noteItem as ExportableNote, type, path, cacheFolder);
} else {
return createZip(1, cacheFolder, type, path, callback);
}
}
async function createZip(
totalNotes: number,
cacheFolder: string,
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
path: string,
callback: (progress?: string) => void
) {
const fileName = `nn-export-${totalNotes}-${type}-${Date.now()}.zip`;
const dir = path;
try {
callback("Creating zip");
const zipOutputPath =
Platform.OS === "ios"
? join(path, fileName)
: join(RNFetchBlob.fs.dirs.CacheDir, fileName);
await zip(cacheFolder, zipOutputPath);
callback("Saving zip file");
if (Platform.OS === "android") {
const file = await ScopedStorage.createFile(
path,
fileName,
"application/zip"
);
path = file.uri;
await copyFileAsync("file://" + zipOutputPath, path);
await RNFetchBlob.fs.unlink(zipOutputPath);
callback();
} else {
path = zipOutputPath;
}
RNFetchBlob.fs.unlink(cacheFolder);
} catch (e) {
DatabaseLogger.error(e as Error);
}
return {
filePath: path,
fileDir: dir,
type: "application/zip",
name: "zip",
fileName: fileName,
count: totalNotes
};
}
async function createFile(
noteItem: ExportableNote,
type: keyof typeof FileMime,
path: string,
cacheFolder: string
) {
const exportedFile = join(cacheFolder, noteItem?.path as string);
let filePath: string;
if (Platform.OS === "android") {
const file = await ScopedStorage.createFile(
path,
basename(noteItem?.path as string),
FileMime[type]
);
await copyFileAsync("file://" + exportedFile, file.uri);
filePath = file.uri;
} else {
const originalPath = join(path, basename(noteItem.path));
filePath = originalPath;
const ext = extname(originalPath);
let id = 1;
while (await RNFetchBlob.fs.exists(filePath)) {
filePath = originalPath.replace(`${ext}`, "") + "_" + id + ext;
id++;
}
await RNFetchBlob.fs.mv(exportedFile, filePath);
}
return {
filePath: filePath,
fileDir: path,
type: FileMime[type],
name: type,
fileName: basename(noteItem?.path as string),
count: 1
};
}
const FileMime = {
pdf: "application/pdf",
txt: "text/plain",
md: "text/markdown",
html: "text/html",
"md-frontmatter": "text/markdown"
};
const Exporter = {
exportNote,
bulkExport
};
export default Exporter;