mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
373 lines
9.6 KiB
TypeScript
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;
|