2018-10-08 08:36:45 +02:00
|
|
|
const Resource = require('lib/models/Resource');
|
2019-05-22 16:56:07 +02:00
|
|
|
const Setting = require('lib/models/Setting');
|
2018-10-08 08:36:45 +02:00
|
|
|
const BaseService = require('lib/services/BaseService');
|
2019-05-12 16:53:42 +02:00
|
|
|
const ResourceService = require('lib/services/ResourceService');
|
2018-10-08 08:36:45 +02:00
|
|
|
const BaseSyncTarget = require('lib/BaseSyncTarget');
|
|
|
|
const { Logger } = require('lib/logger.js');
|
2018-10-08 20:11:53 +02:00
|
|
|
const EventEmitter = require('events');
|
2019-05-12 12:41:07 +02:00
|
|
|
const { shim } = require('lib/shim');
|
2018-10-08 08:36:45 +02:00
|
|
|
|
|
|
|
class ResourceFetcher extends BaseService {
|
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
constructor(fileApi = null) {
|
2018-10-08 08:36:45 +02:00
|
|
|
super();
|
2018-11-14 00:25:23 +02:00
|
|
|
|
|
|
|
this.dispatch = (action) => {};
|
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
this.setFileApi(fileApi);
|
2018-10-08 08:36:45 +02:00
|
|
|
this.logger_ = new Logger();
|
|
|
|
this.queue_ = [];
|
|
|
|
this.fetchingItems_ = {};
|
|
|
|
this.resourceDirName_ = BaseSyncTarget.resourceDirName();
|
|
|
|
this.maxDownloads_ = 3;
|
2018-10-08 20:11:53 +02:00
|
|
|
this.addingResources_ = false;
|
|
|
|
this.eventEmitter_ = new EventEmitter();
|
|
|
|
}
|
|
|
|
|
|
|
|
static instance() {
|
|
|
|
if (this.instance_) return this.instance_;
|
|
|
|
this.instance_ = new ResourceFetcher();
|
|
|
|
return this.instance_;
|
|
|
|
}
|
|
|
|
|
|
|
|
on(eventName, callback) {
|
|
|
|
return this.eventEmitter_.on(eventName, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
off(eventName, callback) {
|
|
|
|
return this.eventEmitter_.removeListener(eventName, callback);
|
2018-10-08 08:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setLogger(logger) {
|
|
|
|
this.logger_ = logger;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger() {
|
|
|
|
return this.logger_;
|
|
|
|
}
|
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
setFileApi(v) {
|
|
|
|
if (v !== null && typeof v !== 'function') throw new Error('fileApi must be a function that returns the API. Type is ' + (typeof v));
|
|
|
|
this.fileApi_ = v;
|
|
|
|
}
|
|
|
|
|
|
|
|
async fileApi() {
|
2018-10-08 08:36:45 +02:00
|
|
|
return this.fileApi_();
|
|
|
|
}
|
|
|
|
|
|
|
|
queuedItemIndex_(resourceId) {
|
|
|
|
for (let i = 0; i < this.fetchingItems_.length; i++) {
|
|
|
|
const item = this.fetchingItems_[i];
|
|
|
|
if (item.id === resourceId) return i;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2018-11-14 00:25:23 +02:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async markForDownload(resourceIds) {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2019-05-22 16:56:07 +02:00
|
|
|
queueDownload_(resourceId, priority = 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
|
|
|
}
|
|
|
|
|
|
|
|
async startDownload_(resourceId) {
|
|
|
|
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];
|
|
|
|
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();
|
2018-10-09 23:01:50 +02:00
|
|
|
}
|
|
|
|
|
2019-05-16 19:34:16 +02:00
|
|
|
if (!resource) {
|
|
|
|
this.logger().info('ResourceFetcher: Attempting to download a resource that does not exist (has been deleted?): ' + resourceId);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
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);
|
2018-10-08 08:36:45 +02:00
|
|
|
const remoteResourceContentPath = this.resourceDirName_ + "/" + resource.id;
|
|
|
|
|
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
|
|
|
|
|
|
|
const fileApi = await this.fileApi();
|
2018-10-08 08:36:45 +02:00
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
this.logger().debug('ResourceFetcher: Downloading resource: ' + resource.id);
|
|
|
|
|
2019-05-22 16:56:07 +02:00
|
|
|
this.eventEmitter_.emit('downloadStarted', { id: resource.id })
|
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
fileApi.get(remoteResourceContentPath, { path: localResourceContentPath, target: "file" }).then(async () => {
|
2018-11-13 02:45:08 +02:00
|
|
|
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_DONE });
|
2018-10-08 20:11:53 +02:00
|
|
|
this.logger().debug('ResourceFetcher: Resource downloaded: ' + resource.id);
|
2019-05-12 12:41:07 +02:00
|
|
|
await completeDownload(true, localResourceContentPath);
|
2018-10-08 08:36:45 +02:00
|
|
|
}).catch(async (error) => {
|
2018-10-08 20:11:53 +02:00
|
|
|
this.logger().error('ResourceFetcher: Could not download resource: ' + resource.id, error);
|
2018-11-13 02:45:08 +02:00
|
|
|
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_ERROR, fetch_error: error.message });
|
2019-05-12 12:41:07 +02:00
|
|
|
await completeDownload();
|
2018-10-08 08:36:45 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
processQueue_() {
|
|
|
|
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];
|
|
|
|
this.startDownload_(item.id);
|
|
|
|
}
|
2018-10-08 20:11:53 +02:00
|
|
|
|
|
|
|
if (!this.queue_.length) {
|
|
|
|
this.autoAddResources(10);
|
|
|
|
}
|
2018-10-08 08:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async waitForAllFinished() {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const iid = setInterval(() => {
|
2019-05-16 19:34:16 +02:00
|
|
|
if (!this.updateReportIID_ && !this.scheduleQueueProcessIID_ && !this.addingResources_ && !this.queue_.length && !Object.getOwnPropertyNames(this.fetchingItems_).length) {
|
2018-10-08 08:36:45 +02:00
|
|
|
clearInterval(iid);
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
}, 100);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-05-28 23:05:11 +02:00
|
|
|
async autoAddResources(limit = null) {
|
|
|
|
if (limit === null) limit = 10;
|
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
if (this.addingResources_) return;
|
|
|
|
this.addingResources_ = true;
|
|
|
|
|
2019-05-22 16:56:07 +02:00
|
|
|
this.logger().info('ResourceFetcher: Auto-add resources: Mode: ' + Setting.value('sync.resourceDownloadMode'));
|
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
let count = 0;
|
2019-05-22 16:56:07 +02:00
|
|
|
const resources = await Resource.needToBeFetched(Setting.value('sync.resourceDownloadMode'), limit);
|
2018-10-08 20:11:53 +02:00
|
|
|
for (let i = 0; i < resources.length; i++) {
|
2019-05-22 16:56:07 +02:00
|
|
|
const added = this.queueDownload_(resources[i].id);
|
2018-10-08 20:11:53 +02:00
|
|
|
if (added) count++;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logger().info('ResourceFetcher: Auto-added resources: ' + count);
|
|
|
|
this.addingResources_ = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
async start() {
|
2019-03-08 19:14:17 +02:00
|
|
|
await Resource.resetStartedFetchStatus();
|
2018-10-08 20:11:53 +02:00
|
|
|
this.autoAddResources(10);
|
|
|
|
}
|
|
|
|
|
|
|
|
scheduleQueueProcess() {
|
2018-10-08 08:36:45 +02:00
|
|
|
if (this.scheduleQueueProcessIID_) {
|
|
|
|
clearTimeout(this.scheduleQueueProcessIID_);
|
|
|
|
this.scheduleQueueProcessIID_ = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.scheduleQueueProcessIID_ = setTimeout(() => {
|
|
|
|
this.processQueue_();
|
|
|
|
this.scheduleQueueProcessIID_ = null;
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
|
2018-10-08 20:11:53 +02:00
|
|
|
async fetchAll() {
|
2019-03-08 19:14:17 +02:00
|
|
|
await Resource.resetStartedFetchStatus();
|
2018-10-08 20:11:53 +02:00
|
|
|
this.autoAddResources(null);
|
|
|
|
}
|
|
|
|
|
2018-10-08 08:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = ResourceFetcher;
|