From 83b7b7d311123fe5552e5df4ef9d1cca490ae393 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 20 Feb 2024 20:35:58 +0100 Subject: [PATCH 1/4] fix(FilePicker): Adjust DAV composable to be able to work on public `webdav` endpoint Co-authored-by: Eduardo Morales Signed-off-by: Ferdinand Thiessen --- lib/components/FilePicker/FilePicker.vue | 14 +++-- lib/composables/dav.spec.ts | 31 ++++++++-- lib/composables/dav.ts | 77 +++++++++++++++++++----- lib/composables/isPublic.ts | 15 +++++ 4 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 lib/composables/isPublic.ts diff --git a/lib/components/FilePicker/FilePicker.vue b/lib/components/FilePicker/FilePicker.vue index 5d1a3d2a..0a5ab870 100644 --- a/lib/components/FilePicker/FilePicker.vue +++ b/lib/components/FilePicker/FilePicker.vue @@ -61,8 +61,9 @@ import { computed, onMounted, ref, toRef } from 'vue' import { showError } from '../../toast' import { useDAVFiles } from '../../composables/dav' import { useMimeFilter } from '../../composables/mime' -import { t } from '../../utils/l10n' import { useFilesSettings } from '../../composables/filesSettings' +import { useIsPublic } from '../../composables/isPublic' +import { t } from '../../utils/l10n' const props = withDefaults(defineProps<{ /** Buttons to be displayed */ @@ -120,6 +121,11 @@ const emit = defineEmits<{ (e: 'close', v?: Node[]): void }>() +/** + * Whether we are on a public endpoint (e.g. public share) + */ +const { isPublic } = useIsPublic() + const isOpen = ref(true) /** @@ -221,7 +227,7 @@ const filterString = ref('') const { isSupportedMimeType } = useMimeFilter(toRef(props, 'mimetypeFilter')) // vue 3.3 will allow cleaner syntax of toRef(() => props.mimetypeFilter) -const { files, isLoading, loadFiles, getFile, client } = useDAVFiles(currentView, currentPath) +const { files, isLoading, loadFiles, getFile, createDirectory } = useDAVFiles(currentView, currentPath, isPublic) onMounted(() => loadFiles()) @@ -271,9 +277,7 @@ const noFilesDescription = computed(() => { */ const onCreateFolder = async (name: string) => { try { - await client.createDirectory(join(davRootPath, currentPath.value, name)) - // reload file list - await loadFiles() + await createDirectory(name) // emit event bus to force files app to reload that file if needed emitOnEventBus('files:node:created', files.value.filter((file) => file.basename === name)[0]) } catch (error) { diff --git a/lib/composables/dav.spec.ts b/lib/composables/dav.spec.ts index b8ebd1b0..2f755b8d 100644 --- a/lib/composables/dav.spec.ts +++ b/lib/composables/dav.spec.ts @@ -28,6 +28,7 @@ import { useDAVFiles } from './dav' const nextcloudFiles = vi.hoisted(() => ({ davGetClient: vi.fn(), davRootPath: '/root/uid', + davRemoteURL: 'https://localhost/remote.php/dav', davResultToNode: vi.fn(), davGetDefaultPropfind: vi.fn(), davGetRecentSearch: (time: number) => `recent ${time}`, @@ -44,9 +45,9 @@ const waitLoaded = (vue: ReturnType) => new Promise((resolv }) const TestComponent = defineComponent({ - props: ['currentView', 'currentPath'], + props: ['currentView', 'currentPath', 'isPublic'], setup(props) { - const dav = useDAVFiles(toRef(props, 'currentView'), toRef(props, 'currentPath')) + const dav = useDAVFiles(toRef(props, 'currentView'), toRef(props, 'currentPath'), toRef(props, 'isPublic')) return { ...dav, } @@ -67,6 +68,7 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', + isPublic: false, }, }) // Loading is set to true @@ -92,6 +94,7 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', + isPublic: false, }, }) @@ -111,6 +114,7 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', + isPublic: false, }, }) @@ -138,6 +142,7 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', + isPublic: false, }, }) @@ -164,12 +169,28 @@ describe('dav composable', () => { nextcloudFiles.davGetClient.mockImplementationOnce(() => client) nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v) - const { getFile } = useDAVFiles(ref('files'), ref('/')) + const { getFile } = useDAVFiles(ref('files'), ref('/'), ref(false)) const node = await getFile('/some/path') expect(node).toEqual({ path: `${nextcloudFiles.davRootPath}/some/path` }) expect(client.stat).toBeCalledWith(`${nextcloudFiles.davRootPath}/some/path`, { details: true }) - expect(nextcloudFiles.davResultToNode).toBeCalledWith({ path: `${nextcloudFiles.davRootPath}/some/path` }) + expect(nextcloudFiles.davResultToNode).toBeCalledWith({ path: `${nextcloudFiles.davRootPath}/some/path` }, nextcloudFiles.davRootPath, nextcloudFiles.davRemoteURL) + }) + + it('createDirectory works', async () => { + const client = { + stat: vi.fn((v) => ({ data: { path: v } })), + createDirectory: vi.fn(() => {}), + } + nextcloudFiles.davGetClient.mockImplementationOnce(() => client) + nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v) + + const { createDirectory } = useDAVFiles(ref('files'), ref('/foo/'), ref(false)) + + const node = await createDirectory('my-name') + expect(node).toEqual({ path: `${nextcloudFiles.davRootPath}/foo/my-name` }) + expect(client.stat).toBeCalledWith(`${nextcloudFiles.davRootPath}/foo/my-name`, { details: true }) + expect(client.createDirectory).toBeCalledWith(`${nextcloudFiles.davRootPath}/foo/my-name`) }) it('loadFiles work', async () => { @@ -183,7 +204,7 @@ describe('dav composable', () => { const view = ref<'files' | 'recent' | 'favorites'>('files') const path = ref('/') - const { loadFiles, isLoading } = useDAVFiles(view, path) + const { loadFiles, isLoading } = useDAVFiles(view, path, ref(false)) expect(isLoading.value).toBe(true) await loadFiles() diff --git a/lib/composables/dav.ts b/lib/composables/dav.ts index 43877966..55b2d4d5 100644 --- a/lib/composables/dav.ts +++ b/lib/composables/dav.ts @@ -19,12 +19,14 @@ * along with this program. If not, see . * */ -import type { Node } from '@nextcloud/files' +import type { Folder, Node } from '@nextcloud/files' import type { ComputedRef, Ref } from 'vue' -import type { FileStat, ResponseDataDetailed, SearchResult, WebDAVClient } from 'webdav' +import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav' -import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files' -import { onMounted, ref, watch } from 'vue' +import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files' +import { generateRemoteUrl } from '@nextcloud/router' +import { join } from 'path' +import { computed, onMounted, ref, watch } from 'vue' /** * Handle file loading using WebDAV @@ -35,11 +37,35 @@ import { onMounted, ref, watch } from 'vue' export const useDAVFiles = function( currentView: Ref<'files'|'recent'|'favorites'> | ComputedRef<'files'|'recent'|'favorites'>, currentPath: Ref | ComputedRef, -): { isLoading: Ref, client: WebDAVClient, files: Ref, loadFiles: () => Promise, getFile: (path: string) => Promise } { + isPublicEndpoint: Ref | ComputedRef +): { isLoading: Ref, createDirectory: (name: string) => Promise, files: Ref, loadFiles: () => Promise, getFile: (path: string) => Promise } { + + const defaultRootPath = computed(() => isPublicEndpoint.value ? '/' : davRootPath) + + const defaultRemoteUrl = computed(() => { + if (isPublicEndpoint.value) { + return generateRemoteUrl('webdav').replace('/remote.php', '/public.php') + } + return davRemoteURL + }) + /** * The WebDAV client */ - const client = davGetClient() + const client = computed(() => { + if (isPublicEndpoint.value) { + const token = (document.getElementById('sharingToken')! as HTMLInputElement).value + const authorization = btoa(`${token}:null`) + + return davGetClient(defaultRemoteUrl.value, { + Authorization: `Basic ${authorization}`, + }) + } + + return davGetClient() + }) + + const resultToNode = (result: FileStat) => davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value) /** * All files in current view and path @@ -51,17 +77,33 @@ export const useDAVFiles = function( */ const isLoading = ref(true) + /** + * Create a new directory in the current path + * @param name Name of the new directory + * @return The created directory + */ + async function createDirectory(name: string): Promise { + const path = join(currentPath.value, name) + + await client.value.createDirectory(join(defaultRootPath.value, path)) + const directory = await getFile(path) as Folder + files.value.push(directory) + return directory + } + /** * Get information for one file * * @param path The path of the file or folder * @param rootPath DAV root path, defaults to '/files/USERID' */ - async function getFile(path: string, rootPath = davRootPath) { - const result = await client.stat(`${rootPath}${path}`, { + async function getFile(path: string, rootPath: string|undefined = undefined) { + rootPath = rootPath ?? defaultRootPath.value + + const { data } = await client.value.stat(`${rootPath}${path}`, { details: true, }) as ResponseDataDetailed - return davResultToNode(result.data) + return resultToNode(data) } /** @@ -71,21 +113,26 @@ export const useDAVFiles = function( isLoading.value = true if (currentView.value === 'favorites') { - files.value = await getFavoriteNodes(client, currentPath.value) + files.value = await getFavoriteNodes(client.value, currentPath.value, defaultRootPath.value) } else if (currentView.value === 'recent') { // unix timestamp in seconds, two weeks ago const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14) - const { data } = await client.search('/', { + const { data } = await client.value.search('/', { details: true, data: davGetRecentSearch(lastTwoWeek), }) as ResponseDataDetailed - files.value = data.results.map((r) => davResultToNode(r)) + files.value = data.results.map(resultToNode) } else { - const results = await client.getDirectoryContents(`${davRootPath}${currentPath.value}`, { + const results = await client.value.getDirectoryContents(`${defaultRootPath.value}${currentPath.value}`, { details: true, data: davGetDefaultPropfind(), }) as ResponseDataDetailed - files.value = results.data.map((r) => davResultToNode(r)) + files.value = results.data.map(resultToNode) + + // Hack for the public endpoint which always returns folder itself + if (isPublicEndpoint.value) { + files.value = files.value.filter((file) => file.path !== currentPath.value) + } } isLoading.value = false @@ -106,6 +153,6 @@ export const useDAVFiles = function( files, loadFiles: loadDAVFiles, getFile, - client, + createDirectory, } } diff --git a/lib/composables/isPublic.ts b/lib/composables/isPublic.ts new file mode 100644 index 00000000..c80dab17 --- /dev/null +++ b/lib/composables/isPublic.ts @@ -0,0 +1,15 @@ +import { onBeforeMount, ref } from "vue" + +/** + * Check whether the component is mounted in a public share + */ +export const useIsPublic = () => { + const checkIsPublic = () => (document.getElementById('isPublic') as HTMLInputElement|null)?.value === '1' + + const isPublic = ref(true) + onBeforeMount(() => { isPublic.value = checkIsPublic() }) + + return { + isPublic, + } +} From d3c4e26c05ad572b970ad04a1dc4aaa82f46c0d4 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 20 Feb 2024 20:37:35 +0100 Subject: [PATCH 2/4] fix(FilePicker): Only load files views and files config on internal routes Signed-off-by: Ferdinand Thiessen --- lib/components/FilePicker/FilePicker.vue | 5 +- .../FilePicker/FilePickerNavigation.vue | 73 ++++++++--------- lib/composables/filesSettings.spec.ts | 22 ++++-- lib/composables/filesSettings.ts | 67 +++++++++++----- lib/composables/views.spec.ts | 50 ++++++++++++ lib/composables/views.ts | 79 +++++++++++++++++++ package-lock.json | 6 ++ package.json | 1 + 8 files changed, 231 insertions(+), 72 deletions(-) create mode 100644 lib/composables/views.spec.ts create mode 100644 lib/composables/views.ts diff --git a/lib/components/FilePicker/FilePicker.vue b/lib/components/FilePicker/FilePicker.vue index 0a5ab870..8d36cbf2 100644 --- a/lib/components/FilePicker/FilePicker.vue +++ b/lib/components/FilePicker/FilePicker.vue @@ -45,8 +45,9 @@