2020-11-08 18:46:48 +02: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 19:41:11 +02:00
import ItemChange from '../models/ItemChange' ;
import Note from '../models/Note' ;
import Resource from '../models/Resource' ;
import SearchEngine from './searchengine/SearchEngine' ;
import ItemChangeUtils from './ItemChangeUtils' ;
2021-01-29 20:45:11 +02:00
import time from '../time' ;
2019-04-21 14:49:40 +02:00
const { sprintf } = require ( 'sprintf-js' ) ;
2018-03-13 01:40:43 +02:00
2020-11-08 18:46:48 +02:00
export default class ResourceService extends BaseService {
2020-03-16 04:30:54 +02:00
2021-01-22 19:41:11 +02:00
public static isRunningInBackground_ : boolean = false ;
2021-01-29 20:45:11 +02:00
private isIndexing_ : boolean = false ;
2020-03-16 04:30:54 +02:00
2020-11-12 21:13:28 +02:00
private maintenanceCalls_ : boolean [ ] = [ ] ;
2020-11-12 21:29:22 +02:00
private maintenanceTimer1_ : any = null ;
private maintenanceTimer2_ : any = null ;
2020-03-16 04:30:54 +02:00
2020-11-08 18:46:48 +02:00
public async indexNoteResources() {
2018-03-15 20:08:46 +02:00
this . logger ( ) . info ( 'ResourceService::indexNoteResources: Start' ) ;
2021-01-29 20:45:11 +02: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-13 01:40:43 +02:00
2021-01-29 20:45:11 +02:00
try {
await ItemChange . waitForAllSaved ( ) ;
2019-04-21 14:49:40 +02:00
2021-01-29 20:45:11 +02:00
let foundNoteWithEncryption = false ;
2018-03-13 01:40:43 +02:00
2021-01-29 20:45:11 +02: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
` , [BaseModel.TYPE_NOTE, Setting.value('resourceService.lastProcessedChangeId')]
) ;
2018-03-13 01:40:43 +02:00
2021-01-29 20:45:11 +02:00
if ( ! changes . length ) break ;
2018-03-13 01:40:43 +02:00
2021-01-29 20:45:11 +02: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 08:38:16 +02:00
2021-01-29 20:45:11 +02: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 21:50:50 +02:00
} else {
2021-01-29 20:45:11 +02:00
throw new Error ( ` Invalid change type: ${ change . type } ` ) ;
2018-11-21 21:50:50 +02:00
}
2021-01-29 20:45:11 +02:00
Setting . setValue ( 'resourceService.lastProcessedChangeId' , change . id ) ;
2018-03-13 01:40:43 +02:00
}
2021-01-29 20:45:11 +02:00
if ( foundNoteWithEncryption ) break ;
2018-03-13 01:40:43 +02:00
}
2019-04-21 14:49:40 +02:00
2021-01-29 20:45:11 +02:00
await Setting . saveAll ( ) ;
2018-03-15 20:08:46 +02:00
2021-01-29 20:45:11 +02:00
await NoteResource . addOrphanedResources ( ) ;
2018-03-15 20:08:46 +02:00
2021-01-29 20:45:11 +02:00
await ItemChangeUtils . deleteProcessedChanges ( ) ;
} catch ( error ) {
this . logger ( ) . error ( 'ResourceService::indexNoteResources:' , error ) ;
}
2018-03-16 19:39:44 +02:00
2021-01-29 20:45:11 +02:00
this . isIndexing_ = false ;
2019-01-14 21:11:54 +02:00
2018-03-15 20:08:46 +02:00
this . logger ( ) . info ( 'ResourceService::indexNoteResources: Completed' ) ;
}
2020-11-12 21:13:28 +02:00
public async setAssociatedResources ( noteId : string , noteBody : string ) {
2020-11-08 18:46:48 +02:00
const resourceIds = await Note . linkedResourceIds ( noteBody ) ;
await NoteResource . setAssociatedResources ( noteId , resourceIds ) ;
2019-04-21 14:49:40 +02:00
}
2020-11-12 21:13:28 +02:00
public async deleteOrphanResources ( expiryDelay : number = null ) {
2019-05-06 22:35:29 +02:00
if ( expiryDelay === null ) expiryDelay = Setting . value ( 'revisionService.ttlDays' ) * 24 * 60 * 60 * 1000 ;
2018-03-15 20:08:46 +02:00
const resourceIds = await NoteResource . orphanResources ( expiryDelay ) ;
this . logger ( ) . info ( 'ResourceService::deleteOrphanResources:' , resourceIds ) ;
for ( let i = 0 ; i < resourceIds . length ; i ++ ) {
2019-04-21 14:49:40 +02: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 18:46:48 +02:00
await this . setAssociatedResources ( note . id , note . body ) ;
2019-04-21 14:49:40 +02:00
}
} else {
await Resource . delete ( resourceId ) ;
}
2018-03-15 20:08:46 +02:00
}
}
2020-11-12 21:13:28 +02:00
private static async autoSetFileSize ( resourceId : string , filePath : string , waitTillExists : boolean = true ) {
2019-05-28 23:05:11 +02:00
const itDoes = await shim . fsDriver ( ) . waitTillExists ( filePath , waitTillExists ? 10000 : 0 ) ;
2019-05-19 12:18:44 +02:00
if ( ! itDoes ) {
2019-05-19 13:04:09 +02:00
// this.logger().warn('Trying to set file size on non-existent resource:', resourceId, filePath);
2019-05-19 12:18:44 +02:00
return ;
}
2019-05-12 16:53:42 +02:00
const fileStat = await shim . fsDriver ( ) . stat ( filePath ) ;
await Resource . setFileSizeOnly ( resourceId , fileStat . size ) ;
}
2020-11-08 18:46:48 +02:00
public static async autoSetFileSizes() {
2019-05-12 16:53:42 +02:00
const resources = await Resource . needFileSizeSet ( ) ;
for ( const r of resources ) {
2019-05-28 23:05:11 +02:00
await this . autoSetFileSize ( r . id , Resource . fullPath ( r ) , false ) ;
2019-05-12 16:53:42 +02:00
}
}
2020-11-08 18:46:48 +02:00
public async maintenance() {
2020-03-16 04:30:54 +02:00
this . maintenanceCalls_ . push ( true ) ;
try {
await this . indexNoteResources ( ) ;
await this . deleteOrphanResources ( ) ;
} finally {
this . maintenanceCalls_ . pop ( ) ;
}
2018-03-13 01:40:43 +02:00
}
2020-11-08 18:46:48 +02:00
public static runInBackground() {
2018-03-16 16:32:47 +02:00
if ( this . isRunningInBackground_ ) return ;
this . isRunningInBackground_ = true ;
2020-03-16 04:30:54 +02:00
const service = this . instance ( ) ;
2018-03-16 16:32:47 +02:00
2020-10-09 19:35:46 +02:00
service . maintenanceTimer1_ = shim . setTimeout ( ( ) = > {
2020-11-25 16:40:25 +02:00
void service . maintenance ( ) ;
2018-03-16 16:32:47 +02:00
} , 1000 * 30 ) ;
2019-07-29 15:43:53 +02:00
2020-03-16 04:30:54 +02:00
service . maintenanceTimer2_ = shim . setInterval ( ( ) = > {
2020-11-25 16:40:25 +02:00
void service . maintenance ( ) ;
2018-03-16 16:32:47 +02:00
} , 1000 * 60 * 60 * 4 ) ;
}
2020-03-16 04:30:54 +02:00
2020-11-08 18:46:48 +02:00
public async cancelTimers() {
2020-03-16 04:30:54 +02:00
if ( this . maintenanceTimer1_ ) {
2020-11-08 18:46:48 +02:00
shim . clearTimeout ( this . maintenanceTimer1_ ) ;
2020-03-16 04:30:54 +02:00
this . maintenanceTimer1_ = null ;
}
if ( this . maintenanceTimer2_ ) {
2020-11-08 18:46:48 +02:00
shim . clearInterval ( this . maintenanceTimer2_ ) ;
2020-03-16 04:30:54 +02:00
this . maintenanceTimer2_ = null ;
}
return new Promise ( ( resolve ) = > {
2020-10-09 19:35:46 +02:00
const iid = shim . setInterval ( ( ) = > {
2020-03-16 04:30:54 +02:00
if ( ! this . maintenanceCalls_ . length ) {
2020-10-09 19:35:46 +02:00
shim . clearInterval ( iid ) ;
2021-01-29 20:45:11 +02:00
resolve ( null ) ;
2020-03-16 04:30:54 +02:00
}
} , 100 ) ;
} ) ;
}
2018-03-13 01:40:43 +02:00
2021-01-22 19:41:11 +02:00
public static instance_ : ResourceService = null ;
2020-11-08 18:46:48 +02:00
public static instance() {
if ( this . instance_ ) return this . instance_ ;
this . instance_ = new ResourceService ( ) ;
return this . instance_ ;
}
}