2018-03-13 01:40:43 +02:00
const ItemChange = require ( 'lib/models/ItemChange' ) ;
const NoteResource = require ( 'lib/models/NoteResource' ) ;
const Note = require ( 'lib/models/Note' ) ;
2018-03-15 20:08:46 +02:00
const Resource = require ( 'lib/models/Resource' ) ;
2018-03-13 01:40:43 +02:00
const BaseModel = require ( 'lib/BaseModel' ) ;
2018-03-15 20:08:46 +02:00
const BaseService = require ( 'lib/services/BaseService' ) ;
2019-04-21 14:49:40 +02:00
const SearchEngine = require ( 'lib/services/SearchEngine' ) ;
2018-12-10 02:39:31 +02:00
const Setting = require ( 'lib/models/Setting' ) ;
2018-03-16 16:32:47 +02:00
const { shim } = require ( 'lib/shim' ) ;
2019-01-14 21:11:54 +02:00
const ItemChangeUtils = require ( 'lib/services/ItemChangeUtils' ) ;
2019-04-21 14:49:40 +02:00
const { sprintf } = require ( 'sprintf-js' ) ;
2018-03-13 01:40:43 +02:00
2018-03-15 20:08:46 +02:00
class ResourceService extends BaseService {
2020-03-16 04:30:54 +02:00
constructor ( ) {
super ( ) ;
this . maintenanceCalls _ = [ ] ;
this . maintenanceTimer1 _ = null ;
this . maintenanceTimer2 _ = null ;
}
static instance ( ) {
if ( this . instance _ ) return this . instance _ ;
this . instance _ = new ResourceService ( ) ;
return this . instance _ ;
}
2018-03-13 01:40:43 +02:00
async indexNoteResources ( ) {
2018-03-15 20:08:46 +02:00
this . logger ( ) . info ( 'ResourceService::indexNoteResources: Start' ) ;
await ItemChange . waitForAllSaved ( ) ;
2018-03-13 01:40:43 +02:00
2019-04-21 14:49:40 +02:00
let foundNoteWithEncryption = false ;
2018-03-13 01:40:43 +02:00
while ( true ) {
2019-10-12 00:18:40 +02:00
const changes = await ItemChange . modelSelectAll ( `
2018-03-15 20:08:46 +02:00
SELECT id , item _id , type
2018-03-13 01:40:43 +02:00
FROM item _changes
WHERE item _type = ?
AND id > ?
2018-03-15 20:08:46 +02:00
ORDER BY id ASC
2019-06-29 00:49:43 +02:00
LIMIT 10
2019-07-29 15:43:53 +02:00
` ,
2019-10-12 00:18:40 +02:00
[ BaseModel . TYPE _NOTE , Setting . value ( 'resourceService.lastProcessedChangeId' ) ]
2019-07-29 15:43:53 +02:00
) ;
2018-03-13 01:40:43 +02:00
if ( ! changes . length ) break ;
2020-05-21 10:14:33 +02:00
const noteIds = changes . map ( a => a . item _id ) ;
2019-09-19 23:51:18 +02:00
const notes = await Note . modelSelectAll ( ` SELECT id, title, body, encryption_applied FROM notes WHERE id IN (" ${ noteIds . join ( '","' ) } ") ` ) ;
2018-03-13 01:40:43 +02:00
2020-05-21 10:14:33 +02:00
const noteById = noteId => {
2018-03-15 20:08:46 +02:00
for ( let i = 0 ; i < notes . length ; i ++ ) {
if ( notes [ i ] . id === noteId ) return notes [ i ] ;
}
2018-11-21 21:50:50 +02:00
// 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 ;
2019-07-29 15:43:53 +02:00
} ;
2018-03-15 20:08:46 +02:00
for ( let i = 0 ; i < changes . length ; i ++ ) {
const change = changes [ i ] ;
2018-03-13 01:40:43 +02:00
if ( change . type === ItemChange . TYPE _CREATE || change . type === ItemChange . TYPE _UPDATE ) {
2018-03-15 20:08:46 +02:00
const note = noteById ( change . item _id ) ;
2019-07-29 15:43:53 +02:00
2018-11-21 21:50:50 +02:00
if ( note ) {
2019-10-02 08:38:16 +02:00
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 ;
}
2019-04-21 14:49:40 +02:00
await this . setAssociatedResources _ ( note ) ;
2018-11-21 21:50:50 +02:00
} else {
2019-09-19 23:51:18 +02:00
this . logger ( ) . warn ( ` ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ${ change . item _id } ` ) ;
2018-11-21 21:50:50 +02:00
}
2018-03-13 01:40:43 +02:00
} else if ( change . type === ItemChange . TYPE _DELETE ) {
2018-03-15 20:08:46 +02:00
await NoteResource . remove ( change . item _id ) ;
2018-03-13 01:40:43 +02:00
} else {
2019-09-19 23:51:18 +02:00
throw new Error ( ` Invalid change type: ${ change . type } ` ) ;
2018-03-13 01:40:43 +02:00
}
2018-12-10 02:39:31 +02:00
Setting . setValue ( 'resourceService.lastProcessedChangeId' , change . id ) ;
2018-03-13 01:40:43 +02:00
}
2019-04-21 14:49:40 +02:00
if ( foundNoteWithEncryption ) break ;
2018-03-13 01:40:43 +02:00
}
2018-03-15 20:08:46 +02:00
2018-12-10 02:39:31 +02:00
await Setting . saveAll ( ) ;
2018-03-15 20:08:46 +02:00
2018-03-16 19:39:44 +02:00
await NoteResource . addOrphanedResources ( ) ;
2019-01-14 21:11:54 +02:00
await ItemChangeUtils . deleteProcessedChanges ( ) ;
2018-03-15 20:08:46 +02:00
this . logger ( ) . info ( 'ResourceService::indexNoteResources: Completed' ) ;
}
2019-04-21 14:49:40 +02:00
async setAssociatedResources _ ( note ) {
const resourceIds = await Note . linkedResourceIds ( note . body ) ;
await NoteResource . setAssociatedResources ( note . id , resourceIds ) ;
}
2018-03-15 20:08:46 +02:00
async deleteOrphanResources ( expiryDelay = 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 ) ) ;
await this . setAssociatedResources _ ( note ) ;
}
} else {
await Resource . delete ( resourceId ) ;
}
2018-03-15 20:08:46 +02:00
}
}
2019-05-28 23:05:11 +02:00
static async autoSetFileSize ( resourceId , filePath , waitTillExists = true ) {
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 ) ;
}
static async autoSetFileSizes ( ) {
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
}
}
2018-03-15 20:08:46 +02:00
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
}
2018-03-16 16:32:47 +02:00
static runInBackground ( ) {
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-03-16 04:30:54 +02:00
service . maintenanceTimer1 _ = setTimeout ( ( ) => {
2018-03-16 16:32:47 +02:00
service . maintenance ( ) ;
} , 1000 * 30 ) ;
2019-07-29 15:43:53 +02:00
2020-03-16 04:30:54 +02:00
service . maintenanceTimer2 _ = shim . setInterval ( ( ) => {
2018-03-16 16:32:47 +02:00
service . maintenance ( ) ;
} , 1000 * 60 * 60 * 4 ) ;
}
2020-03-16 04:30:54 +02:00
async cancelTimers ( ) {
if ( this . maintenanceTimer1 _ ) {
clearTimeout ( this . maintenanceTimer1 ) ;
this . maintenanceTimer1 _ = null ;
}
if ( this . maintenanceTimer2 _ ) {
shim . clearInterval ( this . maintenanceTimer2 ) ;
this . maintenanceTimer2 _ = null ;
}
return new Promise ( ( resolve ) => {
const iid = setInterval ( ( ) => {
if ( ! this . maintenanceCalls _ . length ) {
clearInterval ( iid ) ;
resolve ( ) ;
}
} , 100 ) ;
} ) ;
}
2018-03-13 01:40:43 +02:00
}
2019-07-29 15:43:53 +02:00
module . exports = ResourceService ;