2023-03-08 09:01:23 +01:00
"use strict" ;
2024-07-18 21:35:17 +03:00
import utils from "../../services/utils.js" ;
import dateUtils from "../../services/date_utils.js" ;
import AbstractBeccaEntity from "./abstract_becca_entity.js" ;
import sql from "../../services/sql.js" ;
import protectedSessionService from "../../services/protected_session.js" ;
import log from "../../services/log.js" ;
2024-07-24 20:19:27 +03:00
import { AttachmentRow } from './rows.js' ;
2024-07-18 21:35:17 +03:00
import BNote from "./bnote.js" ;
import BBranch from "./bbranch.js" ;
2024-07-18 22:58:12 +03:00
import noteService from "../../services/notes.js" ;
2023-03-08 09:01:23 +01:00
2023-04-14 16:49:06 +02:00
const attachmentRoleToNoteTypeMapping = {
2024-03-27 06:59:57 +01:00
'image' : 'image' ,
'file' : 'file'
2023-04-14 16:49:06 +02:00
} ;
2024-02-17 00:44:44 +02:00
interface ContentOpts {
2024-02-18 20:29:23 +02:00
// TODO: Found in bnote.ts, to check if it's actually used and not a typo.
2024-02-17 10:56:27 +02:00
forceSave? : boolean ;
2024-02-17 00:44:44 +02:00
/** will also save this BAttachment entity */
2024-02-17 10:56:27 +02:00
forceFullSave? : boolean ;
2024-02-17 00:44:44 +02:00
/** override frontend heuristics on when to reload, instruct to reload */
2024-02-17 10:56:27 +02:00
forceFrontendReload? : boolean ;
2024-02-17 00:44:44 +02:00
}
2023-03-08 09:01:23 +01:00
/ * *
2023-03-16 12:17:55 +01:00
* Attachment represent data related / attached to the note . Conceptually similar to attributes , but intended for
2023-03-08 09:01:23 +01:00
* larger amounts of data and generally not accessible to the user .
* /
2024-02-17 10:56:27 +02:00
class BAttachment extends AbstractBeccaEntity < BAttachment > {
2023-03-16 12:17:55 +01:00
static get entityName() { return "attachments" ; }
static get primaryKeyName() { return "attachmentId" ; }
2023-09-28 00:24:53 +02:00
static get hashedProperties() { return [ "attachmentId" , "ownerId" , "role" , "mime" , "title" , "blobId" , "utcDateScheduledForErasureSince" ] ; }
2023-03-08 09:01:23 +01:00
2024-02-17 00:44:44 +02:00
noteId? : number ;
attachmentId? : string ;
/** either noteId or revisionId to which this attachment belongs */
2024-03-30 11:09:45 +02:00
ownerId ! : string ;
role ! : string ;
mime ! : string ;
title ! : string ;
2024-02-17 00:44:44 +02:00
type ? : keyof typeof attachmentRoleToNoteTypeMapping ;
position? : number ;
2024-02-17 22:58:54 +02:00
utcDateScheduledForErasureSince? : string | null ;
2024-02-17 00:44:44 +02:00
/** optionally added to the entity */
contentLength? : number ;
isDecrypted? : boolean ;
constructor ( row : AttachmentRow ) {
2023-03-08 09:01:23 +01:00
super ( ) ;
2024-03-30 11:09:45 +02:00
this . updateFromRow ( row ) ;
this . decrypt ( ) ;
}
updateFromRow ( row : AttachmentRow ) : void {
2023-07-14 17:01:56 +02:00
if ( ! row . ownerId ? . trim ( ) ) {
throw new Error ( "'ownerId' must be given to initialize a Attachment entity" ) ;
2023-03-16 12:11:00 +01:00
} else if ( ! row . role ? . trim ( ) ) {
2023-03-16 12:17:55 +01:00
throw new Error ( "'role' must be given to initialize a Attachment entity" ) ;
2023-03-16 12:11:00 +01:00
} else if ( ! row . mime ? . trim ( ) ) {
2023-03-16 12:17:55 +01:00
throw new Error ( "'mime' must be given to initialize a Attachment entity" ) ;
2023-03-16 12:11:00 +01:00
} else if ( ! row . title ? . trim ( ) ) {
2023-03-16 12:17:55 +01:00
throw new Error ( "'title' must be given to initialize a Attachment entity" ) ;
2023-03-08 09:01:23 +01:00
}
2023-03-16 16:37:31 +01:00
this . attachmentId = row . attachmentId ;
2023-07-14 17:01:56 +02:00
this . ownerId = row . ownerId ;
2023-03-16 12:11:00 +01:00
this . role = row . role ;
2023-03-08 09:01:23 +01:00
this . mime = row . mime ;
2023-03-16 12:11:00 +01:00
this . title = row . title ;
2023-04-11 22:55:50 +02:00
this . position = row . position ;
2023-03-16 18:34:39 +01:00
this . blobId = row . blobId ;
2023-03-08 09:01:23 +01:00
this . isProtected = ! ! row . isProtected ;
2023-04-03 23:47:24 +02:00
this . dateModified = row . dateModified ;
2023-03-08 09:01:23 +01:00
this . utcDateModified = row . utcDateModified ;
2023-04-21 00:19:17 +02:00
this . utcDateScheduledForErasureSince = row . utcDateScheduledForErasureSince ;
2023-05-21 18:14:17 +02:00
this . contentLength = row . contentLength ;
2024-03-30 11:09:45 +02:00
}
2023-05-21 18:14:17 +02:00
2024-02-17 00:44:44 +02:00
copy ( ) : BAttachment {
2023-04-11 23:24:39 +02:00
return new BAttachment ( {
2023-07-14 17:01:56 +02:00
ownerId : this.ownerId ,
2023-04-11 23:24:39 +02:00
role : this.role ,
mime : this.mime ,
title : this.title ,
blobId : this.blobId ,
2023-04-25 00:01:58 +02:00
isProtected : this.isProtected
2023-04-11 23:24:39 +02:00
} ) ;
}
2024-02-17 00:44:44 +02:00
getNote ( ) : BNote {
2023-07-14 17:01:56 +02:00
return this . becca . notes [ this . ownerId ] ;
2023-03-08 09:01:23 +01:00
}
2024-02-17 00:44:44 +02:00
/** @returns true if the note has string content (not binary) */
hasStringContent ( ) : boolean {
2024-12-15 16:54:39 +01:00
return utils . isStringNote ( this . type , this . mime ) ; // here was !== undefined && utils.isStringNote(this.type, this.mime); I dont know why we need !=undefined. But it filters out canvas libary items
2023-03-08 09:01:23 +01:00
}
2023-05-02 22:46:39 +02:00
isContentAvailable() {
return ! this . attachmentId // new attachment which was not encrypted yet
|| ! this . isProtected
|| protectedSessionService . isProtectedSessionAvailable ( )
}
2023-05-20 23:46:45 +02:00
getTitleOrProtected() {
return this . isContentAvailable ( ) ? this . title : '[protected]' ;
}
decrypt() {
2023-09-08 00:19:30 +02:00
if ( ! this . isProtected || ! this . attachmentId ) {
this . isDecrypted = true ;
return ;
}
if ( ! this . isDecrypted && protectedSessionService . isProtectedSessionAvailable ( ) ) {
2023-05-20 23:46:45 +02:00
try {
2024-02-17 00:44:44 +02:00
this . title = protectedSessionService . decryptString ( this . title ) || "" ;
2023-05-20 23:46:45 +02:00
this . isDecrypted = true ;
}
2024-02-17 00:44:44 +02:00
catch ( e : any ) {
2023-05-20 23:46:45 +02:00
log . error ( ` Could not decrypt attachment ${ this . attachmentId } : ${ e . message } ${ e . stack } ` ) ;
}
}
}
2024-02-17 22:58:54 +02:00
getContent ( ) : Buffer {
return this . _getContent ( ) as Buffer ;
2023-03-08 09:01:23 +01:00
}
2024-04-05 22:22:18 +03:00
setContent ( content : string | Buffer , opts? : ContentOpts ) {
2023-03-16 16:37:31 +01:00
this . _setContent ( content , opts ) ;
2023-03-08 09:01:23 +01:00
}
2024-02-17 00:44:44 +02:00
convertToNote ( ) : { note : BNote , branch : BBranch } {
2024-02-18 20:29:23 +02:00
// TODO: can this ever be "search"?
2024-02-17 11:54:41 +02:00
if ( this . type as string === 'search' ) {
2023-04-14 16:49:06 +02:00
throw new Error ( ` Note of type search cannot have child notes ` ) ;
}
if ( ! this . getNote ( ) ) {
throw new Error ( "Cannot find note of this attachment. It is possible that this is note revision's attachment. " +
"Converting note revision's attachments to note is not (yet) supported." ) ;
}
if ( ! ( this . role in attachmentRoleToNoteTypeMapping ) ) {
throw new Error ( ` Mapping from attachment role ' ${ this . role } ' to note's type is not defined ` ) ;
}
2023-05-05 23:41:11 +02:00
if ( ! this . isContentAvailable ( ) ) { // isProtected is the same for attachment
2023-04-14 16:49:06 +02:00
throw new Error ( ` Cannot convert protected attachment outside of protected session ` ) ;
}
const { note , branch } = noteService . createNewNote ( {
2023-07-14 17:01:56 +02:00
parentNoteId : this.ownerId ,
2023-04-14 16:49:06 +02:00
title : this.title ,
2024-02-17 11:54:41 +02:00
type : ( attachmentRoleToNoteTypeMapping as any ) [ this . role ] ,
2023-04-14 16:49:06 +02:00
mime : this.mime ,
content : this.getContent ( ) ,
isProtected : this.isProtected
} ) ;
this . markAsDeleted ( ) ;
2023-05-02 22:46:39 +02:00
const parentNote = this . getNote ( ) ;
if ( this . role === 'image' && parentNote . type === 'text' ) {
const origContent = parentNote . getContent ( ) ;
2024-12-22 15:45:54 +02:00
2024-02-17 22:58:54 +02:00
if ( typeof origContent !== "string" ) {
throw new Error ( ` Note with ID ' ${ note . noteId } has a text type but non-string content. ` ) ;
}
2023-05-02 22:46:39 +02:00
const oldAttachmentUrl = ` api/attachments/ ${ this . attachmentId } /image/ ` ;
2023-04-14 16:49:06 +02:00
const newNoteUrl = ` api/images/ ${ note . noteId } / ` ;
const fixedContent = utils . replaceAll ( origContent , oldAttachmentUrl , newNoteUrl ) ;
2023-05-02 22:46:39 +02:00
if ( fixedContent !== origContent ) {
parentNote . setContent ( fixedContent ) ;
2023-04-14 16:49:06 +02:00
}
2023-05-02 23:04:41 +02:00
noteService . asyncPostProcessContent ( note , fixedContent ) ;
2023-04-14 16:49:06 +02:00
}
return { note , branch } ;
}
2023-05-03 10:23:20 +02:00
getFileName() {
const type = this . role === 'image' ? 'image' : 'file' ;
return utils . formatDownloadTitle ( this . title , type , this . mime ) ;
}
2023-03-08 09:01:23 +01:00
beforeSaving() {
super . beforeSaving ( ) ;
2023-04-11 22:55:50 +02:00
if ( this . position === undefined || this . position === null ) {
2024-02-17 00:44:44 +02:00
this . position = 10 + sql . getValue < number > ( ` SELECT COALESCE(MAX(position), 0)
2024-12-22 15:45:54 +02:00
FROM attachments
WHERE ownerId = ? ` , [this.noteId]);
2023-04-11 22:55:50 +02:00
}
2023-04-03 23:47:24 +02:00
this . dateModified = dateUtils . localNowDateTime ( ) ;
2023-03-08 09:01:23 +01:00
this . utcDateModified = dateUtils . utcNowDateTime ( ) ;
}
getPojo() {
return {
2023-03-16 12:17:55 +01:00
attachmentId : this.attachmentId ,
2023-07-14 17:01:56 +02:00
ownerId : this.ownerId ,
2023-03-16 17:43:37 +01:00
role : this.role ,
2023-03-08 09:01:23 +01:00
mime : this.mime ,
2024-02-17 00:44:44 +02:00
title : this.title || undefined ,
2023-04-11 22:55:50 +02:00
position : this.position ,
2023-03-16 18:34:39 +01:00
blobId : this.blobId ,
2023-03-08 09:01:23 +01:00
isProtected : ! ! this . isProtected ,
isDeleted : false ,
2023-04-03 23:47:24 +02:00
dateModified : this.dateModified ,
utcDateModified : this.utcDateModified ,
2023-05-21 18:14:17 +02:00
utcDateScheduledForErasureSince : this.utcDateScheduledForErasureSince ,
contentLength : this.contentLength
2023-03-08 09:01:23 +01:00
} ;
}
getPojoToSave() {
2023-05-20 23:46:45 +02:00
const pojo = this . getPojo ( ) ;
2023-05-21 18:14:17 +02:00
delete pojo . contentLength ;
2023-05-20 23:46:45 +02:00
if ( pojo . isProtected ) {
if ( this . isDecrypted ) {
2024-02-17 00:44:44 +02:00
pojo . title = protectedSessionService . encrypt ( pojo . title || "" ) || undefined ;
2023-05-20 23:46:45 +02:00
}
else {
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo . title ;
}
}
return pojo ;
2023-03-08 09:01:23 +01:00
}
}
2024-07-18 21:50:12 +03:00
export default BAttachment ;