@@ -9,8 +9,11 @@ import cls from "../../services/cls.js";
99import log from "../../services/log.js" ;
1010import protectedSessionService from "../../services/protected_session.js" ;
1111import blobService from "../../services/blob.js" ;
12+ import blobStorageService from "../../services/blob-storage.js" ;
13+ import type { Blob } from "../../services/blob-interface.js" ;
1214import type { default as Becca , ConstructorData } from "../becca-interface.js" ;
1315import becca from "../becca.js" ;
16+ import type { BlobContentLocation , BlobRow } from "@triliumnext/commons" ;
1417
1518interface ContentOpts {
1619 forceSave ?: boolean ;
@@ -195,6 +198,14 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
195198 return ;
196199 }
197200
201+ if ( blobStorageService . hasExternalContentColumns ( ) ) {
202+ const row = sql . getRow < { contentLocation : string } > ( "SELECT contentLocation FROM blobs WHERE blobId = ?" , [ oldBlobId ] ) ;
203+ if ( row ?. contentLocation . startsWith ( 'file://' ) ) {
204+ const filePath = row . contentLocation . replace ( 'file://' , '' ) ;
205+ blobStorageService . deleteExternal ( filePath ) ;
206+ }
207+ }
208+
198209 sql . execute ( "DELETE FROM blobs WHERE blobId = ?" , [ oldBlobId ] ) ;
199210 // blobs are not marked as erased in entity_changes, they are just purged completely
200211 // this is because technically every keystroke can create a new blob, and there would be just too many
@@ -225,14 +236,40 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
225236 return newBlobId ;
226237 }
227238
228- const pojo = {
239+ // Check if we should store this blob externally
240+ const shouldStoreExternally = blobStorageService . shouldStoreExternally ( content ) ;
241+ let contentLocation : BlobContentLocation = 'internal' ;
242+ if ( shouldStoreExternally ) {
243+ try {
244+ const filePath = blobStorageService . saveExternal ( newBlobId , content ) ;
245+ contentLocation = `file://${ filePath } ` as BlobContentLocation ;
246+ } catch ( error ) {
247+ log . error ( `Failed to store blob ${ newBlobId } externally, falling back to internal storage: ${ error } ` ) ;
248+ contentLocation = 'internal' ;
249+ }
250+ }
251+
252+ const contentLength = blobService . getContentLength ( content ) ;
253+
254+ const pojo : BlobRow = {
229255 blobId : newBlobId ,
230- content : content ,
256+ content : contentLocation === 'internal' ? content : null ,
257+ contentLocation,
258+ contentLength,
231259 dateModified : dateUtils . localNowDateTime ( ) ,
232260 utcDateModified : dateUtils . utcNowDateTime ( )
233261 } ;
234262
235- sql . upsert ( "blobs" , "blobId" , pojo ) ;
263+ // external content columns might not be present when applying older migrations
264+ const pojoToSave = blobStorageService . hasExternalContentColumns ( )
265+ ? pojo
266+ : {
267+ blobId : pojo . blobId ,
268+ content,
269+ dateModified : pojo . dateModified ,
270+ utcDateModified : pojo . utcDateModified
271+ } ;
272+ sql . upsert ( "blobs" , "blobId" , pojoToSave ) ;
236273
237274 // we can't reuse blobId as an entity_changes hash, because this one has to be calculatable without having
238275 // access to the decrypted content
@@ -259,14 +296,20 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
259296 }
260297
261298 protected _getContent ( ) : string | Buffer {
262- const row = sql . getRow < { content : string | Buffer } > ( /*sql*/ `SELECT content FROM blobs WHERE blobId = ?` , [ this . blobId ] ) ;
299+ const query = blobStorageService . hasExternalContentColumns ( )
300+ ? /*sql*/ `SELECT content, contentLocation FROM blobs WHERE blobId = ?`
301+ : /*sql*/ `SELECT content, 'internal' as contentLocation FROM blobs WHERE blobId = ?` ;
302+
303+ const row = sql . getRow < { content : string | Buffer , contentLocation : string } > ( query , [ this . blobId ] ) ;
263304
264305 if ( ! row ) {
265306 const constructorData = this . constructor as unknown as ConstructorData < T > ;
266307 throw new Error ( `Cannot find content for ${ constructorData . primaryKeyName } '${ ( this as any ) [ constructorData . primaryKeyName ] } ', blobId '${ this . blobId } '` ) ;
267308 }
268309
269- return blobService . processContent ( row . content , this . isProtected || false , this . hasStringContent ( ) ) ;
310+ const content = blobStorageService . getContent ( row ) ;
311+
312+ return blobService . processContent ( content , this . isProtected || false , this . hasStringContent ( ) ) ;
270313 }
271314
272315 /**
0 commit comments