You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-06 09:19:22 +02:00
Handle resource conflicts
This commit is contained in:
@@ -122,6 +122,11 @@ class Resource extends BaseItem {
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
static async requireIsReady(resource) {
|
||||
const readyStatus = await Resource.readyStatus(resource);
|
||||
if (readyStatus !== 'ok') throw new Error(`Resource is not ready. Status: ${readyStatus}`);
|
||||
}
|
||||
|
||||
// For resources, we need to decrypt the item (metadata) and the resource binary blob.
|
||||
static async decrypt(item) {
|
||||
// The item might already be decrypted but not the blob (for instance if it crashes while
|
||||
@@ -236,7 +241,7 @@ class Resource extends BaseItem {
|
||||
return url.substr(2);
|
||||
}
|
||||
|
||||
static localState(resourceOrId) {
|
||||
static async localState(resourceOrId) {
|
||||
return ResourceLocalState.byResourceId(typeof resourceOrId === 'object' ? resourceOrId.id : resourceOrId);
|
||||
}
|
||||
|
||||
@@ -315,6 +320,25 @@ class Resource extends BaseItem {
|
||||
throw new Error(`Invalid status: ${status}`);
|
||||
}
|
||||
|
||||
static async updateResourceBlobContent(resourceId, newBlobFilePath) {
|
||||
const resource = await Resource.load(resourceId);
|
||||
await this.requireIsReady(resource);
|
||||
|
||||
const fileStat = await this.fsDriver().stat(newBlobFilePath);
|
||||
await this.fsDriver().copy(newBlobFilePath, Resource.fullPath(resource));
|
||||
|
||||
await Resource.save({
|
||||
id: resource.id,
|
||||
size: fileStat.size,
|
||||
});
|
||||
}
|
||||
|
||||
static async resourceBlobContent(resourceId, encoding = 'Buffer') {
|
||||
const resource = await Resource.load(resourceId);
|
||||
await this.requireIsReady(resource);
|
||||
return await this.fsDriver().readFile(Resource.fullPath(resource), encoding);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Resource.IMAGE_MAX_DIMENSION = 1920;
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class ResourceEditWatcher {
|
||||
const makeSaveAction = (resourceId:string, path:string) => {
|
||||
return async () => {
|
||||
this.logger().info(`ResourceEditWatcher: Saving resource ${resourceId}`);
|
||||
await shim.updateResourceBlob(resourceId, path);
|
||||
await Resource.updateResourceBlob(resourceId, path);
|
||||
this.eventEmitter_.emit('resourceChange', { id: resourceId });
|
||||
};
|
||||
};
|
||||
|
||||
@@ -205,18 +205,26 @@ function shimInit() {
|
||||
return Resource.save(resource, { isNew: true });
|
||||
};
|
||||
|
||||
shim.updateResourceBlob = async function(resourceId, newBlobFilePath) {
|
||||
shim.duplicateResource = async function(resourceId) {
|
||||
const resource = await Resource.load(resourceId);
|
||||
const readyStatus = await Resource.readyStatus(resourceId);
|
||||
if (readyStatus !== 'ok') throw new Error(`Cannot set resource blob because resource is not ready. Status: ${readyStatus}`);
|
||||
const localState = await Resource.localState(resource);
|
||||
|
||||
const fileStat = await shim.fsDriver().stat(newBlobFilePath);
|
||||
await shim.fsDriver().copy(newBlobFilePath, Resource.fullPath(resource));
|
||||
let newResource = { ...resource };
|
||||
delete newResource.id;
|
||||
newResource = await Resource.save(newResource);
|
||||
|
||||
await Resource.save({
|
||||
id: resource.id,
|
||||
size: fileStat.size,
|
||||
});
|
||||
const newLocalState = { ...localState };
|
||||
newLocalState.resource_id = newResource.id;
|
||||
delete newLocalState.id;
|
||||
|
||||
await Resource.setLocalState(newResource, newLocalState);
|
||||
|
||||
const sourcePath = Resource.fullPath(resource);
|
||||
if (await shim.fsDriver().exists(sourcePath)) {
|
||||
await shim.fsDriver().copy(sourcePath, Resource.fullPath(newResource));
|
||||
}
|
||||
|
||||
return newResource;
|
||||
};
|
||||
|
||||
shim.attachFileToNoteBody = async function(noteBody, filePath, position = null, options = null) {
|
||||
|
||||
@@ -372,6 +372,12 @@ class Synchronizer {
|
||||
let reason = '';
|
||||
let remoteContent = null;
|
||||
|
||||
const getConflictType = (conflictedItem) => {
|
||||
if (conflictedItem.type_ === BaseModel.TYPE_NOTE) return 'noteConflict';
|
||||
if (conflictedItem.type_ === BaseModel.TYPE_RESOURCE) return 'resourceConflict';
|
||||
return 'itemConflict';
|
||||
};
|
||||
|
||||
if (!remote) {
|
||||
if (!local.sync_time) {
|
||||
action = 'createRemote';
|
||||
@@ -379,7 +385,7 @@ class Synchronizer {
|
||||
} else {
|
||||
// Note or item was modified after having been deleted remotely
|
||||
// "itemConflict" is for all the items except the notes, which are dealt with in a special way
|
||||
action = local.type_ == BaseModel.TYPE_NOTE ? 'noteConflict' : 'itemConflict';
|
||||
action = getConflictType(local);
|
||||
reason = 'remote has been deleted, but local has changes';
|
||||
}
|
||||
} else {
|
||||
@@ -416,7 +422,7 @@ class Synchronizer {
|
||||
// Since, in this loop, we are only dealing with items that require sync, if the
|
||||
// remote has been modified after the sync time, it means both items have been
|
||||
// modified and so there's a conflict.
|
||||
action = local.type_ == BaseModel.TYPE_NOTE ? 'noteConflict' : 'itemConflict';
|
||||
action = getConflictType(local);
|
||||
reason = 'both remote and local have changes';
|
||||
} else {
|
||||
action = 'updateRemote';
|
||||
@@ -528,8 +534,29 @@ class Synchronizer {
|
||||
conflictedNote.is_conflict = 1;
|
||||
await Note.save(conflictedNote, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC });
|
||||
}
|
||||
|
||||
} else if (action == 'resourceConflict') {
|
||||
// ------------------------------------------------------------------------------
|
||||
// Unlike notes we always handle the conflict for resources
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const conflictResource = await shim.duplicateResource(local.id);
|
||||
|
||||
await Note.save({
|
||||
title: _('Attachment conflict: "%s"', local.title),
|
||||
body: _('There was a [conflict](%s) on the attachment below.\n\n%s', 'https://joplinapp.org/conflict', Resource.markdownTag(conflictResource)),
|
||||
is_conflict: 1,
|
||||
}, { changeSource: ItemChange.SOURCE_SYNC });
|
||||
|
||||
// The local content we have is no longer valid and should be re-downloaded
|
||||
await Resource.setLocalState(local.id, {
|
||||
fetch_status: Resource.FETCH_STATUS_IDLE,
|
||||
});
|
||||
}
|
||||
|
||||
if (['noteConflict', 'resourceConflict'].includes(action)) {
|
||||
// ------------------------------------------------------------------------------
|
||||
// For note and resource conflicts, the creation of the conflict item is done
|
||||
// differently. However the way the local content is handled is the same.
|
||||
// Either copy the remote content to local or, if the remote content has
|
||||
// been deleted, delete the local content.
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user