2021-01-22 19:41:11 +02:00
import Resource from '../models/Resource' ;
import Setting from '../models/Setting' ;
import BaseService from './BaseService' ;
import ResourceService from './ResourceService' ;
2023-07-27 17:05:56 +02:00
import Logger from '@joplin/utils/Logger' ;
2021-01-22 19:41:11 +02:00
import shim from '../shim' ;
2024-01-27 18:59:19 +02:00
import notifyDisabledSyncItems from './synchronizer/utils/checkDisabledSyncItemsNotification' ;
2020-11-05 18:58:23 +02:00
const { Dirnames } = require ( './synchronizer/utils/types' ) ;
2018-10-08 20:11:53 +02:00
const EventEmitter = require ( 'events' ) ;
2018-10-08 08:36:45 +02:00
2021-01-22 19:41:11 +02:00
export default class ResourceFetcher extends BaseService {
public static instance_ : ResourceFetcher ;
2018-11-14 00:25:23 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
2021-01-22 19:41:11 +02:00
public dispatch : Function = ( _o : any ) = > { } ;
private logger_ : Logger = new Logger ( ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
private queue_ : any [ ] = [ ] ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
private fetchingItems_ : any = { } ;
private maxDownloads_ = 3 ;
private addingResources_ = false ;
private eventEmitter_ = new EventEmitter ( ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
private autoAddResourcesCalls_ : any [ ] = [ ] ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
private fileApi_ : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
private updateReportIID_ : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
private scheduleQueueProcessIID_ : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-22 19:41:11 +02:00
private scheduleAutoAddResourcesIID_ : any ;
2018-11-14 00:25:23 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public constructor ( fileApi : any = null ) {
2021-01-22 19:41:11 +02:00
super ( ) ;
2018-10-08 20:11:53 +02:00
this . setFileApi ( fileApi ) ;
}
2023-03-06 16:22:01 +02:00
public static instance() {
2020-02-27 20:25:42 +02:00
if ( ResourceFetcher . instance_ ) return ResourceFetcher . instance_ ;
ResourceFetcher . instance_ = new ResourceFetcher ( ) ;
return ResourceFetcher . instance_ ;
2018-10-08 20:11:53 +02:00
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public on ( eventName : string , callback : Function ) {
2018-10-08 20:11:53 +02:00
return this . eventEmitter_ . on ( eventName , callback ) ;
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public off ( eventName : string , callback : Function ) {
2018-10-08 20:11:53 +02:00
return this . eventEmitter_ . removeListener ( eventName , callback ) ;
2018-10-08 08:36:45 +02:00
}
2023-03-06 16:22:01 +02:00
public setLogger ( logger : Logger ) {
2018-10-08 08:36:45 +02:00
this . logger_ = logger ;
}
2023-03-06 16:22:01 +02:00
public logger() {
2018-10-08 08:36:45 +02:00
return this . logger_ ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public setFileApi ( v : any ) {
2019-09-19 23:51:18 +02:00
if ( v !== null && typeof v !== 'function' ) throw new Error ( ` fileApi must be a function that returns the API. Type is ${ typeof v } ` ) ;
2018-10-08 20:11:53 +02:00
this . fileApi_ = v ;
}
2023-03-06 16:22:01 +02:00
public async fileApi() {
2018-10-08 08:36:45 +02:00
return this . fileApi_ ( ) ;
}
2023-03-06 16:22:01 +02:00
private queuedItemIndex_ ( resourceId : string ) {
2018-10-08 08:36:45 +02:00
for ( let i = 0 ; i < this . fetchingItems_ . length ; i ++ ) {
const item = this . fetchingItems_ [ i ] ;
if ( item . id === resourceId ) return i ;
}
return - 1 ;
}
2023-03-06 16:22:01 +02:00
public updateReport() {
2019-05-22 16:56:07 +02:00
const fetchingCount = Object . keys ( this . fetchingItems_ ) . length ;
this . dispatch ( {
type : 'RESOURCE_FETCHER_SET' ,
fetchingCount : fetchingCount ,
toFetchCount : fetchingCount + this . queue_ . length ,
} ) ;
}
2023-03-06 16:22:01 +02:00
public async markForDownload ( resourceIds : string [ ] ) {
2019-05-22 16:56:07 +02:00
if ( ! Array . isArray ( resourceIds ) ) resourceIds = [ resourceIds ] ;
2019-06-15 22:48:37 +02:00
const fetchStatuses = await Resource . fetchStatuses ( resourceIds ) ;
const idsToKeep = [ ] ;
for ( const status of fetchStatuses ) {
if ( status . fetch_status !== Resource . FETCH_STATUS_IDLE ) continue ;
idsToKeep . push ( status . resource_id ) ;
}
for ( const id of idsToKeep ) {
2019-05-22 16:56:07 +02:00
await Resource . markForDownload ( id ) ;
}
2018-11-14 00:25:23 +02:00
2019-06-15 22:48:37 +02:00
for ( const id of idsToKeep ) {
2019-05-22 16:56:07 +02:00
this . queueDownload_ ( id , 'high' ) ;
}
2018-11-14 00:25:23 +02:00
}
2023-03-06 16:22:01 +02:00
public queueDownload_ ( resourceId : string , priority : string = null ) {
2018-10-08 08:36:45 +02:00
if ( priority === null ) priority = 'normal' ;
const index = this . queuedItemIndex_ ( resourceId ) ;
2018-10-08 20:11:53 +02:00
if ( index >= 0 ) return false ;
2019-05-22 16:56:07 +02:00
if ( this . fetchingItems_ [ resourceId ] ) return false ;
2018-10-08 08:36:45 +02:00
const item = { id : resourceId } ;
if ( priority === 'high' ) {
this . queue_ . splice ( 0 , 0 , item ) ;
} else {
this . queue_ . push ( item ) ;
}
2018-11-14 00:25:23 +02:00
this . updateReport ( ) ;
2018-10-08 20:11:53 +02:00
this . scheduleQueueProcess ( ) ;
return true ;
2018-10-08 08:36:45 +02:00
}
2023-03-06 16:22:01 +02:00
private async startDownload_ ( resourceId : string ) {
2018-10-08 08:36:45 +02:00
if ( this . fetchingItems_ [ resourceId ] ) return ;
this . fetchingItems_ [ resourceId ] = true ;
2019-05-22 16:56:07 +02:00
this . updateReport ( ) ;
2019-05-16 19:34:16 +02:00
const resource = await Resource . load ( resourceId ) ;
const localState = await Resource . localState ( resource ) ;
2019-05-12 12:41:07 +02:00
const completeDownload = async ( emitDownloadComplete = true , localResourceContentPath = '' ) = > {
// 2019-05-12: This is only necessary to set the file size of the resources that come via
// sync. The other ones have been done using migrations/20.js. This code can be removed
// after a few months.
2019-05-16 19:34:16 +02:00
if ( resource && resource . size < 0 && localResourceContentPath && ! resource . encryption_blob_encrypted ) {
2019-05-28 23:05:11 +02:00
await shim . fsDriver ( ) . waitTillExists ( localResourceContentPath ) ;
2019-05-12 16:53:42 +02:00
await ResourceService . autoSetFileSizes ( ) ;
2019-05-12 12:41:07 +02:00
}
2018-10-09 23:01:50 +02:00
delete this . fetchingItems_ [ resource . id ] ;
2019-11-20 20:47:18 +02:00
this . logger ( ) . debug ( ` ResourceFetcher: Removed from fetchingItems: ${ resource . id } . New: ${ JSON . stringify ( this . fetchingItems_ ) } ` ) ;
2018-10-09 23:01:50 +02:00
this . scheduleQueueProcess ( ) ;
2019-05-12 16:53:42 +02:00
// Note: This downloadComplete event is not really right or useful because the resource
// might still be encrypted and the caller usually can't do much with this. In particular
// the note being displayed will refresh the resource images but since they are still
// encrypted it's not useful. Probably, the views should listen to DecryptionWorker events instead.
2019-05-22 16:56:07 +02:00
if ( resource && emitDownloadComplete ) this . eventEmitter_ . emit ( 'downloadComplete' , { id : resource.id , encrypted : ! ! resource . encryption_blob_encrypted } ) ;
2018-11-14 00:25:23 +02:00
this . updateReport ( ) ;
2019-07-29 15:43:53 +02:00
} ;
2018-10-09 23:01:50 +02:00
2019-05-16 19:34:16 +02:00
if ( ! resource ) {
2019-09-19 23:51:18 +02:00
this . logger ( ) . info ( ` ResourceFetcher: Attempting to download a resource that does not exist (has been deleted?): ${ resourceId } ` ) ;
2019-05-16 19:34:16 +02:00
await completeDownload ( false ) ;
return ;
}
2018-10-08 08:36:45 +02:00
2018-10-09 23:01:50 +02:00
// Shouldn't happen, but just to be safe don't re-download the
// resource if it's already been downloaded.
2018-11-13 02:45:08 +02:00
if ( localState . fetch_status === Resource . FETCH_STATUS_DONE ) {
2019-05-12 12:41:07 +02:00
await completeDownload ( false ) ;
2018-10-09 23:01:50 +02:00
return ;
}
2021-08-16 17:18:32 +02:00
const fileApi = await this . fileApi ( ) ;
if ( ! fileApi ) {
this . logger ( ) . debug ( 'ResourceFetcher: Disabled because fileApi is not set' ) ;
return ;
}
2018-10-08 08:36:45 +02:00
this . fetchingItems_ [ resourceId ] = resource ;
2019-05-22 16:56:07 +02:00
const localResourceContentPath = Resource . fullPath ( resource , ! ! resource . encryption_blob_encrypted ) ;
2020-08-02 13:28:50 +02:00
const remoteResourceContentPath = ` ${ Dirnames . Resources } / ${ resource . id } ` ;
2018-10-08 08:36:45 +02:00
2018-11-13 02:45:08 +02:00
await Resource . setLocalState ( resource , { fetch_status : Resource.FETCH_STATUS_STARTED } ) ;
2018-10-08 20:11:53 +02:00
2019-09-19 23:51:18 +02:00
this . logger ( ) . debug ( ` ResourceFetcher: Downloading resource: ${ resource . id } ` ) ;
2018-10-08 20:11:53 +02:00
2019-07-29 15:43:53 +02:00
this . eventEmitter_ . emit ( 'downloadStarted' , { id : resource.id } ) ;
2019-05-22 16:56:07 +02:00
2024-01-14 14:35:20 +02:00
try {
2024-01-14 16:57:29 +02:00
await fileApi . get ( remoteResourceContentPath , { path : localResourceContentPath , target : 'file' } ) ;
if ( ! ( await shim . fsDriver ( ) . exists ( localResourceContentPath ) ) ) throw new Error ( ` Resource not found: ${ resource . id } ` ) ;
2024-01-14 14:35:20 +02:00
await Resource . setLocalState ( resource , { fetch_status : Resource.FETCH_STATUS_DONE } ) ;
this . logger ( ) . debug ( ` ResourceFetcher: Resource downloaded: ${ resource . id } ` ) ;
await completeDownload ( true , localResourceContentPath ) ;
} catch ( error ) {
this . logger ( ) . error ( ` ResourceFetcher: Could not download resource: ${ resource . id } ` , error ) ;
await Resource . setLocalState ( resource , { fetch_status : Resource.FETCH_STATUS_ERROR , fetch_error : error.message } ) ;
await completeDownload ( ) ;
}
2018-10-08 08:36:45 +02:00
}
2023-03-06 16:22:01 +02:00
private processQueue_() {
2018-10-08 08:36:45 +02:00
while ( Object . getOwnPropertyNames ( this . fetchingItems_ ) . length < this . maxDownloads_ ) {
2018-10-08 20:11:53 +02:00
if ( ! this . queue_ . length ) break ;
2018-10-08 08:36:45 +02:00
const item = this . queue_ . splice ( 0 , 1 ) [ 0 ] ;
2021-01-22 19:41:11 +02:00
void this . startDownload_ ( item . id ) ;
2018-10-08 08:36:45 +02:00
}
2018-10-08 20:11:53 +02:00
if ( ! this . queue_ . length ) {
2021-01-22 19:41:11 +02:00
void this . autoAddResources ( 10 ) ;
2018-10-08 20:11:53 +02:00
}
2018-10-08 08:36:45 +02:00
}
2023-03-06 16:22:01 +02:00
public async waitForAllFinished() {
2019-09-13 00:16:42 +02:00
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 . updateReportIID_ &&
! this . scheduleQueueProcessIID_ &&
! this . queue_ . length &&
! this . autoAddResourcesCalls_ . length &&
! Object . getOwnPropertyNames ( this . fetchingItems_ ) . length ) {
2020-10-09 19:35:46 +02:00
shim . clearInterval ( iid ) ;
2021-01-22 19:41:11 +02:00
resolve ( null ) ;
2018-10-08 08:36:45 +02:00
}
} , 100 ) ;
} ) ;
}
2023-03-06 16:22:01 +02:00
public async autoAddResources ( limit : number = null ) {
2020-03-16 04:30:54 +02:00
this . autoAddResourcesCalls_ . push ( true ) ;
try {
if ( limit === null ) limit = 10 ;
2019-05-28 23:05:11 +02:00
2020-03-16 04:30:54 +02:00
if ( this . addingResources_ ) return ;
this . addingResources_ = true ;
2018-10-08 20:11:53 +02:00
2020-03-16 04:30:54 +02:00
this . logger ( ) . info ( ` ResourceFetcher: Auto-add resources: Mode: ${ Setting . value ( 'sync.resourceDownloadMode' ) } ` ) ;
2019-05-22 16:56:07 +02:00
2020-03-16 04:30:54 +02:00
let count = 0 ;
const resources = await Resource . needToBeFetched ( Setting . value ( 'sync.resourceDownloadMode' ) , limit ) ;
for ( let i = 0 ; i < resources . length ; i ++ ) {
const added = this . queueDownload_ ( resources [ i ] . id ) ;
if ( added ) count ++ ;
}
2018-10-08 20:11:53 +02:00
2020-03-16 04:30:54 +02:00
this . logger ( ) . info ( ` ResourceFetcher: Auto-added resources: ${ count } ` ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2024-01-27 18:59:19 +02:00
await notifyDisabledSyncItems ( ( action : any ) = > this . dispatch ( action ) ) ;
2020-03-16 04:30:54 +02:00
} finally {
this . addingResources_ = false ;
this . autoAddResourcesCalls_ . pop ( ) ;
}
2018-10-08 20:11:53 +02:00
}
2023-03-06 16:22:01 +02:00
public async start() {
2019-03-08 19:14:17 +02:00
await Resource . resetStartedFetchStatus ( ) ;
2021-01-22 19:41:11 +02:00
void this . autoAddResources ( 10 ) ;
2018-10-08 20:11:53 +02:00
}
2023-12-13 21:24:58 +02:00
public async startAndWait() {
await this . start ( ) ;
await this . waitForAllFinished ( ) ;
}
2023-03-06 16:22:01 +02:00
public scheduleQueueProcess() {
2018-10-08 08:36:45 +02:00
if ( this . scheduleQueueProcessIID_ ) {
2020-10-09 19:35:46 +02:00
shim . clearTimeout ( this . scheduleQueueProcessIID_ ) ;
2018-10-08 08:36:45 +02:00
this . scheduleQueueProcessIID_ = null ;
}
2020-10-09 19:35:46 +02:00
this . scheduleQueueProcessIID_ = shim . setTimeout ( ( ) = > {
2018-10-08 08:36:45 +02:00
this . processQueue_ ( ) ;
this . scheduleQueueProcessIID_ = null ;
} , 100 ) ;
}
2023-03-06 16:22:01 +02:00
public scheduleAutoAddResources() {
2020-03-16 04:30:54 +02:00
if ( this . scheduleAutoAddResourcesIID_ ) return ;
2020-10-09 19:35:46 +02:00
this . scheduleAutoAddResourcesIID_ = shim . setTimeout ( ( ) = > {
2020-03-16 04:30:54 +02:00
this . scheduleAutoAddResourcesIID_ = null ;
2021-01-22 19:41:11 +02:00
void ResourceFetcher . instance ( ) . autoAddResources ( ) ;
2020-03-16 04:30:54 +02:00
} , 1000 ) ;
}
2023-03-06 16:22:01 +02:00
public async fetchAll() {
2019-03-08 19:14:17 +02:00
await Resource . resetStartedFetchStatus ( ) ;
2021-01-22 19:41:11 +02:00
void this . autoAddResources ( null ) ;
2018-10-08 20:11:53 +02:00
}
2020-02-27 20:25:42 +02:00
2023-03-06 16:22:01 +02:00
public async destroy() {
2020-02-27 20:25:42 +02:00
this . eventEmitter_ . removeAllListeners ( ) ;
if ( this . scheduleQueueProcessIID_ ) {
2020-10-09 19:35:46 +02:00
shim . clearTimeout ( this . scheduleQueueProcessIID_ ) ;
2020-02-27 20:25:42 +02:00
this . scheduleQueueProcessIID_ = null ;
}
2020-03-16 04:30:54 +02:00
if ( this . scheduleAutoAddResourcesIID_ ) {
2020-10-09 19:35:46 +02:00
shim . clearTimeout ( this . scheduleAutoAddResourcesIID_ ) ;
2020-03-16 04:30:54 +02:00
this . scheduleAutoAddResourcesIID_ = null ;
}
await this . waitForAllFinished ( ) ;
2020-02-27 20:25:42 +02:00
this . eventEmitter_ = null ;
ResourceFetcher . instance_ = null ;
}
2018-10-08 08:36:45 +02:00
}