mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
All: Created ResourceFetcher class to handle resource downloads
This commit is contained in:
parent
d66fa87b2b
commit
dbdd602f50
@ -7,6 +7,7 @@ const fs = require('fs-extra');
|
|||||||
const Folder = require('lib/models/Folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const Note = require('lib/models/Note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const Resource = require('lib/models/Resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
|
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||||
const Tag = require('lib/models/Tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const Setting = require('lib/models/Setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
@ -872,12 +873,46 @@ describe('Synchronizer', function() {
|
|||||||
let allResources = await Resource.all();
|
let allResources = await Resource.all();
|
||||||
expect(allResources.length).toBe(1);
|
expect(allResources.length).toBe(1);
|
||||||
let resource1_2 = allResources[0];
|
let resource1_2 = allResources[0];
|
||||||
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
|
||||||
|
|
||||||
expect(resource1_2.id).toBe(resource1.id);
|
expect(resource1_2.id).toBe(resource1.id);
|
||||||
|
expect(resource1_2.fetch_status).toBe(Resource.FETCH_STATUS_IDLE);
|
||||||
|
|
||||||
|
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
|
||||||
|
fetcher.queueDownload(resource1_2.id);
|
||||||
|
await fetcher.waitForAllFinished();
|
||||||
|
|
||||||
|
resource1_2 = await Resource.load(resource1.id);
|
||||||
|
expect(resource1_2.fetch_status).toBe(Resource.FETCH_STATUS_DONE);
|
||||||
|
|
||||||
|
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||||
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should handle resource download errors', asyncTest(async () => {
|
||||||
|
while (insideBeforeEach) await time.msleep(500);
|
||||||
|
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||||
|
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||||
|
let resource1 = (await Resource.all())[0];
|
||||||
|
let resourcePath1 = Resource.fullPath(resource1);
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
await switchClient(2);
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
const fetcher = new ResourceFetcher(() => { return {
|
||||||
|
// Simulate a failed download
|
||||||
|
get: () => { return new Promise((resolve, reject) => { reject(new Error('did not work')) }); }
|
||||||
|
} });
|
||||||
|
fetcher.queueResource(resource1.id);
|
||||||
|
await fetcher.waitForAllFinished();
|
||||||
|
|
||||||
|
resource1 = await Resource.load(resource1.id);
|
||||||
|
expect(resource1.fetch_status).toBe(Resource.FETCH_STATUS_ERROR);
|
||||||
|
expect(resource1.fetch_error).toBe('did not work');
|
||||||
|
}));
|
||||||
|
|
||||||
it('should delete resources', asyncTest(async () => {
|
it('should delete resources', asyncTest(async () => {
|
||||||
while (insideBeforeEach) await time.msleep(500);
|
while (insideBeforeEach) await time.msleep(500);
|
||||||
|
|
||||||
@ -926,6 +961,10 @@ describe('Synchronizer', function() {
|
|||||||
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||||
await encryptionService().loadMasterKeysFromSettings();
|
await encryptionService().loadMasterKeysFromSettings();
|
||||||
|
|
||||||
|
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
|
||||||
|
fetcher.queueDownload(resource1.id);
|
||||||
|
await fetcher.waitForAllFinished();
|
||||||
|
|
||||||
let resource1_2 = (await Resource.all())[0];
|
let resource1_2 = (await Resource.all())[0];
|
||||||
resource1_2 = await Resource.decrypt(resource1_2);
|
resource1_2 = await Resource.decrypt(resource1_2);
|
||||||
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||||
|
@ -14,6 +14,10 @@ class BaseSyncTarget {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static resourceDirName() {
|
||||||
|
return '.resource';
|
||||||
|
}
|
||||||
|
|
||||||
option(name, defaultValue = null) {
|
option(name, defaultValue = null) {
|
||||||
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
|
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
|
||||||
}
|
}
|
||||||
|
116
ReactNativeClient/lib/services/ResourceFetcher.js
Normal file
116
ReactNativeClient/lib/services/ResourceFetcher.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
const Resource = require('lib/models/Resource');
|
||||||
|
const BaseService = require('lib/services/BaseService');
|
||||||
|
const BaseSyncTarget = require('lib/BaseSyncTarget');
|
||||||
|
const { Logger } = require('lib/logger.js');
|
||||||
|
|
||||||
|
class ResourceFetcher extends BaseService {
|
||||||
|
|
||||||
|
constructor(fileApi) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (typeof fileApi !== 'function') throw new Error('fileApi must be a function that returns the API');
|
||||||
|
|
||||||
|
this.logger_ = new Logger();
|
||||||
|
this.fileApi_ = fileApi;
|
||||||
|
this.queue_ = [];
|
||||||
|
this.fetchingItems_ = {};
|
||||||
|
this.resourceDirName_ = BaseSyncTarget.resourceDirName();
|
||||||
|
this.queueMutex_ = new Mutex();
|
||||||
|
this.maxDownloads_ = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogger(logger) {
|
||||||
|
this.logger_ = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger() {
|
||||||
|
return this.logger_;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileApi() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueDownload(resourceId, priority = null) {
|
||||||
|
if (priority === null) priority = 'normal';
|
||||||
|
|
||||||
|
const index = this.queuedItemIndex_(resourceId);
|
||||||
|
if (index >= 0) return;
|
||||||
|
|
||||||
|
const item = { id: resourceId };
|
||||||
|
|
||||||
|
if (priority === 'high') {
|
||||||
|
this.queue_.splice(0, 0, item);
|
||||||
|
} else {
|
||||||
|
this.queue_.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleQueueProcess_();
|
||||||
|
}
|
||||||
|
|
||||||
|
async startDownload_(resourceId) {
|
||||||
|
if (this.fetchingItems_[resourceId]) return;
|
||||||
|
this.fetchingItems_[resourceId] = true;
|
||||||
|
|
||||||
|
const resource = await Resource.load(resourceId);
|
||||||
|
|
||||||
|
this.fetchingItems_[resourceId] = resource;
|
||||||
|
|
||||||
|
const localResourceContentPath = Resource.fullPath(resource);
|
||||||
|
const remoteResourceContentPath = this.resourceDirName_ + "/" + resource.id;
|
||||||
|
|
||||||
|
await Resource.save({ id: resource.id, fetch_status: Resource.FETCH_STATUS_STARTED });
|
||||||
|
|
||||||
|
this.fileApi().get(remoteResourceContentPath, { path: localResourceContentPath, target: "file" }).then(async () => {
|
||||||
|
delete this.fetchingItems_[resource.id];
|
||||||
|
await Resource.save({ id: resource.id, fetch_status: Resource.FETCH_STATUS_DONE });
|
||||||
|
this.scheduleQueueProcess_();
|
||||||
|
}).catch(async (error) => {
|
||||||
|
delete this.fetchingItems_[resource.id];
|
||||||
|
await Resource.save({ id: resource.id, fetch_status: Resource.FETCH_STATUS_ERROR, fetch_error: error.message });
|
||||||
|
this.scheduleQueueProcess_();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processQueue_() {
|
||||||
|
while (Object.getOwnPropertyNames(this.fetchingItems_).length < this.maxDownloads_) {
|
||||||
|
if (!this.queue_.length) return;
|
||||||
|
const item = this.queue_.splice(0, 1)[0];
|
||||||
|
this.startDownload_(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForAllFinished() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const iid = setInterval(() => {
|
||||||
|
if (!this.queue_.length && !Object.getOwnPropertyNames(this.fetchingItems_).length) {
|
||||||
|
clearInterval(iid);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleQueueProcess_() {
|
||||||
|
if (this.scheduleQueueProcessIID_) {
|
||||||
|
clearTimeout(this.scheduleQueueProcessIID_);
|
||||||
|
this.scheduleQueueProcessIID_ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleQueueProcessIID_ = setTimeout(() => {
|
||||||
|
this.processQueue_();
|
||||||
|
this.scheduleQueueProcessIID_ = null;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ResourceFetcher;
|
@ -11,6 +11,7 @@ const { Logger } = require('lib/logger.js');
|
|||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { shim } = require('lib/shim.js');
|
const { shim } = require('lib/shim.js');
|
||||||
const JoplinError = require('lib/JoplinError');
|
const JoplinError = require('lib/JoplinError');
|
||||||
|
const BaseSyncTarget = require('lib/BaseSyncTarget');
|
||||||
|
|
||||||
class Synchronizer {
|
class Synchronizer {
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ class Synchronizer {
|
|||||||
this.db_ = db;
|
this.db_ = db;
|
||||||
this.api_ = api;
|
this.api_ = api;
|
||||||
this.syncDirName_ = '.sync';
|
this.syncDirName_ = '.sync';
|
||||||
this.resourceDirName_ = '.resource';
|
this.resourceDirName_ = BaseSyncTarget.resourceDirName();
|
||||||
this.logger_ = new Logger();
|
this.logger_ = new Logger();
|
||||||
this.appType_ = appType;
|
this.appType_ = appType;
|
||||||
this.cancelling_ = false;
|
this.cancelling_ = false;
|
||||||
|
Loading…
Reference in New Issue
Block a user