mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
1 Commits
feat-remov
...
fix-notebo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29a9483aae |
@@ -38,7 +38,7 @@ await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
"npx nx build:desktop @notesnook/web",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
|
||||
if (os.platform() === "win32")
|
||||
await exec(
|
||||
`yarn prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
);
|
||||
|
||||
@@ -58,15 +58,15 @@ await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`yarn run bundle:mas`);
|
||||
await exec(`npm run bundle:mas`);
|
||||
} else {
|
||||
await exec(`yarn run bundle`);
|
||||
await exec(`npm run bundle`);
|
||||
}
|
||||
|
||||
await exec(`yarn tsc`);
|
||||
await exec(`npx tsc`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
|
||||
@@ -53,11 +53,11 @@ async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("yarn electron-builder install-app-deps");
|
||||
await exec("npx electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`yarn run bundle`);
|
||||
execAsync(`yarn`, [`tsc`]);
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
@@ -66,7 +66,7 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
@@ -78,7 +78,7 @@ async function onChange(first) {
|
||||
}
|
||||
|
||||
execAsync(
|
||||
"yarn",
|
||||
"npx",
|
||||
["electron", path.join("build", "electron.js")],
|
||||
true,
|
||||
cleanup
|
||||
|
||||
@@ -164,8 +164,6 @@ function SubNotebooks({
|
||||
const contextNotes = useNotesStore((store) => store.contextNotes);
|
||||
const context = useNotesStore((store) => store.context);
|
||||
|
||||
const [title, setTitle] = useState<string>();
|
||||
|
||||
const saveViewState = useCallback((id: string) => {
|
||||
if (!treeRef.current?.viewState) return;
|
||||
Config.set(`${id}:viewState`, treeRef.current.viewState[id]);
|
||||
@@ -200,16 +198,7 @@ function SubNotebooks({
|
||||
});
|
||||
}, [contextNotes, context]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const notebook = await db.notebooks.notebook(rootId);
|
||||
if (notebook) setTitle(notebook.title);
|
||||
})();
|
||||
});
|
||||
|
||||
if (!rootId) return null;
|
||||
// if (!notebook) return null;
|
||||
// const { title } = notebook;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -242,7 +231,7 @@ function SubNotebooks({
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<Text variant="subBody" sx={{ fontSize: 11 }}>
|
||||
{title}
|
||||
NOTEBOOKS
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
@@ -307,6 +296,17 @@ function SubNotebooks({
|
||||
};
|
||||
},
|
||||
async getTreeItem(itemId) {
|
||||
if (itemId === "root") {
|
||||
return {
|
||||
data: { notebook: { title: "Root" } },
|
||||
index: itemId,
|
||||
isFolder: true,
|
||||
canMove: false,
|
||||
canRename: false,
|
||||
children: [rootId]
|
||||
};
|
||||
}
|
||||
|
||||
const notebook = (await db.notebooks.notebook(itemId as string))!;
|
||||
const children = await db.relations
|
||||
.from({ type: "notebook", id: itemId as string }, "notebook")
|
||||
@@ -331,6 +331,18 @@ function SubNotebooks({
|
||||
.get();
|
||||
|
||||
return itemIds.reduce((prev, id) => {
|
||||
if (id === "root") {
|
||||
prev.push({
|
||||
data: { notebook: { title: "Root" } },
|
||||
index: id,
|
||||
isFolder: true,
|
||||
canMove: false,
|
||||
canRename: false,
|
||||
children: [rootId]
|
||||
});
|
||||
return prev;
|
||||
}
|
||||
|
||||
const notebook = notebooks[id];
|
||||
if (!notebook) return prev;
|
||||
|
||||
@@ -389,7 +401,7 @@ function SubNotebooks({
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
rootItem={rootId}
|
||||
rootItem="root"
|
||||
treeLabel="Tree Example"
|
||||
/>
|
||||
</UncontrolledTreeEnvironment>
|
||||
@@ -436,6 +448,13 @@ function NotebookHeader({
|
||||
})();
|
||||
}, [context.id, totalNotes, notebook]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if ((await db.notebooks.totalNotes(context.id)) !== totalNotes)
|
||||
setTotalNotes(await db.notebooks.totalNotes(context.id));
|
||||
})();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setCrumbs(await db.notebooks.breadcrumbs(context.id));
|
||||
|
||||
@@ -95,7 +95,6 @@ class Database {
|
||||
isInitialized = false;
|
||||
eventManager = new EventManager();
|
||||
sseMutex = new Mutex();
|
||||
_fs?: FileStorage;
|
||||
|
||||
storage: StorageAccessor = () => {
|
||||
if (!this.options?.storage)
|
||||
@@ -110,10 +109,7 @@ class Database {
|
||||
throw new Error(
|
||||
"Database not initialized. Did you forget to call db.setup()?"
|
||||
);
|
||||
return (
|
||||
this._fs ||
|
||||
(this._fs = new FileStorage(this.options.fs, this.tokenManager))
|
||||
);
|
||||
return new FileStorage(this.options.fs, this.tokenManager);
|
||||
};
|
||||
|
||||
crypto: CryptoAccessor = () => {
|
||||
|
||||
@@ -22,12 +22,10 @@ import { CURRENT_DATABASE_VERSION } from "../common.js";
|
||||
import Migrator from "./migrator.js";
|
||||
import Database from "../api/index.js";
|
||||
import {
|
||||
Attachment,
|
||||
Item,
|
||||
MaybeDeletedItem,
|
||||
Note,
|
||||
Notebook,
|
||||
Relation,
|
||||
ValueOf,
|
||||
isDeleted
|
||||
} from "../types.js";
|
||||
@@ -38,7 +36,6 @@ import { DatabaseCollection } from "./index.js";
|
||||
import { DefaultColors } from "../collections/colors.js";
|
||||
import { toChunks } from "../utils/array.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { clone } from "../utils/clone.js";
|
||||
|
||||
type BackupDataItem = MaybeDeletedItem<Item> | string[];
|
||||
type BackupPlatform = "web" | "mobile" | "node";
|
||||
@@ -95,44 +92,10 @@ function isEncryptedBackup(
|
||||
return "encrypted" in backup ? backup.encrypted : isCipher(backup.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Due to a bug in v3.0, legacy backups were created with version set to 6.1
|
||||
* while their actual data was at version 5.9. This caused various issues when
|
||||
* restoring such a backup.
|
||||
* This function tries to work around that bug by detecting the version based on
|
||||
* the actual data.
|
||||
*/
|
||||
function isLegacyBackup(data: BackupDataItem[]) {
|
||||
const note = data.find(
|
||||
(c): c is Note => !isDeleted(c) && !Array.isArray(c) && c.type === "note"
|
||||
);
|
||||
if (note)
|
||||
return (
|
||||
"color" in note ||
|
||||
"notebooks" in note ||
|
||||
"tags" in note ||
|
||||
"locked" in note
|
||||
);
|
||||
|
||||
const notebook = data.find(
|
||||
(c): c is Notebook =>
|
||||
!isDeleted(c) && !Array.isArray(c) && c.type === "notebook"
|
||||
);
|
||||
if (notebook) return "topics" in notebook;
|
||||
|
||||
const attachment = data.find(
|
||||
(c): c is Attachment =>
|
||||
!isDeleted(c) && !Array.isArray(c) && c.type === "attachment"
|
||||
);
|
||||
if (attachment) return "noteIds" in attachment;
|
||||
|
||||
const relation = data.find(
|
||||
(c): c is Relation =>
|
||||
!isDeleted(c) && !Array.isArray(c) && c.type === "relation"
|
||||
);
|
||||
if (relation) return "from" in relation || "to" in relation;
|
||||
|
||||
return false;
|
||||
function isLegacyBackupFile(
|
||||
backup: LegacyBackupFile | BackupFile
|
||||
): backup is LegacyBackupFile {
|
||||
return backup.version <= 5.8;
|
||||
}
|
||||
|
||||
const MAX_CHUNK_SIZE = 10 * 1024 * 1024;
|
||||
@@ -258,7 +221,7 @@ export default class Backup {
|
||||
yield {
|
||||
path: `${chunkIndex++}-${encrypt ? "encrypted" : "plain"}-${hash}`,
|
||||
data: `{
|
||||
"version": 5.9,
|
||||
"version": ${CURRENT_DATABASE_VERSION},
|
||||
"type": "${type}",
|
||||
"date": ${Date.now()},
|
||||
"data": ${itemsJSON},
|
||||
@@ -434,16 +397,13 @@ export default class Backup {
|
||||
|
||||
if (!data) throw new Error("No data found.");
|
||||
|
||||
const normalizedData: BackupDataItem[] = Array.isArray(data)
|
||||
? (data as BackupDataItem[])
|
||||
: typeof data === "object"
|
||||
? Object.values(data)
|
||||
: [];
|
||||
await this.migrateData(
|
||||
normalizedData,
|
||||
backup.version === 6.1 && isLegacyBackup(normalizedData)
|
||||
? 5.9
|
||||
: backup.version
|
||||
Array.isArray(data)
|
||||
? (data as BackupDataItem[])
|
||||
: typeof data === "object"
|
||||
? Object.values(data)
|
||||
: [],
|
||||
backup.version
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,22 +30,16 @@ import { logger } from "../logger";
|
||||
export type FileStorageAccessor = () => FileStorage;
|
||||
export type DownloadableFile = {
|
||||
filename: string;
|
||||
// metadata: AttachmentMetadata;
|
||||
chunkSize: number;
|
||||
};
|
||||
export type QueueItem = DownloadableFile & {
|
||||
cancel?: (reason?: string) => Promise<void>;
|
||||
operation?: Promise<boolean>;
|
||||
};
|
||||
|
||||
export class FileStorage {
|
||||
id = Date.now();
|
||||
downloads = new Map<string, QueueItem>();
|
||||
uploads = new Map<string, QueueItem>();
|
||||
groups = {
|
||||
downloads: new Map<string, Set<string>>(),
|
||||
uploads: new Map<string, Set<string>>()
|
||||
};
|
||||
|
||||
downloads = new Map<string, QueueItem[]>();
|
||||
uploads = new Map<string, QueueItem[]>();
|
||||
constructor(
|
||||
private readonly fs: IFileStorage,
|
||||
private readonly tokenManager: TokenManager
|
||||
@@ -56,26 +50,12 @@ export class FileStorage {
|
||||
groupId: string,
|
||||
eventData?: Record<string, unknown>
|
||||
) {
|
||||
let current = 0;
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
const total = files.length;
|
||||
const group = this.groups.downloads.get(groupId) || new Set();
|
||||
files.forEach((f) => group.add(f.filename));
|
||||
this.groups.downloads.set(groupId, group);
|
||||
let current = 0;
|
||||
this.downloads.set(groupId, files);
|
||||
|
||||
for (const file of files as QueueItem[]) {
|
||||
if (!group.has(file.filename)) continue;
|
||||
|
||||
const download = this.downloads.get(file.filename);
|
||||
if (download && download.operation) {
|
||||
logger.debug("[queueDownloads] duplicate download", {
|
||||
filename: file.filename,
|
||||
groupId
|
||||
});
|
||||
await download.operation;
|
||||
continue;
|
||||
}
|
||||
|
||||
const { filename, chunkSize } = file;
|
||||
if (await this.exists(filename)) {
|
||||
current++;
|
||||
@@ -88,13 +68,6 @@ export class FileStorage {
|
||||
continue;
|
||||
}
|
||||
|
||||
EV.publish(EVENTS.fileDownload, {
|
||||
total,
|
||||
current,
|
||||
groupId,
|
||||
filename
|
||||
});
|
||||
|
||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||
const { execute, cancel } = this.fs.downloadFile(filename, {
|
||||
url,
|
||||
@@ -102,15 +75,15 @@ export class FileStorage {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
file.cancel = cancel;
|
||||
file.operation = execute()
|
||||
.catch(() => false)
|
||||
.finally(() => {
|
||||
this.downloads.delete(filename);
|
||||
group.delete(filename);
|
||||
});
|
||||
|
||||
this.downloads.set(filename, file);
|
||||
const result = await file.operation;
|
||||
EV.publish(EVENTS.fileDownload, {
|
||||
total,
|
||||
current,
|
||||
groupId,
|
||||
filename
|
||||
});
|
||||
|
||||
const result = await execute().catch(() => false);
|
||||
if (eventData)
|
||||
EV.publish(EVENTS.fileDownloaded, {
|
||||
success: result,
|
||||
@@ -121,31 +94,17 @@ export class FileStorage {
|
||||
eventData
|
||||
});
|
||||
}
|
||||
this.downloads.delete(groupId);
|
||||
}
|
||||
|
||||
async queueUploads(files: DownloadableFile[], groupId: string) {
|
||||
let current = 0;
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
const total = files.length;
|
||||
const group = this.groups.uploads.get(groupId) || new Set();
|
||||
files.forEach((f) => group.add(f.filename));
|
||||
this.groups.uploads.set(groupId, group);
|
||||
let current = 0;
|
||||
this.uploads.set(groupId, files);
|
||||
|
||||
for (const file of files as QueueItem[]) {
|
||||
if (!group.has(file.filename)) continue;
|
||||
|
||||
const upload = this.uploads.get(file.filename);
|
||||
if (upload && upload.operation) {
|
||||
logger.debug("[queueUploads] duplicate upload", {
|
||||
filename: file.filename,
|
||||
groupId
|
||||
});
|
||||
await file.operation;
|
||||
continue;
|
||||
}
|
||||
|
||||
const { filename, chunkSize } = file;
|
||||
let error = null;
|
||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||
const { execute, cancel } = this.fs.uploadFile(filename, {
|
||||
chunkSize,
|
||||
@@ -153,16 +112,6 @@ export class FileStorage {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
file.cancel = cancel;
|
||||
file.operation = execute()
|
||||
.catch((e) => {
|
||||
logger.error(e, "failed to upload attachment", { hash: filename });
|
||||
error = e;
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
this.uploads.delete(filename);
|
||||
group.delete(filename);
|
||||
});
|
||||
|
||||
EV.publish(EVENTS.fileUpload, {
|
||||
total,
|
||||
@@ -171,8 +120,13 @@ export class FileStorage {
|
||||
filename
|
||||
});
|
||||
|
||||
this.uploads.set(filename, file);
|
||||
const result = await file.operation;
|
||||
let error = null;
|
||||
const result = await execute().catch((e) => {
|
||||
logger.error(e, "failed to upload attachment", { hash: filename });
|
||||
error = e;
|
||||
return false;
|
||||
});
|
||||
|
||||
EV.publish(EVENTS.fileUploaded, {
|
||||
error,
|
||||
success: result,
|
||||
@@ -182,67 +136,44 @@ export class FileStorage {
|
||||
filename
|
||||
});
|
||||
}
|
||||
this.uploads.delete(groupId);
|
||||
}
|
||||
|
||||
async downloadFile(groupId: string, filename: string, chunkSize: number) {
|
||||
if (await this.exists(filename)) return true;
|
||||
|
||||
const download = this.downloads.get(filename);
|
||||
if (download && download.operation) {
|
||||
logger.debug("[downloadFile] duplicate download", { filename, groupId });
|
||||
return await download.operation;
|
||||
}
|
||||
|
||||
logger.debug("[downloadFile] downloading", { filename, groupId });
|
||||
|
||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||
const file: QueueItem = { filename, chunkSize };
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
const group = this.groups.downloads.get(groupId) || new Set();
|
||||
const { execute, cancel } = this.fs.downloadFile(filename, {
|
||||
url,
|
||||
chunkSize,
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
file.cancel = cancel;
|
||||
file.operation = execute().finally(() => {
|
||||
this.downloads.delete(filename);
|
||||
group.delete(filename);
|
||||
});
|
||||
|
||||
this.downloads.set(filename, file);
|
||||
this.groups.downloads.set(groupId, group.add(filename));
|
||||
return await file.operation;
|
||||
this.downloads.set(groupId, [{ cancel, filename, chunkSize }]);
|
||||
const result = await execute();
|
||||
this.downloads.delete(groupId);
|
||||
return result;
|
||||
}
|
||||
|
||||
async cancel(groupId: string) {
|
||||
const queues = [
|
||||
{
|
||||
type: "download",
|
||||
ids: this.groups.downloads.get(groupId),
|
||||
files: this.downloads
|
||||
},
|
||||
{
|
||||
type: "upload",
|
||||
ids: this.groups.uploads.get(groupId),
|
||||
files: this.uploads
|
||||
}
|
||||
].filter((a) => !!a.ids);
|
||||
{ type: "download", files: this.downloads.get(groupId) },
|
||||
{ type: "upload", files: this.uploads.get(groupId) }
|
||||
].filter((a) => !!a.files);
|
||||
|
||||
for (const queue of queues) {
|
||||
if (!queue.ids) continue;
|
||||
|
||||
for (const filename of queue.ids) {
|
||||
const file = queue.files.get(filename);
|
||||
if (file?.cancel) await file.cancel("Operation canceled.");
|
||||
queue.ids.delete(filename);
|
||||
if (!queue.files) continue;
|
||||
for (let i = 0; i < queue.files.length; ++i) {
|
||||
const file = queue.files[i];
|
||||
if (file.cancel) await file.cancel("Operation canceled.");
|
||||
queue.files.splice(i, 1);
|
||||
}
|
||||
|
||||
if (queue.type === "download") {
|
||||
this.groups.downloads.delete(groupId);
|
||||
this.downloads.delete(groupId);
|
||||
EV.publish(EVENTS.downloadCanceled, { groupId, canceled: true });
|
||||
} else if (queue.type === "upload") {
|
||||
this.groups.uploads.delete(groupId);
|
||||
this.uploads.delete(groupId);
|
||||
EV.publish(EVENTS.uploadCanceled, { groupId, canceled: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
} from "../types";
|
||||
import { IndexedCollection } from "./indexed-collection";
|
||||
import { SQLCollection } from "./sql-collection";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export type RawItem = MaybeDeletedItem<Item>;
|
||||
type MigratableCollection = {
|
||||
@@ -137,16 +136,10 @@ class Migrator {
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
const item = items[i];
|
||||
// can be true due to corrupted data.
|
||||
if (Array.isArray(item)) {
|
||||
logger.debug("Skipping item during migration to SQLite", {
|
||||
table,
|
||||
version,
|
||||
item
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(item)) continue;
|
||||
if (!item) continue;
|
||||
|
||||
const itemId = item.id;
|
||||
let migrated = await migrateItem(
|
||||
item,
|
||||
version,
|
||||
@@ -168,7 +161,14 @@ class Migrator {
|
||||
);
|
||||
}
|
||||
|
||||
if (migrated !== "skip") toAdd.push(item);
|
||||
if (migrated === true) {
|
||||
toAdd.push(item);
|
||||
|
||||
// if id changed after migration, we need to delete the old one.
|
||||
if (item.id !== itemId) {
|
||||
// await collection.deleteItem(itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
@@ -217,7 +217,7 @@ class Migrator {
|
||||
);
|
||||
}
|
||||
|
||||
if (!migrated || migrated === "skip") continue;
|
||||
if (!migrated) continue;
|
||||
|
||||
toAdd.push(item);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user