mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 22:49:45 +01:00
377 lines
9.4 KiB
JavaScript
377 lines
9.4 KiB
JavaScript
import Collection from "./collection";
|
|
import getId from "../utils/id";
|
|
import { deleteItem, hasItem } from "../utils/array";
|
|
import { EV, EVENTS, sendAttachmentsProgressEvent } from "../common";
|
|
import dataurl from "../utils/dataurl";
|
|
import dayjs from "dayjs";
|
|
import setManipulator from "../utils/set";
|
|
|
|
export default class Attachments extends Collection {
|
|
constructor(db, name, cached) {
|
|
super(db, name, cached);
|
|
this.key = null;
|
|
}
|
|
|
|
merge(remoteAttachment) {
|
|
if (remoteAttachment.deleted)
|
|
return this._collection.addItem(remoteAttachment);
|
|
|
|
const id = remoteAttachment.id;
|
|
let localAttachment = this._collection.getItem(id);
|
|
|
|
if (localAttachment && localAttachment.noteIds) {
|
|
remoteAttachment.noteIds = setManipulator.union(
|
|
remoteAttachment.noteIds,
|
|
localAttachment.noteIds
|
|
);
|
|
}
|
|
|
|
return this._collection.addItem(remoteAttachment);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {{
|
|
* iv: string,
|
|
* length: number,
|
|
* alg: string,
|
|
* hash: string,
|
|
* hashType: string,
|
|
* filename: string,
|
|
* type: string,
|
|
* salt: string,
|
|
* chunkSize: number,
|
|
* key: {}
|
|
* }} attachmentArg
|
|
* @param {string} noteId Optional as attachments will be parsed at extraction time
|
|
* @returns
|
|
*/
|
|
async add(attachmentArg, noteId = undefined) {
|
|
if (!attachmentArg) return console.error("attachment cannot be undefined");
|
|
if (!attachmentArg.hash) throw new Error("Please provide attachment hash.");
|
|
|
|
const oldAttachment =
|
|
this.all.find((a) => a.metadata.hash === attachmentArg.hash) || {};
|
|
let id = oldAttachment.id || getId();
|
|
|
|
const noteIds = oldAttachment.noteIds || [];
|
|
if (noteId && !noteIds.includes(noteId)) noteIds.push(noteId);
|
|
|
|
const attachment = {
|
|
...oldAttachment,
|
|
...oldAttachment.metadata,
|
|
...attachmentArg,
|
|
noteIds,
|
|
};
|
|
|
|
const {
|
|
iv,
|
|
length,
|
|
alg,
|
|
hash,
|
|
hashType,
|
|
filename,
|
|
salt,
|
|
type,
|
|
chunkSize,
|
|
key,
|
|
} = attachment;
|
|
|
|
if (
|
|
!iv ||
|
|
!length ||
|
|
!alg ||
|
|
!hash ||
|
|
!hashType ||
|
|
!filename ||
|
|
!salt ||
|
|
!chunkSize ||
|
|
!key
|
|
) {
|
|
console.error(
|
|
"Attachment is invalid because all properties are required:",
|
|
attachment
|
|
);
|
|
// throw new Error("Could not add attachment: all properties are required.");
|
|
return;
|
|
}
|
|
|
|
const encryptedKey = attachmentArg.key
|
|
? await this._encryptKey(attachmentArg.key)
|
|
: key;
|
|
const attachmentItem = {
|
|
type: "attachment",
|
|
id,
|
|
noteIds,
|
|
iv,
|
|
salt,
|
|
length,
|
|
alg,
|
|
key: encryptedKey,
|
|
chunkSize,
|
|
metadata: {
|
|
hash,
|
|
hashType,
|
|
filename,
|
|
type: type || "application/octet-stream",
|
|
},
|
|
dateCreated: attachment.dateCreated || Date.now(),
|
|
dateModified: attachment.dateModified,
|
|
dateUploaded: attachment.dateUploaded,
|
|
dateDeleted: undefined,
|
|
failed: attachment.failed,
|
|
};
|
|
return this._collection.addItem(attachmentItem);
|
|
}
|
|
|
|
async generateKey() {
|
|
await this._getEncryptionKey();
|
|
return await this._db.storage.generateRandomKey();
|
|
}
|
|
|
|
async decryptKey(key) {
|
|
const encryptionKey = await this._getEncryptionKey();
|
|
const plainData = await this._db.storage.decrypt(encryptionKey, key);
|
|
return JSON.parse(plainData);
|
|
}
|
|
|
|
async delete(hash, noteId) {
|
|
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
|
if (!attachment || !deleteItem(attachment.noteIds, noteId)) return;
|
|
if (!attachment.noteIds.length) {
|
|
attachment.dateDeleted = Date.now();
|
|
EV.publish(EVENTS.attachmentDeleted, attachment);
|
|
}
|
|
return await this._collection.updateItem(attachment);
|
|
}
|
|
|
|
async remove(hash, localOnly) {
|
|
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
|
if (!attachment) return false;
|
|
if (await this._db.fs.deleteFile(hash, localOnly)) {
|
|
if (!localOnly) {
|
|
await this.detach(hash);
|
|
}
|
|
await this._collection.removeItem(attachment.id);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async detach(hashOrId) {
|
|
const attachment = this.attachment(hashOrId);
|
|
if (!attachment) return;
|
|
|
|
await this._db.notes.init();
|
|
for (let noteId of attachment.noteIds) {
|
|
const note = this._db.notes.note(noteId);
|
|
if (!note) continue;
|
|
const contentId = note.data.contentId;
|
|
await this._db.content.removeAttachments(contentId, [
|
|
attachment.metadata.hash,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get specified type of attachments of a note
|
|
* @param {string} noteId
|
|
* @param {"files"|"images"|"all"} type
|
|
* @returns {Array}
|
|
*/
|
|
ofNote(noteId, type) {
|
|
let attachments = [];
|
|
|
|
if (type === "files") attachments = this.files;
|
|
else if (type === "images") attachments = this.images;
|
|
else if (type === "all") attachments = this.all;
|
|
|
|
return attachments.filter((attachment) =>
|
|
hasItem(attachment.noteIds, noteId)
|
|
);
|
|
}
|
|
|
|
exists(hash) {
|
|
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
|
return !!attachment;
|
|
}
|
|
|
|
/**
|
|
* @param {string} hash
|
|
* @returns {Promise<string>} dataurl formatted string
|
|
*/
|
|
async read(hash) {
|
|
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
|
if (!attachment) return;
|
|
|
|
const key = await this.decryptKey(attachment.key);
|
|
const data = await this._db.fs.readEncrypted(
|
|
attachment.metadata.hash,
|
|
key,
|
|
{
|
|
chunkSize: attachment.chunkSize,
|
|
iv: attachment.iv,
|
|
salt: attachment.salt,
|
|
length: attachment.length,
|
|
alg: attachment.alg,
|
|
outputType: "base64",
|
|
}
|
|
);
|
|
return dataurl.fromObject({ type: attachment.metadata.type, data });
|
|
}
|
|
|
|
attachment(hashOrId) {
|
|
return this.all.find(
|
|
(a) => a.id === hashOrId || a.metadata.hash === hashOrId
|
|
);
|
|
}
|
|
|
|
markAsUploaded(id) {
|
|
const attachment = this.attachment(id);
|
|
if (!attachment) return;
|
|
attachment.dateUploaded = Date.now();
|
|
attachment.failed = undefined;
|
|
return this._collection.updateItem(attachment);
|
|
}
|
|
|
|
reset(id) {
|
|
const attachment = this.attachment(id);
|
|
if (!attachment) return;
|
|
attachment.dateUploaded = undefined;
|
|
return this._collection.updateItem(attachment);
|
|
}
|
|
|
|
markAsFailed(id, reason) {
|
|
const attachment = this.attachment(id);
|
|
if (!attachment) return;
|
|
attachment.failed = reason;
|
|
return this._collection.updateItem(attachment);
|
|
}
|
|
|
|
async save(data, type) {
|
|
const key = await this._db.attachments.generateKey();
|
|
const metadata = await this._db.fs.writeEncrypted(null, data, type, key);
|
|
return { key, metadata };
|
|
}
|
|
|
|
async downloadImages(noteId) {
|
|
const attachments = this.images.filter((attachment) =>
|
|
hasItem(attachment.noteIds, noteId)
|
|
);
|
|
try {
|
|
for (let i = 0; i < attachments.length; i++) {
|
|
const attachment = attachments[i];
|
|
await this._downloadMedia(attachment, {
|
|
total: attachments.length,
|
|
current: i,
|
|
groupId: noteId,
|
|
});
|
|
}
|
|
} finally {
|
|
sendAttachmentsProgressEvent("download", noteId, attachments.length);
|
|
}
|
|
}
|
|
|
|
async _downloadMedia(attachment, { total, current, groupId }, notify = true) {
|
|
const { metadata, chunkSize } = attachment;
|
|
const filename = metadata.hash;
|
|
|
|
sendAttachmentsProgressEvent("download", groupId, total, current);
|
|
const isDownloaded = await this._db.fs.downloadFile(
|
|
groupId,
|
|
filename,
|
|
chunkSize,
|
|
metadata
|
|
);
|
|
if (!isDownloaded) return;
|
|
|
|
const src = await this.read(metadata.hash);
|
|
if (!src) return;
|
|
|
|
if (notify)
|
|
EV.publish(EVENTS.mediaAttachmentDownloaded, {
|
|
groupId,
|
|
hash: metadata.hash,
|
|
src,
|
|
});
|
|
|
|
return src;
|
|
}
|
|
|
|
async cleanup() {
|
|
const now = dayjs().unix();
|
|
for (const attachment of this.deleted) {
|
|
if (dayjs(attachment.dateDeleted).add(7, "days").unix() < now) continue;
|
|
|
|
const isDeleted = await this._db.fs.deleteFile(attachment.metadata.hash);
|
|
if (!isDeleted) continue;
|
|
|
|
await this._collection.removeItem(attachment.id);
|
|
}
|
|
}
|
|
|
|
get pending() {
|
|
return this.all.filter(
|
|
(attachment) =>
|
|
(attachment.dateUploaded <= 0 || !attachment.dateUploaded) &&
|
|
attachment.noteIds.length > 0
|
|
);
|
|
}
|
|
|
|
get uploaded() {
|
|
return this.all.filter((attachment) => attachment.dateUploaded > 0);
|
|
}
|
|
|
|
get syncable() {
|
|
return this._collection
|
|
.getRaw()
|
|
.filter(
|
|
(attachment) => attachment.dateUploaded > 0 || attachment.deleted
|
|
);
|
|
}
|
|
|
|
get deleted() {
|
|
return this.all.filter((attachment) => attachment.dateDeleted > 0);
|
|
}
|
|
|
|
get images() {
|
|
return this.all.filter((attachment) =>
|
|
attachment.metadata.type.startsWith("image/")
|
|
);
|
|
}
|
|
|
|
get files() {
|
|
return this.all.filter(
|
|
(attachment) => !attachment.metadata.type.startsWith("image/")
|
|
);
|
|
}
|
|
|
|
get all() {
|
|
return this._collection.getItems();
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _encryptKey(key) {
|
|
const encryptionKey = await this._getEncryptionKey();
|
|
const encryptedKey = await this._db.storage.encrypt(
|
|
encryptionKey,
|
|
JSON.stringify(key)
|
|
);
|
|
return encryptedKey;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _getEncryptionKey() {
|
|
this.key = await this._db.user.getAttachmentsKey();
|
|
if (!this.key)
|
|
throw new Error(
|
|
"Failed to get user encryption key. Cannot cache attachments."
|
|
);
|
|
return this.key;
|
|
}
|
|
}
|