Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/web-app-files/src/HandleUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export class HandleUpload extends BasePlugin {
if (
!targetUploadSpace ||
isShareSpaceResource(targetUploadSpace) ||
targetUploadSpace.driveType === 'explorer' ||
(isPersonalSpaceResource(targetUploadSpace) &&
!targetUploadSpace.isOwner(this.userStore.user))
) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web-app-files/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ export const navItems = (context: ComponentCustomProperties): AppNavigationItem[
route: {
path: `/${appInfo.id}/spaces/projects`
},
isActive: () => {
// the eos explorer space's driveAlias ('eos') is a url-prefix of real eos-backed
// personal/project driveAliases (`eos/user/...`, `eos/project/...`), so activeFor's
// href-prefix match below would otherwise also fire while browsing those unrelated
// spaces. Gate on the actually resolved current space first.
const currentSpace = spacesStores.currentSpace
return (
!currentSpace ||
isProjectSpaceResource(currentSpace) ||
currentSpace.driveType === 'explorer'
)
},
activeFor: () => {
const projects = [{ path: `/${appInfo.id}/spaces/project` }]
spacesStores.spaces.forEach((drive) => {
Expand Down
8 changes: 4 additions & 4 deletions packages/web-app-files/src/views/spaces/Projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -471,20 +471,20 @@ export default defineComponent({
}

const getTotalQuota = (space: SpaceResource) => {
if (space.spaceQuota.total === 0) {
if (space.spaceQuota?.total === 0) {
return $gettext('Unrestricted')
}

return formatFileSize(space.spaceQuota.total, language.current)
return formatFileSize(space.spaceQuota?.total, language.current)
}
const getUsedQuota = (space: SpaceResource) => {
if (space.spaceQuota.used === undefined) {
if (space.spaceQuota?.used === undefined) {
return '-'
}
return formatFileSize(space.spaceQuota.used, language.current)
}
const getRemainingQuota = (space: SpaceResource) => {
if (space.spaceQuota.remaining === undefined) {
if (space.spaceQuota?.remaining === undefined) {
return '-'
}
return formatFileSize(space.spaceQuota.remaining, language.current)
Expand Down
16 changes: 16 additions & 0 deletions packages/web-app-files/tests/unit/HandleUpload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,22 @@ describe('HandleUpload', () => {
])
expect(result).toBeFalsy()
})
it('does not check quota for explorer spaces', async () => {
const size = 100
const space = mock<SpaceResource>({
driveType: 'explorer',
id: '1'
})
const { instance } = getWrapper({ spaces: [space] })
const result = await instance.checkQuotaExceeded([
mock<UppyResource>({
name: 'name',
meta: { spaceId: '1', routeName: locationSpacesGeneric.name as string },
data: { size } as Blob
})
])
expect(result).toBeFalsy()
})
it("does not check quota for other's personal spaces", async () => {
const size = 100
const remaining = 90
Expand Down
16 changes: 16 additions & 0 deletions packages/web-app-files/tests/unit/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,21 @@ describe('Web app files', () => {
expect(items[0].isVisible()).toBeFalsy()
})
})
describe('Spaces', () => {
it.each([
{ currentSpace: undefined, expectedResult: true },
{ currentSpace: mock<SpaceResource>({ driveType: 'project' }), expectedResult: true },
{ currentSpace: mock<SpaceResource>({ driveType: 'explorer' }), expectedResult: true },
{ currentSpace: mock<SpaceResource>({ driveType: 'personal' }), expectedResult: false }
])(
'is active only for project/explorer spaces, not for other eos-backed spaces like personal',
({ currentSpace, expectedResult }) => {
const spacesStore = useSpacesStore()
spacesStore.currentSpace = currentSpace
const items = navItems(undefined)
expect(items[4].isActive()).toBe(expectedResult)
}
)
})
})
})
40 changes: 25 additions & 15 deletions packages/web-client/src/helpers/resource/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,35 @@ const convertObjectToCamelCaseKeys = (data: Record<string, any>) => {
return converted
}

export function buildResource(resource: WebDavResponseResource): Resource {
const name =
resource.basename || resource.props[DavProperty.Name]?.toString() || basename(resource.filename)
const id = resource.props[DavProperty.FileId]

const isFolder = resource.type === 'directory'
let resourcePath: string

if (resource.filename.startsWith('/files') || resource.filename.startsWith('/space')) {
resourcePath = resource.filename.split('/').slice(3).join('/')
} else {
resourcePath = resource.filename
function deriveResourcePath(filename: string, webDavBasePath?: string): string {
if (webDavBasePath) {
const base = urlJoin(webDavBasePath, { leadingSlash: true, trailingSlash: false })
if (filename === base) {
return '/'
}
if (filename.startsWith(`${base}/`)) {
return filename.slice(base.length)
}
// supplied base didn't match this resource -> fall through to the legacy heuristic below
}

let resourcePath = filename
if (filename.startsWith('/files') || filename.startsWith('/space')) {
resourcePath = filename.split('/').slice(3).join('/')
}
if (!resourcePath.startsWith('/')) {
resourcePath = `/${resourcePath}`
}
return resourcePath
}

export function buildResource(resource: WebDavResponseResource, webDavBasePath?: string): Resource {
const name =
resource.basename || resource.props[DavProperty.Name]?.toString() || basename(resource.filename)
const id = resource.props[DavProperty.FileId]

const isFolder = resource.type === 'directory'
const resourcePath = deriveResourcePath(resource.filename, webDavBasePath)

const extension = extractExtensionFromFile({ ...resource, id, name, path: resourcePath })

Expand Down Expand Up @@ -204,9 +216,7 @@ export function buildResource(resource: WebDavResponseResource): Resource {
return this.permissions.indexOf(DavPermission.Shared) >= 0
},
isShareRoot(): boolean {
return resource.props[DavProperty.ShareRoot]
? resource.filename.split('/').length === 3
: false
return resource.props[DavProperty.ShareRoot] ? resourcePath === '/' : false
},
canDeny: function () {
return this.permissions.indexOf(DavPermission.Deny) >= 0
Expand Down
27 changes: 27 additions & 0 deletions packages/web-client/src/helpers/space/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ResourceIndicator
} from '../resource'
import {
EOS_EXPLORER_SPACE_ID,
isPersonalSpaceResource,
isPublicSpaceResource,
PublicSpaceResource,
Expand Down Expand Up @@ -130,6 +131,32 @@ export function buildShareSpaceResource({
return space
}

export function buildWebDavEosExplorerPath(userName: string) {
return urlJoin('files', userName, 'eos', { leadingSlash: true })
}

export function buildEosExplorerSpace({
userName,
serverUrl
}: {
userName: string
serverUrl: string
}): SpaceResource {
const space = buildSpace(
{
id: EOS_EXPLORER_SPACE_ID,
name: 'EOS',
driveType: 'explorer',
driveAlias: 'eos',
webDavPath: buildWebDavEosExplorerPath(userName),
serverUrl
},
{}
)
space.mimeType = 'eos'
return space
}

export function buildSpace(
data: Drive & {
path?: string
Expand Down
1 change: 1 addition & 0 deletions packages/web-client/src/helpers/space/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Ability, Resource } from '../resource'

export const SHARE_JAIL_ID = 'a0ca6a90-a365-4782-871e-d44447bbc668'
export const OCM_PROVIDER_ID = '89f37a33-858b-45fa-8890-a1f2b27d90e1'
export const EOS_EXPLORER_SPACE_ID = 'f6b1a2c3-7d4e-4b8a-9c2f-3e5d6a7b8c9d'

export type SpaceMember = {
grantedTo: SharePointIdentitySet
Expand Down
8 changes: 3 additions & 5 deletions packages/web-client/src/webdav/listFileVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import { buildResource } from '../helpers'
export const ListFileVersionsFactory = (dav: DAV, options: WebDavOptions) => {
return {
async listFileVersions(id: string, opts: DAVRequestOptions = {}) {
const webDavPath = urlJoin('meta', id, 'v', { leadingSlash: true })
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [currentFolder, ...versions] = await dav.propfind(
urlJoin('meta', id, 'v', { leadingSlash: true }),
opts
)
return versions.map(buildResource)
const [currentFolder, ...versions] = await dav.propfind(webDavPath, opts)
return versions.map((v) => buildResource(v, webDavPath))
}
}
}
8 changes: 4 additions & 4 deletions packages/web-client/src/webdav/listFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ export const ListFilesFactory = (
driveAlias: space.driveAlias,
webDavPath: space.webDavPath
}),
children: children.map(buildResource)
children: children.map((r) => buildResource(r, space.webDavPath))
} as ListFilesResult
}
const resources = webDavResources.map(buildResource)
const resources = webDavResources.map((r) => buildResource(r, space.webDavPath))
return { resource: resources[0], children: resources.slice(1) } as ListFilesResult
}

Expand All @@ -116,11 +116,11 @@ export const ListFilesFactory = (
})
if (isTrash) {
return {
resource: buildResource(webDavResources[0]),
resource: buildResource(webDavResources[0], webDavPath),
children: webDavResources.slice(1).map(buildDeletedResource)
} as ListFilesResult
}
const resources = webDavResources.map(buildResource)
const resources = webDavResources.map((r) => buildResource(r, space.webDavPath))
const resourceIsSpace = fileId === space.id
if (fileId && !resourceIsSpace && fileId !== resources[0].fileId) {
return listFilesCorrectedPath()
Expand Down
72 changes: 72 additions & 0 deletions packages/web-client/tests/unit/helpers/resource/functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,76 @@ describe('buildResource', () => {
expect(resource.canEditTags()).toBeFalsy()
}
)

describe('path', () => {
it('resolves to "/" when the filename is an exact match of webDavBasePath', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
filename: '/spaces/abc',
basename: 'abc',
props: {}
})
const resource = buildResource(webDavResponse, '/spaces/abc')
expect(resource.path).toBe('/')
})

it('strips webDavBasePath as a prefix when the filename is nested underneath it', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
filename: '/spaces/abc/sub/dir/file.txt',
basename: 'file.txt',
props: {}
})
const resource = buildResource(webDavResponse, '/spaces/abc')
expect(resource.path).toBe('/sub/dir/file.txt')
})

it('falls back to the legacy heuristic when webDavBasePath only shares a string prefix, not a real path segment', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
filename: '/spaces/abcdef/foo',
basename: 'foo',
props: {}
})
const resource = buildResource(webDavResponse, '/spaces/abc')
// '/spaces/abc' doesn't actually match '/spaces/abcdef/foo' (different space id), so this
// falls through to the legacy heuristic rather than being falsely treated as a prefix match
expect(resource.path).toBe('/foo')
})

it('falls back to the legacy /files-or/space heuristic when no webDavBasePath is given', () => {
const webDavResponse = buildMockWebDavResponse({})
const resource = buildResource(webDavResponse)
expect(resource.path).toBe('/file.txt')
})
})

describe('isShareRoot', () => {
it('is false when the ShareRoot prop is absent', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
filename: '/spaces/abc',
basename: 'abc',
props: { [DavProperty.ShareRoot]: undefined }
})
const resource = buildResource(webDavResponse, '/spaces/abc')
expect(resource.isShareRoot()).toBeFalsy()
})

it('is true when the ShareRoot prop is present and the resource is the root', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
filename: '/spaces/abc',
basename: 'abc',
props: { [DavProperty.ShareRoot]: '/some/remote/path' }
})
const resource = buildResource(webDavResponse, '/spaces/abc')
expect(resource.isShareRoot()).toBeTruthy()
})

it('is false when the ShareRoot prop is present but the resource is a descendant of the root', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
filename: '/spaces/abc/sub',
basename: 'sub',
props: { [DavProperty.ShareRoot]: '/some/remote/path' }
})
const resource = buildResource(webDavResponse, '/spaces/abc')
expect(resource.isShareRoot()).toBeFalsy()
})
})
})
31 changes: 30 additions & 1 deletion packages/web-client/tests/unit/helpers/space/functions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildSpace } from '../../../../src/helpers/space'
import { buildEosExplorerSpace, buildSpace, EOS_EXPLORER_SPACE_ID } from '../../../../src/helpers/space'
import { mock } from 'vitest-mock-extended'
import { Ability, GraphSharePermission, ShareRole } from '@ownclouders/web-client'
import { Drive, User } from '@ownclouders/web-client/graph/generated'
Expand Down Expand Up @@ -396,3 +396,32 @@ describe('buildSpace', () => {
)
})
})

describe('buildEosExplorerSpace', () => {
const userName = 'jdoe'
const serverUrl = 'https://example.com'

it('builds a synthetic space with the expected identity and webdav path', () => {
const space = buildEosExplorerSpace({ userName, serverUrl })
expect(space.id).toBe(EOS_EXPLORER_SPACE_ID)
expect(space.driveType).toBe('explorer')
expect(space.driveAlias).toBe('eos')
expect(space.webDavPath).toBe('/files/jdoe/eos')
expect(space.mimeType).toBe('eos')
})

it('resolves the webdav url through the legacy per-user files endpoint', () => {
const space = buildEosExplorerSpace({ userName, serverUrl })
expect(space.getWebDavUrl({ path: '' })).toBe(
'https://example.com/remote.php/dav/files/jdoe/eos'
)
})

it.each(['canRename', 'canBeDeleted', 'canShare'] as const)(
'%s is false given no user/membership',
(method) => {
const space = buildEosExplorerSpace({ userName, serverUrl })
expect(space[method]()).toBeFalsy()
}
)
})
12 changes: 11 additions & 1 deletion packages/web-pkg/src/composables/piniaStores/spaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { computed, ref, unref } from 'vue'
import {
buildEosExplorerSpace,
buildShareSpaceResource,
isMountPointSpaceResource,
SpaceResource
Expand Down Expand Up @@ -214,7 +215,16 @@ export const useSpacesStore = defineStore('spaces', () => {
})
])

addSpaces([...personalSpaces, ...projectSpaces])
const eosExplorerSpaces = configStore.options.runningOnEos
? [
buildEosExplorerSpace({
userName: userStore.user.onPremisesSamAccountName,
serverUrl: configStore.serverUrl
})
]
: []

addSpaces([...personalSpaces, ...projectSpaces, ...eosExplorerSpaces])
spacesInitialized.value = true
} finally {
spacesLoading.value = false
Expand Down
Loading