2020-11-08 16:46:48 +00:00
import NoteResource from '../models/NoteResource' ;
import BaseModel from '../BaseModel' ;
import BaseService from './BaseService' ;
import Setting from '../models/Setting' ;
import shim from '../shim' ;
2021-01-22 17:41:11 +00:00
import ItemChange from '../models/ItemChange' ;
import Note from '../models/Note' ;
import Resource from '../models/Resource' ;
2024-01-05 14:15:47 +00:00
import SearchEngine from './search/SearchEngine' ;
2021-01-22 17:41:11 +00:00
import ItemChangeUtils from './ItemChangeUtils' ;
2021-01-29 18:45:11 +00:00
import time from '../time' ;
2024-02-09 11:55:29 +00:00
import eventManager , { EventName } from '../eventManager' ;
2019-04-21 13:49:40 +01:00
const { sprintf } = require ( 'sprintf-js' ) ;
2018-03-12 23:40:43 +00:00
2020-11-08 16:46:48 +00:00
export default class ResourceService extends BaseService {
2020-03-16 13:30:54 +11:00
2023-06-30 09:07:03 +01:00
public static isRunningInBackground_ = false ;
private isIndexing_ = false ;
2020-03-16 13:30:54 +11:00
2020-11-12 19:13:28 +00:00
private maintenanceCalls_ : boolean [ ] = [ ] ;
2020-11-12 19:29:22 +00:00
private maintenanceTimer1_ : any = null ;
private maintenanceTimer2_ : any = null ;
2020-03-16 13:30:54 +11:00
2020-11-08 16:46:48 +00:00
public async indexNoteResources() {
2018-03-15 18:08:46 +00:00
this . logger ( ) . info ( 'ResourceService::indexNoteResources: Start' ) ;
2021-01-29 18:45:11 +00:00
if ( this . isIndexing_ ) {
this . logger ( ) . info ( 'ResourceService::indexNoteResources: Already indexing - waiting for it to finish' ) ;
await time . waitTillCondition ( ( ) = > ! this . isIndexing_ ) ;
return ;
}
this . isIndexing_ = true ;
2018-03-12 23:40:43 +00:00
2021-01-29 18:45:11 +00:00
try {
await ItemChange . waitForAllSaved ( ) ;
2019-04-21 13:49:40 +01:00
2021-01-29 18:45:11 +00:00
let foundNoteWithEncryption = false ;
2018-03-12 23:40:43 +00:00
2021-01-29 18:45:11 +00:00
while ( true ) {
const changes = await ItemChange . modelSelectAll ( `
SELECT id , item_id , type
FROM item_changes
WHERE item_type = ?
AND id > ?
ORDER BY id ASC
LIMIT 10
2023-08-22 11:58:53 +01:00
` , [BaseModel.TYPE_NOTE, Setting.value('resourceService.lastProcessedChangeId')],
2021-01-29 18:45:11 +00:00
) ;
2018-03-12 23:40:43 +00:00
2021-01-29 18:45:11 +00:00
if ( ! changes . length ) break ;
2018-03-12 23:40:43 +00:00
2021-01-29 18:45:11 +00:00
const noteIds = changes . map ( ( a : any ) = > a . item_id ) ;
const notes = await Note . modelSelectAll ( ` SELECT id, title, body, encryption_applied FROM notes WHERE id IN (" ${ noteIds . join ( '","' ) } ") ` ) ;
2019-10-02 07:38:16 +01:00
2021-01-29 18:45:11 +00:00
const noteById = ( noteId : string ) = > {
for ( let i = 0 ; i < notes . length ; i ++ ) {
if ( notes [ i ] . id === noteId ) return notes [ i ] ;
}
// The note may have been deleted since the change was recorded. For example in this case:
// - Note created (Some Change object is recorded)
// - Note is deleted
// - ResourceService indexer runs.
// In that case, there will be a change for the note, but the note will be gone.
return null ;
} ;
for ( let i = 0 ; i < changes . length ; i ++ ) {
const change = changes [ i ] ;
if ( change . type === ItemChange . TYPE_CREATE || change . type === ItemChange . TYPE_UPDATE ) {
const note = noteById ( change . item_id ) ;
if ( note ) {
if ( note . encryption_applied ) {
// If we hit an encrypted note, abort processing for now.
// Note will eventually get decrypted and processing can resume then.
// This is a limitation of the change tracking system - we cannot skip a change
// and keep processing the rest since we only keep track of "lastProcessedChangeId".
foundNoteWithEncryption = true ;
break ;
}
await this . setAssociatedResources ( note . id , note . body ) ;
} else {
this . logger ( ) . warn ( ` ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ${ change . item_id } ` ) ;
}
} else if ( change . type === ItemChange . TYPE_DELETE ) {
await NoteResource . remove ( change . item_id ) ;
2018-11-21 19:50:50 +00:00
} else {
2021-01-29 18:45:11 +00:00
throw new Error ( ` Invalid change type: ${ change . type } ` ) ;
2018-11-21 19:50:50 +00:00
}
2021-01-29 18:45:11 +00:00
Setting . setValue ( 'resourceService.lastProcessedChangeId' , change . id ) ;
2018-03-12 23:40:43 +00:00
}
2021-01-29 18:45:11 +00:00
if ( foundNoteWithEncryption ) break ;
2018-03-12 23:40:43 +00:00
}
2019-04-21 13:49:40 +01:00
2021-01-29 18:45:11 +00:00
await Setting . saveAll ( ) ;
2018-03-15 18:08:46 +00:00
2021-01-29 18:45:11 +00:00
await NoteResource . addOrphanedResources ( ) ;
2018-03-15 18:08:46 +00:00
2021-01-29 18:45:11 +00:00
await ItemChangeUtils . deleteProcessedChanges ( ) ;
} catch ( error ) {
this . logger ( ) . error ( 'ResourceService::indexNoteResources:' , error ) ;
}
2018-03-16 17:39:44 +00:00
2021-01-29 18:45:11 +00:00
this . isIndexing_ = false ;
2019-01-14 19:11:54 +00:00
2024-02-09 11:55:29 +00:00
eventManager . emit ( EventName . NoteResourceIndexed ) ;
2018-03-15 18:08:46 +00:00
this . logger ( ) . info ( 'ResourceService::indexNoteResources: Completed' ) ;
}
2020-11-12 19:13:28 +00:00
public async setAssociatedResources ( noteId : string , noteBody : string ) {
2020-11-08 16:46:48 +00:00
const resourceIds = await Note . linkedResourceIds ( noteBody ) ;
await NoteResource . setAssociatedResources ( noteId , resourceIds ) ;
2019-04-21 13:49:40 +01:00
}
2020-11-12 19:13:28 +00:00
public async deleteOrphanResources ( expiryDelay : number = null ) {
2019-05-06 21:35:29 +01:00
if ( expiryDelay === null ) expiryDelay = Setting . value ( 'revisionService.ttlDays' ) * 24 * 60 * 60 * 1000 ;
2018-03-15 18:08:46 +00:00
const resourceIds = await NoteResource . orphanResources ( expiryDelay ) ;
this . logger ( ) . info ( 'ResourceService::deleteOrphanResources:' , resourceIds ) ;
for ( let i = 0 ; i < resourceIds . length ; i ++ ) {
2019-04-21 13:49:40 +01:00
const resourceId = resourceIds [ i ] ;
const results = await SearchEngine . instance ( ) . search ( resourceId ) ;
if ( results . length ) {
const note = await Note . load ( results [ 0 ] . id ) ;
if ( note ) {
this . logger ( ) . info ( sprintf ( 'ResourceService::deleteOrphanResources: Skipping deletion of resource %s because it is still referenced in note %s. Re-indexing note content to fix the issue.' , resourceId , note . id ) ) ;
2020-11-08 16:46:48 +00:00
await this . setAssociatedResources ( note . id , note . body ) ;
2019-04-21 13:49:40 +01:00
}
} else {
await Resource . delete ( resourceId ) ;
}
2018-03-15 18:08:46 +00:00
}
}
2023-06-30 09:11:26 +01:00
private static async autoSetFileSize ( resourceId : string , filePath : string , waitTillExists = true ) {
2019-05-28 22:05:11 +01:00
const itDoes = await shim . fsDriver ( ) . waitTillExists ( filePath , waitTillExists ? 10000 : 0 ) ;
2019-05-19 11:18:44 +01:00
if ( ! itDoes ) {
2019-05-19 12:04:09 +01:00
// this.logger().warn('Trying to set file size on non-existent resource:', resourceId, filePath);
2019-05-19 11:18:44 +01:00
return ;
}
2019-05-12 15:53:42 +01:00
const fileStat = await shim . fsDriver ( ) . stat ( filePath ) ;
await Resource . setFileSizeOnly ( resourceId , fileStat . size ) ;
}
2020-11-08 16:46:48 +00:00
public static async autoSetFileSizes() {
2019-05-12 15:53:42 +01:00
const resources = await Resource . needFileSizeSet ( ) ;
for ( const r of resources ) {
2019-05-28 22:05:11 +01:00
await this . autoSetFileSize ( r . id , Resource . fullPath ( r ) , false ) ;
2019-05-12 15:53:42 +01:00
}
}
2020-11-08 16:46:48 +00:00
public async maintenance() {
2020-03-16 13:30:54 +11:00
this . maintenanceCalls_ . push ( true ) ;
try {
await this . indexNoteResources ( ) ;
await this . deleteOrphanResources ( ) ;
} finally {
this . maintenanceCalls_ . pop ( ) ;
}
2018-03-12 23:40:43 +00:00
}
2020-11-08 16:46:48 +00:00
public static runInBackground() {
2018-03-16 14:32:47 +00:00
if ( this . isRunningInBackground_ ) return ;
this . isRunningInBackground_ = true ;
2020-03-16 13:30:54 +11:00
const service = this . instance ( ) ;
2018-03-16 14:32:47 +00:00
2020-10-09 18:35:46 +01:00
service . maintenanceTimer1_ = shim . setTimeout ( ( ) = > {
2020-11-25 14:40:25 +00:00
void service . maintenance ( ) ;
2018-03-16 14:32:47 +00:00
} , 1000 * 30 ) ;
2019-07-29 15:43:53 +02:00
2020-03-16 13:30:54 +11:00
service . maintenanceTimer2_ = shim . setInterval ( ( ) = > {
2020-11-25 14:40:25 +00:00
void service . maintenance ( ) ;
2018-03-16 14:32:47 +00:00
} , 1000 * 60 * 60 * 4 ) ;
}
2020-03-16 13:30:54 +11:00
2020-11-08 16:46:48 +00:00
public async cancelTimers() {
2020-03-16 13:30:54 +11:00
if ( this . maintenanceTimer1_ ) {
2020-11-08 16:46:48 +00:00
shim . clearTimeout ( this . maintenanceTimer1_ ) ;
2020-03-16 13:30:54 +11:00
this . maintenanceTimer1_ = null ;
}
if ( this . maintenanceTimer2_ ) {
2020-11-08 16:46:48 +00:00
shim . clearInterval ( this . maintenanceTimer2_ ) ;
2020-03-16 13:30:54 +11:00
this . maintenanceTimer2_ = null ;
}
return new Promise ( ( resolve ) = > {
2020-10-09 18:35:46 +01:00
const iid = shim . setInterval ( ( ) = > {
2020-03-16 13:30:54 +11:00
if ( ! this . maintenanceCalls_ . length ) {
2020-10-09 18:35:46 +01:00
shim . clearInterval ( iid ) ;
2021-01-29 18:45:11 +00:00
resolve ( null ) ;
2020-03-16 13:30:54 +11:00
}
} , 100 ) ;
} ) ;
}
2018-03-12 23:40:43 +00:00
2021-01-22 17:41:11 +00:00
public static instance_ : ResourceService = null ;
2020-11-08 16:46:48 +00:00
public static instance() {
if ( this . instance_ ) return this . instance_ ;
this . instance_ = new ResourceService ( ) ;
return this . instance_ ;
}
}