diff --git a/apps/mobile/app/common/database/encryption.ts b/apps/mobile/app/common/database/encryption.ts
index 13ee15208..8d9510909 100644
--- a/apps/mobile/app/common/database/encryption.ts
+++ b/apps/mobile/app/common/database/encryption.ts
@@ -18,6 +18,7 @@ along with this program. If not, see .
*/
import Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
+import { SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
@@ -233,7 +234,40 @@ export async function getDatabaseKey(appLockPassword?: string) {
}
}
-export async function deriveCryptoKey(data: Required) {
+export async function deriveCryptoKeyFallback(data: SerializedKey) {
+ if (Platform.OS !== "ios") return;
+ try {
+ if (!data.password || !data.salt)
+ throw new Error(
+ "Invalid password and salt provided to deriveCryptoKeyFallback"
+ );
+
+ const credentials = await Sodium.deriveKeyFallback?.(
+ data.password,
+ data.salt
+ );
+
+ if (!credentials) return;
+
+ const userKeyCipher = (await encrypt(
+ {
+ key: (await getDatabaseKey()) as string,
+ salt: NOTESNOOK_DB_KEY_SALT
+ },
+ credentials.key as string
+ )) as Cipher<"base64">;
+ DatabaseLogger.info("User key fallback stored: ", {
+ userKeyCipher: !!userKeyCipher
+ });
+
+ // Store encrypted user key in MMKV
+ MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
+ } catch (e) {
+ DatabaseLogger.error(e);
+ }
+}
+
+export async function deriveCryptoKey(data: SerializedKey) {
try {
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
@@ -248,14 +282,13 @@ export async function deriveCryptoKey(data: Required) {
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
- )) as Cipher;
+ )) as Cipher<"base64">;
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
- return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
@@ -266,7 +299,7 @@ export async function getCryptoKey() {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
- return null;
+ return undefined;
}
const key = await decrypt(
@@ -298,41 +331,65 @@ export async function getRandomBytes(length: number) {
return await generateSecureRandom(length);
}
-export async function hash(password: string, email: string) {
- //@ts-ignore
- return await Sodium.hashPassword(password, email);
+export async function hash(
+ password: string,
+ email: string,
+ options?: { usesFallback?: boolean }
+) {
+ DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
+
+ if (options?.usesFallback && Platform.OS !== "ios") {
+ return null;
+ }
+
+ return (
+ options?.usesFallback
+ ? await Sodium.hashPasswordFallback?.(password, email)
+ : await Sodium.hashPassword(password, email)
+ ) as string;
}
export async function generateCryptoKey(password: string, salt?: string) {
- try {
- //@ts-ignore
- const credentials = await Sodium.deriveKey(password, salt || null);
- return credentials;
- } catch (e) {
- DatabaseLogger.error(e);
- }
+ return (await Sodium.deriveKey(password, salt)) as Promise;
}
export function getAlgorithm(base64Variant: number) {
return `xcha-argon2i13-${base64Variant}`;
}
-export async function decrypt(password: Password, data: Cipher) {
- if (!password.password && !password.key) return undefined;
- if (password.password && password.password === "" && !password.key)
- return undefined;
+export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
const _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
+
+ if (Platform.OS === "ios" && !password.key && password.password) {
+ const key = await Sodium.deriveKey(password.password, password.salt);
+ try {
+ return await Sodium.decrypt(key, _data);
+ } catch (e) {
+ const fallbackKey = await Sodium.deriveKeyFallback?.(
+ password.password,
+ password.salt
+ );
+ if (Platform.OS === "ios" && fallbackKey) {
+ DatabaseLogger.info("Using fallback key for decryption");
+ }
+ if (fallbackKey) {
+ return await Sodium.decrypt(fallbackKey, _data);
+ } else {
+ throw e;
+ }
+ }
+ }
+
return await Sodium.decrypt(password, _data);
}
-export async function decryptMulti(password: Password, data: Cipher[]) {
- if (!password.password && !password.key) return undefined;
- if (password.password && password.password === "" && !password.key)
- return undefined;
-
+export async function decryptMulti(
+ password: Password,
+ data: Cipher<"base64">[]
+) {
data = data.map((d) => {
d.output = "plain";
return d;
@@ -342,6 +399,26 @@ export async function decryptMulti(password: Password, data: Cipher[]) {
password.salt = data[0].salt;
}
+ if (Platform.OS === "ios" && !password.key && password.password) {
+ const key = await Sodium.deriveKey(password.password, password.salt);
+ try {
+ return await Sodium.decryptMulti(key, data);
+ } catch (e) {
+ const fallbackKey = await Sodium.deriveKeyFallback?.(
+ password.password,
+ password.salt as string
+ );
+ if (Platform.OS === "ios" && fallbackKey) {
+ DatabaseLogger.info("Using fallback key for decryption");
+ }
+ if (fallbackKey) {
+ return await Sodium.decryptMulti(fallbackKey, data);
+ } else {
+ throw e;
+ }
+ }
+ }
+
return await Sodium.decryptMulti(password, data);
}
@@ -357,14 +434,10 @@ export function parseAlgorithm(alg: string) {
};
}
-export async function encrypt(password: Password, data: string) {
- if (!password.password && !password.key) return undefined;
- if (password.password && password.password === "" && !password.key)
- return undefined;
-
- const result = await Sodium.encrypt(password, {
+export async function encrypt(password: SerializedKey, plainText: string) {
+ const result = await Sodium.encrypt<"base64">(password, {
type: "plain",
- data: data
+ data: plainText
});
return {
@@ -373,14 +446,13 @@ export async function encrypt(password: Password, data: string) {
};
}
-export async function encryptMulti(password: Password, data: string[]) {
- if (!password.password && !password.key) return undefined;
- if (password.password && password.password === "" && !password.key)
- return undefined;
-
- const results = await Sodium.encryptMulti(
+export async function encryptMulti(
+ password: SerializedKey,
+ plainText: string[]
+) {
+ const results = await Sodium.encryptMulti<"base64">(
password,
- data.map((item) => ({
+ plainText.map((item) => ({
type: "plain",
data: item
}))
diff --git a/apps/mobile/app/common/database/index.js b/apps/mobile/app/common/database/index.ts
similarity index 83%
rename from apps/mobile/app/common/database/index.js
rename to apps/mobile/app/common/database/index.ts
index 9224b558f..8f63c4f0f 100644
--- a/apps/mobile/app/common/database/index.js
+++ b/apps/mobile/app/common/database/index.ts
@@ -16,26 +16,26 @@ 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 .
*/
-import "./logger";
import { database } from "@notesnook/common";
-import { logger as dbLogger } from "@notesnook/core";
-import { Platform } from "react-native";
-import * as Gzip from "react-native-gzip";
-import EventSource from "../../utils/sse/even-source-ios";
-import AndroidEventSource from "../../utils/sse/event-source";
+import { logger as dbLogger, ICompressor } from "@notesnook/core";
+import { strings } from "@notesnook/intl";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
-import filesystem from "../filesystem";
-import Storage from "./storage";
-import { RNSqliteDriver } from "./sqlite.kysely";
-import { getDatabaseKey } from "./encryption";
+import { Platform } from "react-native";
+import * as Gzip from "react-native-gzip";
import SettingsService from "../../services/settings";
-import { strings } from "@notesnook/intl";
+import EventSource from "../../utils/sse/even-source-ios";
+import AndroidEventSource from "../../utils/sse/event-source";
+import { FileStorage } from "../filesystem";
+import { getDatabaseKey } from "./encryption";
+import "./logger";
+import { RNSqliteDriver } from "./sqlite.kysely";
+import { Storage } from "./storage";
-export async function setupDatabase(password) {
+export async function setupDatabase(password?: string) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
@@ -47,17 +47,21 @@ export async function setupDatabase(password) {
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
+ MONOGRAPH_HOST: "https://monogr.ph",
...(SettingsService.getProperty("serverUrls") || {})
});
database.setup({
storage: Storage,
- eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
- fs: filesystem,
- compressor: () => ({
- compress: Gzip.deflate,
- decompress: Gzip.inflate
- }),
+ eventsource: (Platform.OS === "ios"
+ ? EventSource
+ : AndroidEventSource) as any,
+ fs: FileStorage,
+ compressor: async () =>
+ ({
+ compress: Gzip.deflate,
+ decompress: Gzip.inflate
+ } as ICompressor),
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({
diff --git a/apps/mobile/app/common/database/logger.js b/apps/mobile/app/common/database/logger.ts
similarity index 100%
rename from apps/mobile/app/common/database/logger.js
rename to apps/mobile/app/common/database/logger.ts
diff --git a/apps/mobile/app/common/database/mmkv.js b/apps/mobile/app/common/database/mmkv.ts
similarity index 100%
rename from apps/mobile/app/common/database/mmkv.js
rename to apps/mobile/app/common/database/mmkv.ts
diff --git a/apps/mobile/app/common/database/sqlite.kysely.ts b/apps/mobile/app/common/database/sqlite.kysely.ts
index a5898af80..04a0c4857 100644
--- a/apps/mobile/app/common/database/sqlite.kysely.ts
+++ b/apps/mobile/app/common/database/sqlite.kysely.ts
@@ -26,7 +26,7 @@ import { CompiledQuery } from "@streetwriters/kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
-type Config = { dbName: string; async: boolean; location: string };
+type Config = { dbName: string; async: boolean; location?: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
diff --git a/apps/mobile/app/common/database/storage.js b/apps/mobile/app/common/database/storage.ts
similarity index 50%
rename from apps/mobile/app/common/database/storage.js
rename to apps/mobile/app/common/database/storage.ts
index 53c462824..e1523b108 100644
--- a/apps/mobile/app/common/database/storage.js
+++ b/apps/mobile/app/common/database/storage.ts
@@ -16,58 +16,51 @@ 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 .
*/
-
-import { Platform } from "react-native";
-import RNFetchBlob from "react-native-blob-util";
+import { IStorage } from "@notesnook/core";
+import { MMKVInstance } from "react-native-mmkv-storage";
import {
decrypt,
decryptMulti,
deriveCryptoKey,
+ deriveCryptoKeyFallback,
encrypt,
encryptMulti,
generateCryptoKey,
getCryptoKey,
- getRandomBytes,
- hash,
- removeCryptoKey
+ hash
} from "./encryption";
import { MMKV } from "./mmkv";
export class KV {
- /**
- * @type {typeof MMKV}
- */
- storage = null;
- constructor(storage) {
+ storage: MMKVInstance;
+ constructor(storage: MMKVInstance) {
this.storage = storage;
}
- async read(key) {
- if (!key) return null;
- let data = this.storage.getString(key);
- if (!data) return null;
+
+ async read(key: string, isArray?: boolean) {
+ if (!key) return undefined;
+ const data = this.storage.getString(key);
+ if (!data) return undefined;
try {
- let parse = JSON.parse(data);
- return parse;
+ return JSON.parse(data) as T;
} catch (e) {
- return data;
+ return data as T;
}
}
- async write(key, data) {
+ async write(key: string, data: T) {
this.storage.setString(
key,
typeof data === "string" ? data : JSON.stringify(data)
);
-
- return true;
}
- async readMulti(keys) {
+ async readMulti(keys: string[]) {
if (keys.length <= 0) {
return [];
} else {
try {
- let data = await this.storage.getMultipleItemsAsync(
+ const data = await this.storage.getMultipleItemsAsync(
keys.slice(),
"string"
);
@@ -79,24 +72,24 @@ export class KV {
obj = value;
}
return [key, obj];
- });
+ }) as [string, T][];
} catch (e) {
- console.log(e);
+ return [];
}
}
}
- async remove(key) {
- return this.storage.removeItem(key);
+ async remove(key: string) {
+ this.storage.removeItem(key);
}
- async removeMulti(keys) {
- if (!keys) return true;
- return this.storage.removeItems(keys);
+ async removeMulti(keys: string[]) {
+ if (!keys) return;
+ this.storage.removeItems(keys);
}
async clear() {
- return this.storage.clearStore();
+ this.storage.clearStore();
}
async getAllKeys() {
@@ -113,54 +106,45 @@ export class KV {
return keys;
}
- async writeMulti(items) {
- return this.storage.setMultipleItemsAsync(items, "object");
+ async writeMulti(items: [string, any][]) {
+ await this.storage.setMultipleItemsAsync(items, "object");
}
}
const DefaultStorage = new KV(MMKV);
-async function requestPermission() {
- if (Platform.OS === "ios") return true;
- return true;
-}
-async function checkAndCreateDir(path) {
- let dir =
- Platform.OS === "ios"
- ? RNFetchBlob.fs.dirs.DocumentDir + path
- : RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
-
- try {
- let exists = await RNFetchBlob.fs.exists(dir);
- let isDir = await RNFetchBlob.fs.isDir(dir);
- if (!exists || !isDir) {
- await RNFetchBlob.fs.mkdir(dir);
- }
- } catch (e) {
- await RNFetchBlob.fs.mkdir(dir);
- }
- return dir;
-}
-
-export default {
- read: (key) => DefaultStorage.read(key),
- write: (key, value) => DefaultStorage.write(key, value),
- readMulti: (keys) => DefaultStorage.readMulti(keys),
- remove: (key) => DefaultStorage.remove(key),
- clear: () => DefaultStorage.clear(),
- getAllKeys: () => DefaultStorage.getAllKeys(),
- writeMulti: (items) => DefaultStorage.writeMulti(items),
- removeMulti: (keys) => DefaultStorage.removeMulti(keys),
+export const Storage: IStorage = {
+ write(key: string, data: T): Promise {
+ return DefaultStorage.write(key, data);
+ },
+ writeMulti(entries: [string, T][]): Promise {
+ return DefaultStorage.writeMulti(entries);
+ },
+ readMulti(keys: string[]): Promise<[string, T][]> {
+ return DefaultStorage.readMulti(keys);
+ },
+ read(key: string, isArray?: boolean): Promise {
+ return DefaultStorage.read(key, isArray);
+ },
+ remove(key: string): Promise {
+ return DefaultStorage.remove(key);
+ },
+ removeMulti(keys: string[]): Promise {
+ return DefaultStorage.removeMulti(keys);
+ },
+ clear(): Promise {
+ return DefaultStorage.clear();
+ },
+ getAllKeys(): Promise {
+ return DefaultStorage.getAllKeys();
+ },
+ hash,
+ getCryptoKey,
encrypt,
+ encryptMulti,
decrypt,
decryptMulti,
- getRandomBytes,
- checkAndCreateDir,
- requestPermission,
deriveCryptoKey,
- getCryptoKey,
- removeCryptoKey,
- hash,
generateCryptoKey,
- encryptMulti
+ deriveCryptoKeyFallback
};
diff --git a/apps/mobile/app/common/filesystem/download-attachment.js b/apps/mobile/app/common/filesystem/download-attachment.tsx
similarity index 79%
rename from apps/mobile/app/common/filesystem/download-attachment.js
rename to apps/mobile/app/common/filesystem/download-attachment.tsx
index 699eb5fe7..4a6d4bfd8 100644
--- a/apps/mobile/app/common/filesystem/download-attachment.js
+++ b/apps/mobile/app/common/filesystem/download-attachment.tsx
@@ -16,7 +16,6 @@ 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 .
*/
-
import Sodium from "@ammarahmed/react-native-sodium";
import { getFileNameWithExtension } from "@notesnook/core";
import { strings } from "@notesnook/intl";
@@ -26,11 +25,11 @@ import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import { ShareComponent } from "../../components/sheets/export-notes/share";
-import { ToastManager, presentSheet } from "../../services/event-manager";
+import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
-import Storage from "../database/storage";
+import filesystem from "../filesystem";
import { createCacheDir, exists } from "./io";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
@@ -51,23 +50,23 @@ export async function downloadAllAttachments() {
/**
* Downloads provided attachments to a .zip file
* on user's device.
- * @param {string[]} attachments
+ * @param {string[]} attachmentIds
* @param onProgress
* @returns
*/
-export async function downloadAttachments(attachments) {
+export async function downloadAttachments(attachmentIds: string[]) {
await createCacheDir();
- if (!attachments || !attachments.length) return;
+ if (!attachmentIds || !attachmentIds.length) return;
const groupId = `download-all-${Date.now()}`;
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
- let file = await ScopedStorage.openDocumentTree(true);
+ const file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
- outputFolder = await Storage.checkAndCreateDir("/downloads/");
+ outputFolder = await filesystem.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
@@ -83,7 +82,7 @@ export async function downloadAttachments(attachments) {
await RNFetchBlob.fs.mkdir(zipSourceFolder);
const isCancelled = () => {
- if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
+ if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
useAttachmentStore.getState().setDownloading({
groupId,
@@ -97,19 +96,21 @@ export async function downloadAttachments(attachments) {
}
};
- for (let i = 0; i < attachments.length; i++) {
+ for (let i = 0; i < attachmentIds.length; i++) {
if (isCancelled()) return;
- let attachment = await db.attachments.attachment(attachments[i]);
+ const attachment = await db.attachments.attachment(attachmentIds[i]);
+ if (!attachment) continue;
+
const hash = attachment.hash;
try {
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: i + 1,
- total: attachments.length,
+ total: attachmentIds.length,
filename: attachment.hash
});
// Download to cache
- let uri = await downloadAttachment(hash, false, {
+ const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
@@ -184,7 +185,7 @@ export async function downloadAttachments(attachments) {
});
releasePermissions(outputFolder);
sub?.remove();
- ToastManager.error(e, "Error zipping attachments");
+ ToastManager.error(e as Error, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -194,38 +195,44 @@ export async function downloadAttachments(attachments) {
}
export default async function downloadAttachment(
- hash,
+ hashOrId: string,
global = true,
- options = {
- silent: false,
- cache: false,
- throwError: false,
- groupId: undefined,
- base64: false,
- text: false
+ options?: {
+ silent?: boolean;
+ cache?: boolean;
+ throwError?: boolean;
+ groupId?: string;
+ base64?: boolean;
+ text?: boolean;
}
) {
await createCacheDir();
- let attachment = await db.attachments.attachment(hash);
+ const attachment = await db.attachments.attachment(hashOrId);
if (!attachment) {
- DatabaseLogger.log("Attachment not found");
+ DatabaseLogger.log("Attachment not found", {
+ hash: hashOrId
+ });
return;
}
- let folder = {};
- if (!options.cache) {
+ let folder: {
+ uri: string;
+ } | null = null;
+ if (!options?.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
- folder.uri = await Storage.checkAndCreateDir("/downloads/");
+ folder = {
+ uri: await filesystem.checkAndCreateDir("/downloads/")
+ };
}
}
try {
useAttachmentStore.getState().setDownloading({
- groupId: options.groupId || attachment.hash,
+ groupId: options?.groupId || attachment.hash,
current: 0,
total: 1,
filename: attachment.filename
@@ -234,13 +241,13 @@ export default async function downloadAttachment(
await db
.fs()
.downloadFile(
- options.groupId || attachment.hash,
+ options?.groupId || attachment.hash,
attachment.hash,
attachment.chunkSize
);
useAttachmentStore.getState().setDownloading({
- groupId: options.groupId || attachment.hash,
+ groupId: options?.groupId || attachment.hash,
current: 1,
total: 1,
filename: attachment.filename,
@@ -252,7 +259,7 @@ export default async function downloadAttachment(
return;
}
- if (options.base64 || options.text) {
+ if (options?.base64 || options?.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
@@ -260,14 +267,16 @@ export default async function downloadAttachment(
);
}
- let filename = await getFileNameWithExtension(
+ const filename = await getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
- let key = await db.attachments.decryptKey(attachment.key);
+ const key = await db.attachments.decryptKey(attachment.key);
- let info = {
+ if (!key) return;
+
+ const info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
@@ -275,18 +284,18 @@ export default async function downloadAttachment(
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
- fileName: options.cache ? undefined : filename,
- uri: options.cache ? undefined : folder.uri,
+ fileName: options?.cache ? undefined : filename,
+ uri: options?.cache ? undefined : folder?.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
key,
info,
- options.cache ? "cache" : "file"
+ options?.cache ? "cache" : "file"
);
- if (!options.silent) {
+ if (!options?.silent) {
ToastManager.show({
heading: strings.network.downloadSuccess(),
message: strings.network.fileDownloaded(filename),
@@ -294,15 +303,15 @@ export default async function downloadAttachment(
});
}
- if (Platform.OS === "ios" && !options.cache) {
- fileUri = folder.uri + `/${filename}`;
+ if (Platform.OS === "ios" && !options?.cache) {
+ fileUri = folder?.uri + `/${filename}`;
}
- if (!options.silent) {
+ if (!options?.silent) {
presentSheet({
title: strings.network.fileDownloaded(),
paragraph: strings.fileSaved(filename, Platform.OS),
icon: "download",
- context: global ? null : attachment.hash,
+ context: global ? "global" : attachment.hash,
component:
});
}
@@ -319,7 +328,7 @@ export default async function downloadAttachment(
}
useAttachmentStore.getState().setDownloading({
- groupId: options.groupId || attachment.hash,
+ groupId: options?.groupId || attachment.hash,
current: 0,
total: 0,
filename: attachment.filename,
@@ -327,7 +336,7 @@ export default async function downloadAttachment(
});
DatabaseLogger.error(e);
useAttachmentStore.getState().remove(attachment.hash);
- if (options.throwError) {
+ if (options?.throwError) {
throw e;
}
}
diff --git a/apps/mobile/app/common/filesystem/download.js b/apps/mobile/app/common/filesystem/download.ts
similarity index 76%
rename from apps/mobile/app/common/filesystem/download.js
rename to apps/mobile/app/common/filesystem/download.ts
index ae341f12d..f1062b4e7 100644
--- a/apps/mobile/app/common/filesystem/download.js
+++ b/apps/mobile/app/common/filesystem/download.ts
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+import { RequestOptions } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
@@ -26,7 +27,13 @@ import { DatabaseLogger, db } from "../database";
import { createCacheDir, exists } from "./io";
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
-export async function downloadFile(filename, requestOptions, cancelToken) {
+export async function downloadFile(
+ filename: string,
+ requestOptions: RequestOptions,
+ cancelToken: {
+ cancel: (reason?: string) => Promise;
+ }
+) {
if (!requestOptions) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: No requestOptions`
@@ -36,9 +43,11 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
- let { url, headers, chunkSize } = requestOptions;
- let tempFilePath = `${cacheDir}/${filename}_temp`;
- let originalFilePath = `${cacheDir}/${filename}`;
+
+ const { url, headers, chunkSize } = requestOptions;
+ const tempFilePath = `${cacheDir}/${filename}_temp`;
+ const originalFilePath = `${cacheDir}/${filename}`;
+
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
@@ -46,6 +55,8 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
}
const attachment = await db.attachments.attachment(filename);
+ if (!attachment) return false;
+
const size = await getUploadedFileSize(filename);
if (size === -1) {
@@ -71,21 +82,21 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
throw new Error(error);
}
- let res = await fetch(url, {
+ const resolveUrlResponse = await fetch(url, {
method: "GET",
headers
});
- if (!res.ok) {
+ if (!resolveUrlResponse.ok) {
DatabaseLogger.log(
- `Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
+ `Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
);
throw new Error(
- `${res.status}: ${strings.failedToResolvedDownloadUrl()}`
+ `${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
);
}
- const downloadUrl = await res.text();
+ const downloadUrl = await resolveUrlResponse.text();
if (!downloadUrl) {
DatabaseLogger.log(
@@ -95,12 +106,12 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
}
DatabaseLogger.log(`Download starting: ${filename}`);
- let request = RNFetchBlob.config({
+ const request = RNFetchBlob.config({
path: tempFilePath,
IOSBackgroundTask: true,
overwrite: true
})
- .fetch("GET", downloadUrl, null)
+ .fetch("GET", downloadUrl)
.progress(async (recieved, total) => {
useAttachmentStore
.getState()
@@ -109,15 +120,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
- cancelToken.cancel = () => {
+ cancelToken.cancel = async (reason) => {
useAttachmentStore.getState().remove(filename);
request.cancel();
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
- DatabaseLogger.log(`Download cancelled: ${filename}`);
+ DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
};
- let response = await request;
- console.log(response.info().headers);
+ const response = await request;
const contentType =
response.info().headers?.["content-type"] ||
@@ -128,10 +138,10 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
throw new Error(`[${error.Code}] ${error.Message}`);
}
- let status = response.info().status;
+ const status = response.info().status;
useAttachmentStore.getState().remove(filename);
- if (exists(originalFilePath)) {
+ if (await exists(originalFilePath)) {
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
}
@@ -143,11 +153,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
return status >= 200 && status < 300;
} catch (e) {
- if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
+ if (
+ (e as Error).message !== "canceled" &&
+ !(e as Error).message.includes("NoSuchKey")
+ ) {
const toast = {
- heading: strings.downloadError(),
- message: e.message,
- type: "error",
+ heading: strings.downloadError((e as Error).message),
+ message: (e as Error).message,
+ type: "error" as const,
context: "global"
};
ToastManager.show(toast);
@@ -158,15 +171,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
- DatabaseLogger.error(e, {
- url,
- headers
+ DatabaseLogger.error(e, "Download failed: ", {
+ url
});
return false;
}
}
-export async function checkAttachment(hash) {
+export async function checkAttachment(hash: string) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
@@ -184,7 +196,7 @@ export async function checkAttachment(hash) {
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
};
} catch (e) {
- return { failed: e?.message };
+ return { failed: (e as Error)?.message };
}
return { success: true };
}
diff --git a/apps/mobile/app/common/filesystem/index.js b/apps/mobile/app/common/filesystem/index.ts
similarity index 81%
rename from apps/mobile/app/common/filesystem/index.js
rename to apps/mobile/app/common/filesystem/index.ts
index 3fc7afa13..676ec4665 100644
--- a/apps/mobile/app/common/filesystem/index.js
+++ b/apps/mobile/app/common/filesystem/index.ts
@@ -17,24 +17,40 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+import { IFileStorage } from "@notesnook/core";
import { checkAttachment, downloadFile } from "./download";
import {
+ bulkExists,
+ clearCache,
clearFileStorage,
+ deleteCacheFileByName,
+ deleteCacheFileByPath,
deleteFile,
exists,
- readEncrypted,
- writeEncryptedBase64,
+ getCacheSize,
hashBase64,
+ readEncrypted,
+ writeEncryptedBase64
+} from "./io";
+import { uploadFile } from "./upload";
+import {
+ cancelable,
+ checkAndCreateDir,
+ getUploadedFileSize,
+ requestPermission
+} from "./utils";
+
+export default {
+ checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
- bulkExists,
- getCacheSize
-} from "./io";
-import { uploadFile } from "./upload";
-import { cancelable, getUploadedFileSize } from "./utils";
+ getCacheSize,
+ requestPermission,
+ checkAndCreateDir
+};
-export default {
+export const FileStorage: IFileStorage = {
readEncrypted,
writeEncryptedBase64,
hashBase64,
@@ -44,10 +60,5 @@ export default {
exists,
clearFileStorage,
getUploadedFileSize,
- checkAttachment,
- clearCache,
- deleteCacheFileByName,
- deleteCacheFileByPath,
- bulkExists,
- getCacheSize
+ bulkExists
};
diff --git a/apps/mobile/app/common/filesystem/io.js b/apps/mobile/app/common/filesystem/io.ts
similarity index 70%
rename from apps/mobile/app/common/filesystem/io.js
rename to apps/mobile/app/common/filesystem/io.ts
index e21b85f8c..69c0d0c62 100644
--- a/apps/mobile/app/common/filesystem/io.js
+++ b/apps/mobile/app/common/filesystem/io.ts
@@ -18,6 +18,13 @@ along with this program. If not, see .
*/
import Sodium from "@ammarahmed/react-native-sodium";
+import {
+ FileEncryptionMetadataWithHash,
+ FileEncryptionMetadataWithOutputType,
+ Output,
+ RequestOptions
+} from "@notesnook/core";
+import { DataFormat, SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { eSendEvent } from "../../services/event-manager";
@@ -25,17 +32,21 @@ import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
-export async function readEncrypted(filename, key, cipherData) {
+export async function readEncrypted(
+ filename: string,
+ key: SerializedKey,
+ cipherData: FileEncryptionMetadataWithOutputType
+) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
- let path = `${cacheDir}/${filename}`;
+ const path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
- return false;
+ return;
}
- let output = await Sodium.decryptFile(
+ const output = await Sodium.decryptFile(
key,
{
...cipherData,
@@ -47,15 +58,14 @@ export async function readEncrypted(filename, key, cipherData) {
DatabaseLogger.log("File decrypted...");
- return output;
+ return output as Output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e);
- return false;
}
}
-export async function hashBase64(data) {
+export async function hashBase64(data: string) {
const hash = await Sodium.hashFile({
type: "base64",
data,
@@ -67,61 +77,70 @@ export async function hashBase64(data) {
};
}
-export async function writeEncryptedBase64(data, key) {
+export async function writeEncryptedBase64(
+ data: string,
+ encryptionKey: SerializedKey,
+ mimeType: string
+): Promise {
await createCacheDir();
- let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
+ const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
- let output = await Sodium.encryptFile(key, {
+ const output = await Sodium.encryptFile(encryptionKey, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
+
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
- output.size = output.length;
- delete output.length;
+
return {
...output,
alg: "xcha-stream"
};
}
-export async function deleteFile(filename, data) {
+export async function deleteFile(
+ filename: string,
+ requestOptions?: RequestOptions
+): Promise {
await createCacheDir();
- let delFilePath = cacheDir + `/${filename}`;
- if (!data) {
- if (!filename) return;
- RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
+ const localFilePath = cacheDir + `/${filename}`;
+ if (!requestOptions) {
+ RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
return true;
}
- let { url, headers } = data;
+ const { url, headers } = requestOptions;
+
try {
- let response = await RNFetchBlob.fetch("DELETE", url, headers);
- let status = response.info().status;
- let ok = status >= 200 && status < 300;
+ const response = await RNFetchBlob.fetch("DELETE", url, headers);
+ const status = response.info().status;
+ const ok = status >= 200 && status < 300;
if (ok) {
- RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
+ RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
}
return ok;
} catch (e) {
- console.log("delete file: ", e, url, headers);
+ DatabaseLogger.error(e, "Delete file", {
+ url: url
+ });
return false;
}
}
export async function clearFileStorage() {
try {
- let files = await RNFetchBlob.fs.ls(cacheDir);
- let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
+ const files = await RNFetchBlob.fs.ls(cacheDir);
+ const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
- for (let file of files) {
+ for (const file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
- for (let file of oldCache) {
+ for (const file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
}
} catch (e) {
- console.log("clearFileStorage", e);
+ DatabaseLogger.error(e, "clearFileStorage");
}
}
@@ -146,11 +165,11 @@ export async function migrateFilesFromCache() {
return;
}
- let files = await RNFetchBlob.fs.ls(cacheDir);
+ const files = await RNFetchBlob.fs.ls(cacheDir);
console.log("Files to migrate:", files.join(","));
- let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
- for (let file of oldCache) {
+ const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
+ for (const file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
@@ -159,7 +178,7 @@ export async function migrateFilesFromCache() {
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
- console.log("migrateFilesFromCache", e);
+ DatabaseLogger.error(e, "migrateFilesFromCache");
}
}
@@ -169,14 +188,14 @@ export async function clearCache() {
eSendEvent("cache-cleared");
}
-export async function deleteCacheFileByPath(path) {
+export async function deleteCacheFileByPath(path: string) {
await RNFetchBlob.fs.unlink(path).catch(console.log);
}
-export async function deleteCacheFileByName(name) {
+export async function deleteCacheFileByName(name: string) {
const iosAppGroup =
Platform.OS === "ios"
- ? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
+ ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${name}`;
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
@@ -192,12 +211,12 @@ export async function deleteDCacheFiles() {
}
}
-export async function exists(filename) {
- let path = `${cacheDir}/${filename}`;
+export async function exists(filename: string) {
+ const path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
- ? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
+ ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
@@ -211,6 +230,7 @@ export async function exists(filename) {
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
+ if (!attachment) return false;
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
@@ -234,7 +254,7 @@ export async function exists(filename) {
return exists;
}
-export async function bulkExists(files) {
+export async function bulkExists(files: string[]) {
try {
await createCacheDir();
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
@@ -243,7 +263,7 @@ export async function bulkExists(files) {
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
- ? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
+ ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter(
@@ -262,8 +282,8 @@ export async function getCacheSize() {
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
console.log("Total files", stat.length);
- stat.forEach((s) => {
- total += parseInt(s.size);
+ stat.forEach((file) => {
+ total += parseInt(file.size as unknown as string);
});
return total;
}
diff --git a/apps/mobile/app/common/filesystem/upload.js b/apps/mobile/app/common/filesystem/upload.ts
similarity index 77%
rename from apps/mobile/app/common/filesystem/upload.js
rename to apps/mobile/app/common/filesystem/upload.ts
index 377144dbb..c55f8d87d 100644
--- a/apps/mobile/app/common/filesystem/upload.js
+++ b/apps/mobile/app/common/filesystem/upload.ts
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+import { RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
@@ -24,11 +25,22 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir } from "./io";
-import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
+import {
+ cacheDir,
+ checkUpload,
+ FileSizeResult,
+ getUploadedFileSize
+} from "./utils";
-export async function uploadFile(filename, requestOptions, cancelToken) {
+export async function uploadFile(
+ filename: string,
+ requestOptions: RequestOptions,
+ cancelToken: {
+ cancel: (reason?: string) => Promise;
+ }
+) {
if (!requestOptions) return false;
- let { url, headers } = requestOptions;
+ const { url, headers } = requestOptions;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
@@ -39,7 +51,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
if (!exists && Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
- ? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
+ ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
filePath = appGroupPath;
@@ -54,14 +66,15 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
- let remoteFileSize = await getUploadedFileSize(filename);
- if (remoteFileSize === -1) return false;
- if (remoteFileSize > 0 && remoteFileSize === fileSize) {
+ const remoteFileSize = await getUploadedFileSize(filename);
+ if (remoteFileSize === FileSizeResult.Error) return false;
+
+ if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
- let uploadUrlResponse = await fetch(url, {
+ const uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
@@ -78,7 +91,8 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.info(`Starting upload: ${filename}`);
- let uploadRequest = RNFetchBlob.config({
+ const uploadRequest = RNFetchBlob.config({
+ //@ts-ignore
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
@@ -98,14 +112,14 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
);
});
- cancelToken.cancel = () => {
+ cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
uploadRequest.cancel();
};
- let uploadResponse = await uploadRequest;
- let status = uploadResponse.info().status;
- let uploaded = status >= 200 && status < 300;
+ const uploadResponse = await uploadRequest;
+ const status = uploadResponse.info().status;
+ const uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
@@ -118,12 +132,13 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
);
}
const attachment = await db.attachments.attachment(filename);
+ if (!attachment) return false;
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {
useAttachmentStore.getState().remove(filename);
- ToastManager.error(e, "File upload failed");
+ ToastManager.error(e as Error, "File upload failed");
DatabaseLogger.error(e, "File upload failed", {
filename
});
diff --git a/apps/mobile/app/common/filesystem/utils.js b/apps/mobile/app/common/filesystem/utils.ts
similarity index 65%
rename from apps/mobile/app/common/filesystem/utils.js
rename to apps/mobile/app/common/filesystem/utils.ts
index 0f312debb..b248cf613 100644
--- a/apps/mobile/app/common/filesystem/utils.js
+++ b/apps/mobile/app/common/filesystem/utils.ts
@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+import { hosts, RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { DatabaseLogger, db } from "../database";
-import { hosts } from "@notesnook/core";
export const ABYTES = 17;
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
@@ -31,18 +31,13 @@ export const cacheDir =
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
-export function getRandomId(prefix) {
+export function getRandomId(prefix: string) {
return Math.random()
.toString(36)
.replace("0.", prefix || "");
}
-/**
- *
- * @param {string | undefined} data
- * @returns
- */
-export function parseS3Error(data) {
+export function parseS3Error(data?: string) {
const xml = typeof data === "string" ? data : null;
const error = {
@@ -70,11 +65,19 @@ export function parseS3Error(data) {
}
}
-export function cancelable(operation) {
+export function cancelable(
+ operation: (
+ filename: string,
+ requestOptions: RequestOptions,
+ cancelToken: {
+ cancel: (reason?: string) => Promise;
+ }
+ ) => Promise
+) {
const cancelToken = {
- cancel: () => {}
+ cancel: async (reason?: string) => {}
};
- return (filename, requestOptions) => {
+ return (filename: string, requestOptions: RequestOptions) => {
return {
execute: () => operation(filename, requestOptions, cancelToken),
cancel: async () => {
@@ -84,29 +87,35 @@ export function cancelable(operation) {
};
}
-export function copyFileAsync(source, dest) {
+export function copyFileAsync(source: string, dest: string) {
return new Promise((resolve, reject) => {
- ScopedStorage.copyFile(source, dest, (e, r) => {
+ //@ts-ignore
+ ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
if (e) {
reject(e);
return;
}
- resolve();
+ resolve(true);
});
});
}
-export async function releasePermissions(path) {
+export async function releasePermissions(path: string) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
- for (let uri of uris) {
+ for (const uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}
-export async function getUploadedFileSize(hash) {
+export const FileSizeResult = {
+ Empty: 0,
+ Error: -1
+};
+
+export async function getUploadedFileSize(hash: string) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
@@ -115,16 +124,20 @@ export async function getUploadedFileSize(hash) {
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
- attachmentInfo.headers?.get("content-length")
+ attachmentInfo.headers?.get("content-length") || "0"
);
- return isNaN(contentLength) ? 0 : contentLength;
+ return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
} catch (e) {
DatabaseLogger.error(e);
- return -1;
+ return FileSizeResult.Error;
}
}
-export async function checkUpload(filename, chunkSize, expectedSize) {
+export async function checkUpload(
+ filename: string,
+ chunkSize: number,
+ expectedSize: number
+) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
@@ -138,3 +151,25 @@ export async function checkUpload(filename, chunkSize, expectedSize) {
: undefined;
if (error) throw new Error(error);
}
+
+export async function requestPermission() {
+ if (Platform.OS === "ios") return true;
+ return true;
+}
+export async function checkAndCreateDir(path: string) {
+ const dir =
+ Platform.OS === "ios"
+ ? RNFetchBlob.fs.dirs.DocumentDir + path
+ : RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
+
+ try {
+ const exists = await RNFetchBlob.fs.exists(dir);
+ const isDir = await RNFetchBlob.fs.isDir(dir);
+ if (!exists || !isDir) {
+ await RNFetchBlob.fs.mkdir(dir);
+ }
+ } catch (e) {
+ await RNFetchBlob.fs.mkdir(dir);
+ }
+ return dir;
+}
diff --git a/apps/mobile/app/components/auth/change-password.js b/apps/mobile/app/components/auth/change-password.js
index bab16f0a8..679cfa0eb 100644
--- a/apps/mobile/app/components/auth/change-password.js
+++ b/apps/mobile/app/components/auth/change-password.js
@@ -68,9 +68,14 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
- const result = await BackupService.run(false, "change-password-dialog");
- if (!result.error)
+ const result = await BackupService.run(
+ false,
+ "change-password-dialog",
+ "partial"
+ );
+ if (result.error) {
throw new Error(strings.backupFailed() + `: ${result.error}`);
+ }
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
diff --git a/apps/mobile/app/components/sheets/recovery-key/index.js b/apps/mobile/app/components/sheets/recovery-key/index.jsx
similarity index 98%
rename from apps/mobile/app/components/sheets/recovery-key/index.js
rename to apps/mobile/app/components/sheets/recovery-key/index.jsx
index 04db61ec8..2d0d3b5d0 100644
--- a/apps/mobile/app/components/sheets/recovery-key/index.js
+++ b/apps/mobile/app/components/sheets/recovery-key/index.jsx
@@ -17,12 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+import { sanitizeFilename } from "@notesnook/common";
+import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { createRef } from "react";
import { Platform, View } from "react-native";
+import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
+import { db } from "../../../common/database";
+import filesystem from "../../../common/filesystem";
import {
eSubscribeEvent,
eUnSubscribeEvent,
@@ -30,8 +35,6 @@ import {
} from "../../../services/event-manager";
import { clearMessage } from "../../../services/message";
import SettingsService from "../../../services/settings";
-import { db } from "../../../common/database";
-import Storage from "../../../common/database/storage";
import { eOpenRecoveryKeyDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
@@ -41,9 +44,6 @@ import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
-import RNFetchBlob from "react-native-blob-util";
-import { sanitizeFilename } from "@notesnook/common";
-import { strings } from "@notesnook/intl";
class RecoveryKeySheet extends React.Component {
constructor(props) {
@@ -126,7 +126,7 @@ class RecoveryKeySheet extends React.Component {
"base64"
);
} else {
- path = await Storage.checkAndCreateDir("/");
+ path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, data, "base64");
}
ToastManager.show({
@@ -157,7 +157,7 @@ class RecoveryKeySheet extends React.Component {
if (!file) return;
path = file.uri;
} else {
- path = await Storage.checkAndCreateDir("/");
+ path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, this.state.key, "utf8");
path = path + fileName;
}
diff --git a/apps/mobile/app/screens/settings/debug.tsx b/apps/mobile/app/screens/settings/debug.tsx
index ebdd74576..bb6cd259c 100644
--- a/apps/mobile/app/screens/settings/debug.tsx
+++ b/apps/mobile/app/screens/settings/debug.tsx
@@ -17,24 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import Clipboard from "@react-native-clipboard/clipboard";
-import { LogMessage } from "@notesnook/logger";
+import { sanitizeFilename } from "@notesnook/common";
import { format, LogLevel, logManager } from "@notesnook/core";
+import { strings } from "@notesnook/intl";
+import { LogMessage } from "@notesnook/logger";
+import { useThemeColors } from "@notesnook/theme";
+import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
-import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "react-native-blob-util";
-import Storage from "../../common/database/storage";
+import * as ScopedStorage from "react-native-scoped-storage";
+import filesystem from "../../common/filesystem";
import { presentDialog } from "../../components/dialog/functions";
import { IconButton } from "../../components/ui/icon-button";
import { Notice } from "../../components/ui/notice";
import Paragraph from "../../components/ui/typography/paragraph";
import useTimer from "../../hooks/use-timer";
import { ToastManager } from "../../services/event-manager";
-import { useThemeColors } from "@notesnook/theme";
import { hexToRGBA } from "../../utils/colors";
-import { sanitizeFilename } from "@notesnook/common";
-import { strings } from "@notesnook/intl";
export default function DebugLogs() {
const { colors } = useThemeColors();
@@ -148,7 +148,7 @@ export default function DebugLogs() {
if (!file) return;
path = file.uri;
} else {
- path = await Storage.checkAndCreateDir("/");
+ path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName + ".txt", data, "utf8");
path = path + fileName;
}
diff --git a/apps/mobile/app/screens/settings/restore-backup/index.tsx b/apps/mobile/app/screens/settings/restore-backup/index.tsx
index d22b27625..494abf53e 100644
--- a/apps/mobile/app/screens/settings/restore-backup/index.tsx
+++ b/apps/mobile/app/screens/settings/restore-backup/index.tsx
@@ -28,7 +28,7 @@ import DocumentPicker from "react-native-document-picker";
import * as ScopedStorage from "react-native-scoped-storage";
import { unzip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../../../common/database";
-import storage from "../../../common/database/storage";
+import filesystem from "../../../common/filesystem";
import { deleteCacheFileByName } from "../../../common/filesystem/io";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
import { presentDialog } from "../../../components/dialog/functions";
@@ -300,7 +300,7 @@ export const RestoreBackup = () => {
return;
}
} else {
- const path = await storage.checkAndCreateDir("/backups/");
+ const path = await filesystem.checkAndCreateDir("/backups/");
files = await RNFetchBlob.fs.lstat(path);
}
files = files
diff --git a/apps/mobile/app/services/backup.ts b/apps/mobile/app/services/backup.ts
index c664d2611..033b37da6 100644
--- a/apps/mobile/app/services/backup.ts
+++ b/apps/mobile/app/services/backup.ts
@@ -19,6 +19,7 @@ along with this program. If not, see .
import { sanitizeFilename } from "@notesnook/common";
import { formatDate } from "@notesnook/core";
+import { strings } from "@notesnook/intl";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
@@ -26,20 +27,14 @@ import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { zip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../common/database";
-import storage from "../common/database/storage";
-import filesystem from "../common/filesystem";
+import filesystem, { FileStorage } from "../common/filesystem";
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
import { presentDialog } from "../components/dialog/functions";
-import {
- endProgress,
- startProgress,
- updateProgress
-} from "../components/dialogs/progress";
+import { endProgress, updateProgress } from "../components/dialogs/progress";
import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
-import { strings } from "@notesnook/intl";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -178,7 +173,7 @@ async function run(
let path;
if (Platform.OS === "ios") {
- path = await storage.checkAndCreateDir("/backups");
+ path = await filesystem.checkAndCreateDir("/backups");
}
const backupFileName = sanitizeFilename(
@@ -232,7 +227,7 @@ async function run(
updateProgress({
progress: `Saving attachments in backup... ${file.hash}`
});
- if (await filesystem.exists(file.hash)) {
+ if (await FileStorage.exists(file.hash)) {
await RNFetchBlob.fs.cp(
`${cacheDir}/${file.hash}`,
`${attachmentsDir}/${file.hash}`
@@ -299,7 +294,7 @@ async function run(
path: path
};
} catch (e) {
- ToastManager.error(e, strings.backupFailed(), context || "global");
+ ToastManager.error(e as Error, strings.backupFailed(), context || "global");
if (
(e as Error)?.message?.includes("android.net.Uri") &&
diff --git a/apps/mobile/app/services/exporter.ts b/apps/mobile/app/services/exporter.ts
index 5938a485d..44d993b68 100644
--- a/apps/mobile/app/services/exporter.ts
+++ b/apps/mobile/app/services/exporter.ts
@@ -23,7 +23,6 @@ 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 Storage from "../common/database/storage";
import {
exportNote as _exportNote,
@@ -31,13 +30,13 @@ import {
ExportableNote,
exportNotes
} from "@notesnook/common";
-import { Note } from "@notesnook/core";
-import { FilteredSelector } from "@notesnook/core";
-import { basename, dirname, join, extname } from "pathe";
+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 { strings } from "@notesnook/intl";
const FolderNames: { [name: string]: string } = {
txt: "Text",
@@ -49,7 +48,7 @@ const FolderNames: { [name: string]: string } = {
async function getPath(type: string) {
let path =
Platform.OS === "ios" &&
- (await Storage.checkAndCreateDir(`/exported/${type}/`));
+ (await filesystem.checkAndCreateDir(`/exported/${type}/`));
if (Platform.OS === "android") {
const file = await ScopedStorage.openDocumentTree(true);
diff --git a/apps/mobile/native/ios/Podfile.lock b/apps/mobile/native/ios/Podfile.lock
index a15f39990..069eee95e 100644
--- a/apps/mobile/native/ios/Podfile.lock
+++ b/apps/mobile/native/ios/Podfile.lock
@@ -1021,7 +1021,7 @@ PODS:
- SDWebImage (~> 5.11.1)
- react-native-share-extension (2.6.0):
- React
- - react-native-sodium (1.5.6):
+ - react-native-sodium (1.6.1):
- React
- react-native-theme-switch-animation (0.6.0):
- DoubleConversion
@@ -1859,7 +1859,7 @@ SPEC CHECKSUMS:
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
react-native-screenguard: 8b36a3df84c76cd2b82c477f71c26fa1c8cc14a0
react-native-share-extension: 25437eb1039f7409be6e80a7edf8d02b42e1dc99
- react-native-sodium: 605c1523ec8ff5fbff5e9e7769bbacceb571a3c6
+ react-native-sodium: 4cb76086943a7f60c42b40ebca866695b360a196
react-native-theme-switch-animation: d3eb50365a3829ce5572628888fa514752703f61
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911
React-nativeconfig: ba9a2e54e2f0882cf7882698825052793ed4c851
diff --git a/apps/mobile/native/package.json b/apps/mobile/native/package.json
index 5df967f96..d07aab4db 100644
--- a/apps/mobile/native/package.json
+++ b/apps/mobile/native/package.json
@@ -65,7 +65,7 @@
"react-native-screenguard": "^1.0.0",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-pluralrules": "5.2.14",
- "@ammarahmed/react-native-sodium": "1.5.6",
+ "@ammarahmed/react-native-sodium": "^1.6.1",
"react-native-mmkv-storage": "^0.10.2",
"@react-native-community/datetimepicker": "^8.2.0",
"react-native-exit-app": "github:ammarahm-ed/react-native-exit-app",
diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json
index 48b9497be..fd543819b 100644
--- a/apps/mobile/package-lock.json
+++ b/apps/mobile/package-lock.json
@@ -17,6 +17,7 @@
"@ammarahmed/react-native-share-extension": "^2.7.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
+ "@notesnook/crypto": "file:../../packages/crypto",
"@notesnook/editor": "file:../../packages/editor",
"@notesnook/editor-mobile": "file:../../packages/editor-mobile",
"@notesnook/intl": "file:../../packages/intl",
@@ -24352,7 +24353,6 @@
"../../packages/sodium": {
"name": "@notesnook/sodium",
"version": "2.1.3",
- "dev": true,
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -24971,12 +24971,10 @@
},
"../../packages/sodium/node_modules/libsodium-sumo": {
"version": "0.7.15",
- "dev": true,
"license": "ISC"
},
"../../packages/sodium/node_modules/libsodium-wrappers-sumo": {
"version": "0.7.15",
- "dev": true,
"license": "ISC",
"dependencies": {
"libsodium-sumo": "^0.7.15"
@@ -29080,9 +29078,9 @@
}
},
"node_modules/@ammarahmed/react-native-sodium": {
- "version": "1.5.6",
- "resolved": "https://registry.npmjs.org/@ammarahmed/react-native-sodium/-/react-native-sodium-1.5.6.tgz",
- "integrity": "sha512-DASF/A/cDViTMRnCxvoM35F0v/l/cD8bpecfI+oNOCUmySqWqR37h1L4tdFfEzwnJxLcm/SAyyDSxaa+qgw7BQ=="
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@ammarahmed/react-native-sodium/-/react-native-sodium-1.6.1.tgz",
+ "integrity": "sha512-3qSTIPCEYN8DChHqYuv94ekgtyLF6dinH13JZXdxsj6DHRcZxu9syPrFNb8osItPNx3ncME6UGhPFnSL2eDI8g=="
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
@@ -32854,6 +32852,10 @@
"resolved": "../../packages/core",
"link": true
},
+ "node_modules/@notesnook/crypto": {
+ "resolved": "../../packages/crypto",
+ "link": true
+ },
"node_modules/@notesnook/editor": {
"resolved": "../../packages/editor",
"link": true
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 47bf83155..623f21927 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -43,6 +43,7 @@
"@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes",
+ "@notesnook/crypto": "file:../../packages/crypto",
"diffblazer": "^1.0.1",
"react": "18.2.0",
"react-native": "0.74.5",