2021-05-21 15:17:21 +02:00
import time from '../time' ;
import NoteResource from '../models/NoteResource' ;
import ResourceService from '../services/ResourceService' ;
import shim from '../shim' ;
2021-08-12 17:54:10 +02:00
import { resourceService , decryptionWorker , supportDir , encryptionService , loadEncryptionMasterKey , allSyncTargetItemsEncrypted , setupDatabaseAndSynchronizer , db , synchronizer , switchClient } from '../testing/test-utils' ;
2021-05-21 15:17:21 +02:00
import Folder from '../models/Folder' ;
import Note from '../models/Note' ;
import Resource from '../models/Resource' ;
2024-01-05 16:15:47 +02:00
import SearchEngine from './search/SearchEngine' ;
2021-08-12 17:54:10 +02:00
import { loadMasterKeysFromSettings , setupAndEnableEncryption } from './e2ee/utils' ;
2018-03-15 20:08:46 +02:00
2023-02-20 17:02:29 +02:00
describe ( 'services/ResourceService' , ( ) = > {
2018-03-15 20:08:46 +02:00
2022-11-15 12:23:50 +02:00
beforeEach ( async ( ) = > {
2018-03-15 20:08:46 +02:00
await setupDatabaseAndSynchronizer ( 1 ) ;
2019-04-21 14:49:40 +02:00
await setupDatabaseAndSynchronizer ( 2 ) ;
2018-03-15 20:08:46 +02:00
await switchClient ( 1 ) ;
} ) ;
2020-12-01 20:05:24 +02:00
it ( 'should delete orphaned resources' , ( async ( ) = > {
2018-03-15 20:08:46 +02:00
const service = new ResourceService ( ) ;
2020-03-14 01:46:14 +02:00
const folder1 = await Folder . save ( { title : 'folder1' } ) ;
2018-03-15 20:08:46 +02:00
let note1 = await Note . save ( { title : 'ma note' , parent_id : folder1.id } ) ;
2021-05-21 15:17:21 +02:00
note1 = await shim . attachFileToNote ( note1 , ` ${ supportDir } /photo.jpg ` ) ;
2020-03-14 01:46:14 +02:00
const resource1 = ( await Resource . all ( ) ) [ 0 ] ;
2018-03-15 20:08:46 +02:00
const resourcePath = Resource . fullPath ( resource1 ) ;
await service . indexNoteResources ( ) ;
await service . deleteOrphanResources ( 0 ) ;
expect ( ! ! ( await Resource . load ( resource1 . id ) ) ) . toBe ( true ) ;
await Note . delete ( note1 . id ) ;
await service . deleteOrphanResources ( 0 ) ;
expect ( ! ! ( await Resource . load ( resource1 . id ) ) ) . toBe ( true ) ;
await service . indexNoteResources ( ) ;
await service . deleteOrphanResources ( 1000 * 60 ) ;
expect ( ! ! ( await Resource . load ( resource1 . id ) ) ) . toBe ( true ) ;
await service . deleteOrphanResources ( 0 ) ;
expect ( ! ! ( await Resource . load ( resource1 . id ) ) ) . toBe ( false ) ;
expect ( await shim . fsDriver ( ) . exists ( resourcePath ) ) . toBe ( false ) ;
2018-03-16 19:39:44 +02:00
expect ( ! ( await NoteResource . all ( ) ) . length ) . toBe ( true ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should not delete resource if still associated with at least one note' , ( async ( ) = > {
2018-03-16 19:39:44 +02:00
const service = new ResourceService ( ) ;
2020-03-14 01:46:14 +02:00
const folder1 = await Folder . save ( { title : 'folder1' } ) ;
2018-03-16 19:39:44 +02:00
let note1 = await Note . save ( { title : 'ma note' , parent_id : folder1.id } ) ;
2020-03-14 01:46:14 +02:00
const note2 = await Note . save ( { title : 'ma deuxième note' , parent_id : folder1.id } ) ;
2021-05-21 15:17:21 +02:00
note1 = await shim . attachFileToNote ( note1 , ` ${ supportDir } /photo.jpg ` ) ;
2020-03-14 01:46:14 +02:00
const resource1 = ( await Resource . all ( ) ) [ 0 ] ;
2018-03-16 19:39:44 +02:00
await service . indexNoteResources ( ) ;
await Note . delete ( note1 . id ) ;
2019-07-30 09:35:42 +02:00
2018-03-16 19:39:44 +02:00
await service . indexNoteResources ( ) ;
2019-07-30 09:35:42 +02:00
2023-10-31 18:53:47 +02:00
await Note . save ( { id : note2.id , body : Resource.markupTag ( resource1 ) } ) ;
2018-03-16 19:39:44 +02:00
await service . indexNoteResources ( ) ;
await service . deleteOrphanResources ( 0 ) ;
expect ( ! ! ( await Resource . load ( resource1 . id ) ) ) . toBe ( true ) ;
2018-03-15 20:08:46 +02:00
} ) ) ;
2021-10-05 18:47:38 +02:00
// This is now handled below by more correct tests
//
// it('should not delete a resource that has never been associated with any note, because it probably means the resource came via sync, and associated note has not arrived yet', (async () => {
// const service = new ResourceService();
// await shim.createResourceFromPath(`${supportDir}/photo.jpg`);
2018-06-17 17:59:06 +02:00
2021-10-05 18:47:38 +02:00
// await service.indexNoteResources();
// await service.deleteOrphanResources(0);
2018-06-17 17:59:06 +02:00
2021-10-05 18:47:38 +02:00
// expect((await Resource.all()).length).toBe(1);
// }));
2018-06-17 17:59:06 +02:00
2020-12-01 20:05:24 +02:00
it ( 'should not delete resource if it is used in an IMG tag' , ( async ( ) = > {
2018-09-30 20:24:02 +02:00
const service = new ResourceService ( ) ;
2020-03-14 01:46:14 +02:00
const folder1 = await Folder . save ( { title : 'folder1' } ) ;
2018-09-30 20:24:02 +02:00
let note1 = await Note . save ( { title : 'ma note' , parent_id : folder1.id } ) ;
2021-05-21 15:17:21 +02:00
note1 = await shim . attachFileToNote ( note1 , ` ${ supportDir } /photo.jpg ` ) ;
2020-03-14 01:46:14 +02:00
const resource1 = ( await Resource . all ( ) ) [ 0 ] ;
2018-09-30 20:24:02 +02:00
await service . indexNoteResources ( ) ;
2019-09-19 23:51:18 +02:00
await Note . save ( { id : note1.id , body : ` This is HTML: <img src=":/ ${ resource1 . id } "/> ` } ) ;
2019-07-30 09:35:42 +02:00
2018-09-30 20:24:02 +02:00
await service . indexNoteResources ( ) ;
2019-07-30 09:35:42 +02:00
2018-09-30 20:24:02 +02:00
await service . deleteOrphanResources ( 0 ) ;
expect ( ! ! ( await Resource . load ( resource1 . id ) ) ) . toBe ( true ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should not process twice the same change' , ( async ( ) = > {
2018-12-10 02:39:31 +02:00
const service = new ResourceService ( ) ;
2020-03-14 01:46:14 +02:00
const folder1 = await Folder . save ( { title : 'folder1' } ) ;
2020-11-08 18:46:48 +02:00
const note1 = await Note . save ( { title : 'ma note' , parent_id : folder1.id } ) ;
2021-05-21 15:17:21 +02:00
await shim . attachFileToNote ( note1 , ` ${ supportDir } /photo.jpg ` ) ;
2018-12-10 02:39:31 +02:00
await service . indexNoteResources ( ) ;
const before = ( await NoteResource . all ( ) ) [ 0 ] ;
await time . sleep ( 0.1 ) ;
await service . indexNoteResources ( ) ;
const after = ( await NoteResource . all ( ) ) [ 0 ] ;
expect ( before . last_seen_time ) . toBe ( after . last_seen_time ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should not delete resources that are associated with an encrypted note' , ( async ( ) = > {
2019-04-21 14:49:40 +02:00
// https://github.com/laurent22/joplin/issues/1433
//
// Client 1 and client 2 have E2EE setup.
//
// - Client 1 creates note N1 and add resource R1 to it
// - Client 1 syncs
// - Client 2 syncs and get N1
// - Client 2 add resource R2 to N1
// - Client 2 syncs
// - Client 1 syncs
// - Client 1 runs resource indexer - but because N1 hasn't been decrypted yet, it found that R1 is no longer associated with any note
// - Client 1 decrypts notes, but too late
2019-07-30 09:35:42 +02:00
//
2019-04-21 14:49:40 +02:00
// Eventually R1 is deleted because service thinks that it was at some point associated with a note, but no longer.
const masterKey = await loadEncryptionMasterKey ( ) ;
2021-08-12 17:54:10 +02:00
await setupAndEnableEncryption ( encryptionService ( ) , masterKey , '123456' ) ;
await loadMasterKeysFromSettings ( encryptionService ( ) ) ;
2020-03-14 01:46:14 +02:00
const folder1 = await Folder . save ( { title : 'folder1' } ) ;
const note1 = await Note . save ( { title : 'ma note' , parent_id : folder1.id } ) ;
2021-05-21 15:17:21 +02:00
await shim . attachFileToNote ( note1 , ` ${ supportDir } /photo.jpg ` ) ; // R1
2019-04-21 14:49:40 +02:00
await resourceService ( ) . indexNoteResources ( ) ;
await synchronizer ( ) . start ( ) ;
expect ( await allSyncTargetItemsEncrypted ( ) ) . toBe ( true ) ;
await switchClient ( 2 ) ;
await synchronizer ( ) . start ( ) ;
2021-08-12 17:54:10 +02:00
await setupAndEnableEncryption ( encryptionService ( ) , masterKey , '123456' ) ;
await loadMasterKeysFromSettings ( encryptionService ( ) ) ;
2019-04-21 14:49:40 +02:00
await decryptionWorker ( ) . start ( ) ;
{
const n1 = await Note . load ( note1 . id ) ;
2021-05-21 15:17:21 +02:00
await shim . attachFileToNote ( n1 , ` ${ supportDir } /photo.jpg ` ) ; // R2
2019-04-21 14:49:40 +02:00
}
await synchronizer ( ) . start ( ) ;
await switchClient ( 1 ) ;
await synchronizer ( ) . start ( ) ;
await resourceService ( ) . indexNoteResources ( ) ;
await resourceService ( ) . deleteOrphanResources ( 0 ) ; // Previously, R1 would be deleted here because it's not indexed
expect ( ( await Resource . all ( ) ) . length ) . toBe ( 2 ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should double-check if the resource is still linked before deleting it' , ( async ( ) = > {
2019-04-21 14:49:40 +02:00
SearchEngine . instance ( ) . setDb ( db ( ) ) ; // /!\ Note that we use the global search engine here, which we shouldn't but will work for now
2020-03-14 01:46:14 +02:00
const folder1 = await Folder . save ( { title : 'folder1' } ) ;
2019-04-21 14:49:40 +02:00
let note1 = await Note . save ( { title : 'ma note' , parent_id : folder1.id } ) ;
2021-05-21 15:17:21 +02:00
note1 = await shim . attachFileToNote ( note1 , ` ${ supportDir } /photo.jpg ` ) ;
2019-04-21 14:49:40 +02:00
await resourceService ( ) . indexNoteResources ( ) ;
const bodyWithResource = note1 . body ;
await Note . save ( { id : note1.id , body : '' } ) ;
await resourceService ( ) . indexNoteResources ( ) ;
await Note . save ( { id : note1.id , body : bodyWithResource } ) ;
await SearchEngine . instance ( ) . syncTables ( ) ;
await resourceService ( ) . deleteOrphanResources ( 0 ) ;
expect ( ( await Resource . all ( ) ) . length ) . toBe ( 1 ) ; // It should not have deleted the resource
const nr = ( await NoteResource . all ( ) ) [ 0 ] ;
expect ( ! ! nr . is_associated ) . toBe ( true ) ; // And it should have fixed the situation by re-indexing the note content
} ) ) ;
2021-10-05 18:47:38 +02:00
it ( 'should delete a resource if it is not associated with any note and has never been synced' , ( async ( ) = > {
// - User creates a note and attaches a resource
// - User deletes the note
// - NoteResource service runs - the resource can be deleted
//
// This is because, since the resource didn't come via sync, it's
// guaranteed that it is orphaned, which means it can be safely deleted.
// See related test below to handle case where the resource actually
// comes via sync.
const note = await Note . save ( { } ) ;
await shim . attachFileToNote ( note , ` ${ supportDir } /photo.jpg ` ) ;
await Note . delete ( note . id ) ;
const resource = ( await Resource . all ( ) ) [ 0 ] ;
await resourceService ( ) . indexNoteResources ( ) ;
await resourceService ( ) . deleteOrphanResources ( - 10 ) ;
expect ( await Resource . load ( resource . id ) ) . toBeFalsy ( ) ;
} ) ) ;
it ( 'should NOT delete a resource if it arrived via synced, even if it is not associated with any note' , ( async ( ) = > {
// - C1 creates Resource 1
// - C1 sync
// - C2 sync
// - NoteResource service runs - should find an orphan resource, but not
// delete it because the associated note might come later from U1.
//
// At this point, C1 has the knowledge about Resource 1 so whether it's
// eventually deleted or not will come from there. If it's an orphaned
// resource, it will be deleted on C1 first, then the deletion will be
// synced to other clients.
const note = await Note . save ( { } ) ;
await shim . attachFileToNote ( note , ` ${ supportDir } /photo.jpg ` ) ;
await Note . delete ( note . id ) ;
const resource = ( await Resource . all ( ) ) [ 0 ] ;
await synchronizer ( ) . start ( ) ;
await switchClient ( 2 ) ;
await synchronizer ( ) . start ( ) ;
await resourceService ( ) . indexNoteResources ( ) ;
await resourceService ( ) . deleteOrphanResources ( 0 ) ;
expect ( await Resource . load ( resource . id ) ) . toBeTruthy ( ) ;
} ) ) ;
2019-04-21 14:49:40 +02:00
2020-12-01 20:05:24 +02:00
// it('should auto-delete resource even if the associated note was deleted immediately', (async () => {
2020-11-08 18:46:48 +02:00
// // Previoulsy, when a resource was be attached to a note, then the
// // note was immediately deleted, the ResourceService would not have
// // time to quick in an index the resource/note relation. It means
// // that when doing the orphan resource deletion job, those
// // resources would permanently stay behing.
// // https://github.com/laurent22/joplin/issues/932
// const service = new ResourceService();
// let note = await Note.save({});
2021-05-21 15:17:21 +02:00
// note = await shim.attachFileToNote(note, `${supportDir}/photo.jpg`);
2020-11-08 18:46:48 +02:00
// const resource = (await Resource.all())[0];
// const noteIds = await NoteResource.associatedNoteIds(resource.id);
// expect(noteIds[0]).toBe(note.id);
// await Note.save({ id: note.id, body: '' });
// await resourceService().indexNoteResources();
// await service.deleteOrphanResources(0);
// expect((await Resource.all()).length).toBe(0);
// }));
2019-07-30 09:35:42 +02:00
} ) ;