diff --git a/packages/server/src/models/ShareModel.test.ts b/packages/server/src/models/ShareModel.test.ts index f3d8eb855..45559a588 100644 --- a/packages/server/src/models/ShareModel.test.ts +++ b/packages/server/src/models/ShareModel.test.ts @@ -69,4 +69,19 @@ describe('ShareModel', function() { expect(shares3.find(s => s.folder_id === '000000000000000000000000000000F1')).toBeTruthy(); }); + test('should generate only one link per shared note', async function() { + const { user: user1 } = await createUserAndSession(1); + + await createItemTree(user1.id, '', { + '000000000000000000000000000000F1': { + '00000000000000000000000000000001': null, + }, + }); + + const share1 = await models().share().shareNote(user1, '00000000000000000000000000000001'); + const share2 = await models().share().shareNote(user1, '00000000000000000000000000000001'); + + expect(share1.id).toBe(share2.id); + }); + }); diff --git a/packages/server/src/models/ShareModel.ts b/packages/server/src/models/ShareModel.ts index ea372ba8e..d640e8b98 100644 --- a/packages/server/src/models/ShareModel.ts +++ b/packages/server/src/models/ShareModel.ts @@ -241,295 +241,6 @@ export default class ShareModel extends BaseModel { } } - // public async updateSharedItems2(userId: Uuid) { - // const shares = await this.models().share().byUserId(userId, ShareType.JoplinRootFolder); - // if (!shares.length) return; - - // const existingShareUserItems: UserItem[] = await this.models().userItem().itemsInShare(userId); - - // const allShareUserItems: UserItem[] = []; - - // for (const share of shares) { - // allShareUserItems.push({ - // item_id: share.item_id, - // share_id: share.id, - // }); - - // const shareUserIds = await this.models().share().allShareUserIds(share); - // const shareItems = await this.models().item().sharedFolderChildrenItems(shareUserIds, share.folder_id); - // for (const item of shareItems) { - // allShareUserItems.push({ - // item_id: item.id, - // share_id: share.id, - // }); - // } - // } - - // const userItemsToDelete: UserItem[] = []; - // for (const userItem of existingShareUserItems) { - // if (!allShareUserItems.find(ui => ui.item_id === userItem.item_id && ui.share_id === userItem.share_id)) { - // userItemsToDelete.push(userItem); - // } - // } - - // const userItemsToCreate: UserItem[] = []; - // for (const userItem of allShareUserItems) { - // if (!existingShareUserItems.find(ui => ui.item_id === userItem.item_id && ui.share_id === userItem.share_id)) { - // userItemsToCreate.push(userItem); - // } - // } - - // await this.withTransaction(async () => { - // await this.models().userItem().deleteByUserItemIds(userItemsToDelete.map(ui => ui.id)); - - // for (const userItem of userItemsToCreate) { - // await this.models().userItem().add(userId, userItem.item_id, userItem.share_id); - // } - // }); - // } - - // public async updateSharedItems() { - // enum ResourceChangeAction { - // Added = 1, - // Removed = 2, - // } - - // interface ResourceChange { - // resourceIds: string[]; - // share: Share; - // change: Change; - // action: ResourceChangeAction; - // } - - // let resourceChanges: ResourceChange[] = []; - - // const getSharedRootInfo = async (jopId: string) => { - // try { - // const output = await this.models().item().joplinItemSharedRootInfo(jopId); - // return output; - // } catch (error) { - // // "noPathForItem" means that the note or folder doesn't have a - // // parent yet. It can happen because items are synchronized in - // // random order, sometimes the children before the parents. In - // // that case we simply ignore the error, which means that for - // // now the share status of the item is unknown. The situation - // // will be resolved when the parent is received because in this - // // case, if the folder is within a shared folder, all its - // // children will be shared. - // if (error.code === 'noPathForItem') return null; - // throw error; - // } - // }; - - // const handleAddedToSharedFolder = async (item: Item, shareInfo: SharedRootInfo) => { - // const userIds = await this.allShareUserIds(shareInfo.share); - - // for (const userId of userIds) { - // // If it's a folder we share its content. This is to ensure - // // that children that were synced before their parents get their - // // share status updated. - // // if (item.jop_type === ModelType.Folder) { - // // await this.models().item().shareJoplinFolderAndContent(shareInfo.share.id, shareInfo.share.owner_id, userId, item.jop_id); - // // } - - // try { - // await this.models().userItem().add(userId, item.id); - // } catch (error) { - // if (isUniqueConstraintError(error)) { - // // Ignore - it means this user already has this item - // } else { - // throw error; - // } - // } - // } - // }; - - // const handleRemovedFromSharedFolder = async (change: Change, item: Item, shareInfo: SharedRootInfo) => { - // // This is called when a note parent ID changes and is moved out of - // // the shared folder. In that case, we need to unshare the item from - // // all users, except the one who did the action. - // // - // // - User 1 shares a folder with user 2 - // // - User 2 moves a note out of the shared folder - // // - User 1 should no longer see the note. User 2 still sees it - // // since they have moved it to one of their own folders. - - // const userIds = await this.allShareUserIds(shareInfo.share); - - // for (const userId of userIds) { - // if (change.user_id !== userId) { - // await this.models().userItem().remove(userId, item.id); - // } - // } - // }; - - // const handleResourceSharing = async (change: Change, previousItem: ChangePreviousItem, item: Item, previousShareInfo: SharedRootInfo, currentShareInfo: SharedRootInfo) => { - // // Not a note - we can exit - // if (item.jop_type !== ModelType.Note) return; - - // // Item was not in a shared folder and is still not in one - nothing to do - // if (!previousShareInfo && !currentShareInfo) return; - - // // Item was moved out of a shared folder to a non-shared folder - unshare all resources - // if (previousShareInfo && !currentShareInfo) { - // resourceChanges.push({ - // action: ResourceChangeAction.Removed, - // change, - // share: previousShareInfo.share, - // resourceIds: await this.models().itemResource().byItemId(item.id), - // }); - // return; - // } - - // // Item was moved from a non-shared folder to a shared one - share all resources - // if (!previousShareInfo && currentShareInfo) { - // resourceChanges.push({ - // action: ResourceChangeAction.Added, - // change, - // share: currentShareInfo.share, - // resourceIds: await this.models().itemResource().byItemId(item.id), - // }); - // return; - // } - - // // Note either stayed in the same shared folder, or moved to another - // // shared folder. In that case, we check the note content before and - // // after and see if resources have been added or removed from it, - // // then we share/unshare resources based on this. - - // const previousResourceIds = previousItem ? previousItem.jop_resource_ids : []; - // const currentResourceIds = await this.models().itemResource().byItemId(item.id); - // for (const resourceId of previousResourceIds) { - // if (!currentResourceIds.includes(resourceId)) { - // resourceChanges.push({ - // action: ResourceChangeAction.Removed, - // change, - // share: currentShareInfo.share, - // resourceIds: [resourceId], - // }); - // } - // } - - // for (const resourceId of currentResourceIds) { - // if (!previousResourceIds.includes(resourceId)) { - // resourceChanges.push({ - // action: ResourceChangeAction.Added, - // change, - // share: currentShareInfo.share, - // resourceIds: [resourceId], - // }); - // } - // } - // }; - - // const handleCreatedItem = async (change: Change, item: Item) => { - // if (!item.jop_parent_id) return; - // const shareInfo = await getSharedRootInfo(item.jop_parent_id); - - // if (!shareInfo) return; - - // await handleResourceSharing(change, null, item, null, shareInfo); - // await handleAddedToSharedFolder(item, shareInfo); - // }; - - // const handleUpdatedItem = async (change: Change, item: Item) => { - // if (![ModelType.Note, ModelType.Folder].includes(item.jop_type)) return; - - // const previousItem = this.models().change().unserializePreviousItem(change.previous_item); - - // const previousShareInfo = previousItem?.jop_parent_id ? await getSharedRootInfo(previousItem.jop_parent_id) : null; - // const currentShareInfo = item.jop_parent_id ? await getSharedRootInfo(item.jop_parent_id) : null; - - // await handleResourceSharing(change, previousItem, item, previousShareInfo, currentShareInfo); - - // // Item was not in a shared folder and is still not in one - // if (!previousShareInfo && !currentShareInfo) return; - - // // Item was in a shared folder and is still in the same shared folder - // if (previousShareInfo && currentShareInfo && previousShareInfo.item.jop_parent_id === currentShareInfo.item.jop_parent_id) return; - - // // Item was not previously in a shared folder but has been moved to one - // if (!previousShareInfo && currentShareInfo) { - // await handleAddedToSharedFolder(item, currentShareInfo); - // return; - // } - - // // Item was in a shared folder and is no longer in one - // if (previousShareInfo && !currentShareInfo) { - // await handleRemovedFromSharedFolder(change, item, previousShareInfo); - // return; - // } - - // // Item was in a shared folder and has been moved to a different shared folder - // if (previousShareInfo && currentShareInfo && previousShareInfo.item.jop_parent_id !== currentShareInfo.item.jop_parent_id) { - // await handleRemovedFromSharedFolder(change, item, previousShareInfo); - // await handleAddedToSharedFolder(item, currentShareInfo); - // return; - // } - - // // Sanity check - because normally all cases are covered above - // throw new Error('Unreachable'); - // }; - - // // This loop essentially applies the change made by one user to all the - // // other users in the share. - // // - // // While it's processing changes, it's going to create new user_item - // // objects, which in turn generate more Change items, which are processed - // // again. However there are guards to ensure that it doesn't result in - // // an infinite loop - in particular once a user_item has been added, - // // adding it again will result in a UNIQUE constraint error and thus it - // // won't generate a Change object the second time. - // // - // // Rather than checking if the user_item exists before creating it, we - // // create it directly and let it fail, while catching the Unique error. - // // This is probably safer in terms of avoiding race conditions and - // // possibly faster. - - // while (true) { - // const latestProcessedChange = await this.models().keyValue().value('ShareService::latestProcessedChange'); - - // const changes = await this.models().change().allFromId(latestProcessedChange || ''); - // if (!changes.length) break; - - // const items = await this.models().item().loadByIds(changes.map(c => c.item_id)); - - // await this.withTransaction(async () => { - // for (const change of changes) { - // if (change.type === ChangeType.Create) { - // await handleCreatedItem(change, items.find(i => i.id === change.item_id)); - // } - - // if (change.type === ChangeType.Update) { - // await handleUpdatedItem(change, items.find(i => i.id === change.item_id)); - // } - - // // We don't need to handle ChangeType.Delete because when an - // // item is deleted, all its associated userItems are deleted - // // too. - // } - - // for (const rc of resourceChanges) { - // const shareUserIds = await this.allShareUserIds(rc.share); - // const doShare = rc.action === ResourceChangeAction.Added; - // const changerUserId = rc.change.user_id; - - // for (const shareUserId of shareUserIds) { - // // We apply the updates to all the users, except the one - // // who made the change, since they already have the - // // change. - // if (shareUserId === changerUserId) continue; - // await this.updateResourceShareStatus(doShare, rc.share.id, changerUserId, shareUserId, rc.resourceIds); - // } - // } - - // resourceChanges = []; - - // await this.models().keyValue().setValue('ShareService::latestProcessedChange', changes[changes.length - 1].id); - // }); - // } - // } - public async updateResourceShareStatus(doShare: boolean, _shareId: Uuid, changerUserId: Uuid, toUserId: Uuid, resourceIds: string[]) { const resourceItems = await this.models().item().loadByJopIds(changerUserId, resourceIds); const resourceBlobNames = resourceIds.map(id => resourceBlobPath(id)); @@ -595,30 +306,16 @@ export default class ShareModel extends BaseModel { }; await this.checkIfAllowed(owner, AclAction.Create, shareToSave); - - // const shareItems = await this.models().item().sharedFolderChildrenItems([owner.id], folderId); - // const userItems = await this.models().userItem().byItemIds(shareItems.map(s => s.id)); - // const userItemIds = userItems.map(u => u.id); - return super.save(shareToSave); - - // return this.withTransaction(async () => { - // const savedShare = await super.save(shareToSave); - - // await this - // .db('user_items') - // .whereIn('id', userItemIds) - // .orWhere('item_id', '=', shareToSave.item_id) - // .update({ share_id: savedShare.id }); - - // return savedShare; - // }); } public async shareNote(owner: User, noteId: string): Promise { const noteItem = await this.models().item().loadByJopId(owner.id, noteId); if (!noteItem) throw new ErrorNotFound(`No such note: ${noteId}`); + const existingShare = await this.byItemId(noteItem.id); + if (existingShare) return existingShare; + const shareToSave = { type: ShareType.Link, item_id: noteItem.id,