fix(files_sharing): Drop permissions on unmounted pending and deleted shares

Pending and deleted shares are not mounted into the user's filesystem, so
generic file operations like delete or download produced a misleading
"file is not available" error.

These shares now carry no permissions, so every permission-aware action
hides itself automatically, without the files app having to special-case
each view. Conversion additionally requires read permission, matching the
server-side readability check.

Signed-off-by: nfebe <fenn25.fn@gmail.com>
This commit is contained in:
nfebe 2026-05-05 11:05:41 +01:00 committed by backportbot[bot]
parent 00eb72c361
commit bf5178d979
3 changed files with 50 additions and 9 deletions

View file

@ -36,6 +36,10 @@ export function registerConvertActions() {
// cannot create the converted file in a public share if we don't have create permissions
return false
}
// Conversion reads the source file, so it requires read permission
if (nodes.some((node) => (node.permissions & Permission.READ) === 0)) {
return false
}
// Check that all nodes have the same mime type
return nodes.every((node) => from === node.mime)
},

View file

@ -535,6 +535,34 @@ describe('SharingService share to Node mapping', () => {
expect(file.attributes.favorite).toBe(0)
})
test('Pending share has no permissions', async () => {
axios.get
.mockReturnValueOnce(Promise.resolve({
data: { ocs: { data: [shareFile] } },
}))
.mockReturnValueOnce(Promise.resolve({
data: { ocs: { data: [] } },
}))
const shares = await getContents(false, false, true, false)
expect(axios.get).toHaveBeenCalledTimes(2)
expect(shares.contents).toHaveLength(1)
expect(shares.contents[0].permissions).toBe(0)
})
test('Deleted share has no permissions', async () => {
axios.get.mockReturnValueOnce(Promise.resolve({
data: { ocs: { data: [shareFolder] } },
}))
const shares = await getContents(false, false, false, true)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(shares.contents).toHaveLength(1)
expect(shares.contents[0].permissions).toBe(0)
})
test('Empty', async () => {
vi.spyOn(logger, 'error').mockImplementationOnce(() => {})
axios.get.mockReturnValueOnce(Promise.resolve({

View file

@ -24,8 +24,9 @@ const headers = {
/**
*
* @param ocsEntry
* @param unmounted whether the share is not mounted into the filesystem (pending or deleted)
*/
async function ocsEntryToNode(ocsEntry: any): Promise<Folder | File | null> {
async function ocsEntryToNode(ocsEntry: any, unmounted = false): Promise<Folder | File | null> {
try {
// Federated share handling
if (ocsEntry?.remote_id !== undefined) {
@ -57,6 +58,13 @@ async function ocsEntryToNode(ocsEntry: any): Promise<Folder | File | null> {
ocsEntry.displayname_owner = ocsEntry.owner
}
// Pending and deleted shares are not mounted into the user's filesystem,
// so no file operation can act on them until they are accepted or restored.
if (unmounted) {
ocsEntry.item_permissions = Permission.NONE
ocsEntry.permissions = Permission.NONE
}
const isFolder = ocsEntry?.item_type === 'folder'
const hasPreview = ocsEntry?.has_preview === true
const Node = isFolder ? Folder : File
@ -238,24 +246,25 @@ function groupBy(nodes: (Folder | File)[], key: string) {
* @param filterTypes
*/
export async function getContents(sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> {
const promises = [] as AxiosPromise<OCSResponse<any>>[]
const requests = [] as { promise: AxiosPromise<OCSResponse<any>>, unmounted: boolean }[]
if (sharedWithYou) {
promises.push(getSharedWithYou(), getRemoteShares())
requests.push({ promise: getSharedWithYou(), unmounted: false }, { promise: getRemoteShares(), unmounted: false })
}
if (sharedWithOthers) {
promises.push(getSharedWithOthers())
requests.push({ promise: getSharedWithOthers(), unmounted: false })
}
if (pendingShares) {
promises.push(getPendingShares(), getRemotePendingShares())
requests.push({ promise: getPendingShares(), unmounted: true }, { promise: getRemotePendingShares(), unmounted: true })
}
if (deletedshares) {
promises.push(getDeletedShares())
requests.push({ promise: getDeletedShares(), unmounted: true })
}
const responses = await Promise.all(promises)
const data = responses.map((response) => response.data.ocs.data).flat()
let contents = (await Promise.all(data.map(ocsEntryToNode)))
const responses = await Promise.all(requests.map(({ promise }) => promise))
const data = responses.flatMap((response, index) => response.data.ocs.data
.map((entry) => ({ entry, unmounted: requests[index].unmounted })))
let contents = (await Promise.all(data.map(({ entry, unmounted }) => ocsEntryToNode(entry, unmounted))))
.filter((node) => node !== null) as (Folder | File)[]
if (filterTypes.length > 0) {