1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00

All: Add support for share permissions (#8491)

This commit is contained in:
Laurent Cozic
2023-07-16 17:42:42 +01:00
committed by GitHub
parent b5193e1174
commit 77482a0c95
90 changed files with 1367 additions and 360 deletions

View File

@@ -26,6 +26,10 @@ import { fetchSyncInfo, getActiveMasterKey, localSyncInfo, mergeSyncInfos, saveL
import { getMasterPassword, setupAndDisableEncryption, setupAndEnableEncryption } from './services/e2ee/utils';
import { generateKeyPair } from './services/e2ee/ppk';
import syncDebugLog from './services/synchronizer/syncDebugLog';
import handleConflictAction, { ConflictAction } from './services/synchronizer/utils/handleConflictAction';
import resourceRemotePath from './services/synchronizer/utils/resourceRemotePath';
import syncDeleteStep from './services/synchronizer/utils/syncDeleteStep';
import { ErrorCode } from './errors';
const { sprintf } = require('sprintf-js');
const { Dirnames } = require('./services/synchronizer/utils/types');
@@ -404,10 +408,6 @@ export default class Synchronizer {
this.dispatch({ type: 'SYNC_HAS_DISABLED_SYNC_ITEMS' });
};
const resourceRemotePath = (resourceId: string) => {
return `${Dirnames.Resources}/${resourceId}`;
};
// We index resources before sync mostly to flag any potential orphan
// resource before it is being synced. That way, it can potentially be
// auto-deleted at a later time. Indexing resources is fast so it's fine
@@ -425,8 +425,20 @@ export default class Synchronizer {
// Before synchronising make sure all share_id properties are set
// correctly so as to share/unshare the right items.
await Folder.updateAllShareIds(this.resourceService());
if (this.shareService_) await this.shareService_.checkShareConsistency();
try {
await Folder.updateAllShareIds(this.resourceService());
if (this.shareService_) await this.shareService_.checkShareConsistency();
} catch (error) {
if (error && error.code === ErrorCode.IsReadOnly) {
// We ignore it because the functions above tried to modify a
// read-only item and failed. Normally it shouldn't happen since
// the UI should prevent, but if there's a bug in the UI or some
// other issue we don't want sync to fail because of this.
logger.error('Could not update share because an item is readonly:', error);
} else {
throw error;
}
}
const itemUploader = new ItemUploader(this.api(), this.apiCall);
@@ -515,22 +527,17 @@ export default class Synchronizer {
// ========================================================================
if (syncSteps.indexOf('delete_remote') >= 0) {
const deletedItems = await BaseItem.deletedItems(syncTargetId);
for (let i = 0; i < deletedItems.length; i++) {
if (this.cancelling()) break;
const item = deletedItems[i];
const path = BaseItem.systemPath(item.item_id);
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.apiCall('delete', path);
if (item.item_type === BaseModel.TYPE_RESOURCE) {
const remoteContentPath = resourceRemotePath(item.item_id);
await this.apiCall('delete', remoteContentPath);
}
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
}
await syncDeleteStep(
syncTargetId,
this.cancelling(),
(action, local, logSyncOperation, message, actionCount) => {
this.logSyncOperation(action, local, logSyncOperation, message, actionCount);
},
(fnName, ...args) => {
return this.apiCall(fnName, ...args);
},
action => { return this.dispatch(action); }
);
} // DELETE_REMOTE STEP
// ========================================================================
@@ -573,7 +580,7 @@ export default class Synchronizer {
const remote: RemoteItem = result.neverSyncedItemIds.includes(local.id) ? null : await this.apiCall('stat', path);
let action = null;
let itemIsReadOnly = false;
let reason = '';
let remoteContent = null;
@@ -698,6 +705,10 @@ export default class Synchronizer {
if (isCannotSyncError(error)) {
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
action = null;
} else if (error && error.code === ErrorCode.IsReadOnly) {
action = getConflictType(local);
itemIsReadOnly = true;
logger.info('Resource is readonly and cannot be modified - handling it as a conflict:', local);
} else {
throw error;
}
@@ -709,11 +720,16 @@ export default class Synchronizer {
let canSync = true;
try {
if (this.testingHooks_.indexOf('notesRejectedByTarget') >= 0 && local.type_ === BaseModel.TYPE_NOTE) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget');
if (this.testingHooks_.indexOf('itemIsReadOnly') >= 0) throw new JoplinError('Testing isReadOnly', ErrorCode.IsReadOnly);
await itemUploader.serializeAndUploadItem(ItemClass, path, local);
} catch (error) {
if (error && error.code === 'rejectedByTarget') {
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
canSync = false;
} else if (error && error.code === ErrorCode.IsReadOnly) {
action = getConflictType(local);
itemIsReadOnly = true;
canSync = false;
} else {
throw error;
}
@@ -741,77 +757,18 @@ export default class Synchronizer {
await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time);
}
} else if (action === 'itemConflict') {
// ------------------------------------------------------------------------------
// For non-note conflicts, we take the remote version (i.e. the version that was
// synced first) and overwrite the local content.
// ------------------------------------------------------------------------------
if (remote) {
local = remoteContent;
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
await ItemClass.save(local, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC, nextQueries: syncTimeQueries });
} else {
await ItemClass.delete(local.id, {
changeSource: ItemChange.SOURCE_SYNC,
trackDeleted: false,
});
}
} else if (action === 'noteConflict') {
// ------------------------------------------------------------------------------
// First find out if the conflict matters. For example, if the conflict is on the title or body
// we want to preserve all the changes. If it's on todo_completed it doesn't really matter
// so in this case we just take the remote content.
// ------------------------------------------------------------------------------
let mustHandleConflict = true;
if (remoteContent) {
mustHandleConflict = Note.mustHandleConflict(local, remoteContent);
}
// ------------------------------------------------------------------------------
// Create a duplicate of local note into Conflicts folder
// (to preserve the user's changes)
// ------------------------------------------------------------------------------
if (mustHandleConflict) {
await Note.createConflictNote(local, ItemChange.SOURCE_SYNC);
}
} else if (action === 'resourceConflict') {
// ------------------------------------------------------------------------------
// Unlike notes we always handle the conflict for resources
// ------------------------------------------------------------------------------
await Resource.createConflictResourceNote(local);
if (remote) {
// 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.
// ------------------------------------------------------------------------------
if (remote) {
local = remoteContent;
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
await ItemClass.save(local, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC, nextQueries: syncTimeQueries });
if (local.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' });
} else {
// Remote no longer exists (note deleted) so delete local one too
await ItemClass.delete(local.id, { changeSource: ItemChange.SOURCE_SYNC, trackDeleted: false });
}
}
await handleConflictAction(
action as ConflictAction,
ItemClass,
!!remote,
remoteContent,
local,
syncTargetId,
itemIsReadOnly,
(action: any) => this.dispatch(action)
);
completeItemProcessing(path);
}