diff --git a/lib/composables/dav.spec.ts b/lib/composables/dav.spec.ts index 1d0ccc57..a1357ddb 100644 --- a/lib/composables/dav.spec.ts +++ b/lib/composables/dav.spec.ts @@ -37,7 +37,7 @@ const waitRefLoaded = (isLoading: Ref) => new Promise((resolve) => { }) const TestComponent = defineComponent({ - props: ['currentView', 'currentPath', 'isPublic'], + props: ['currentView', 'currentPath'], setup(props) { const dav = useDAVFiles(toRef(props, 'currentView'), toRef(props, 'currentPath')) return { @@ -60,7 +60,6 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', - isPublic: false, }, }) // Loading is set to true @@ -85,7 +84,6 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', - isPublic: false, }, }) @@ -105,7 +103,6 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', - isPublic: false, }, }) @@ -133,7 +130,6 @@ describe('dav composable', () => { propsData: { currentView: 'files', currentPath: '/', - isPublic: false, }, }) diff --git a/lib/composables/dav.ts b/lib/composables/dav.ts index 6467c302..276fe2d2 100644 --- a/lib/composables/dav.ts +++ b/lib/composables/dav.ts @@ -4,12 +4,14 @@ */ import type { ContentsWithRoot, Folder, Node } from '@nextcloud/files' import type { ComputedRef, Ref } from 'vue' +import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav' -import { davGetClient, davRootPath, getFavoriteNodes } from '@nextcloud/files' -import { CancelablePromise } from 'cancelable-promise' +import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files' +import { generateRemoteUrl } from '@nextcloud/router' +import { isPublicShare } from '@nextcloud/sharing/public' import { join } from 'node:path' -import { onMounted, ref, shallowRef, watch } from 'vue' -import { getFile, getNodes, getRecentNodes } from '../utils/dav' +import { computed, onMounted, ref, shallowRef, watch } from 'vue' +import { CancelablePromise } from 'cancelable-promise' /** * Handle file loading using WebDAV @@ -22,10 +24,76 @@ export const useDAVFiles = function( currentPath: Ref | ComputedRef, ) { + const isPublicEndpoint = isPublicShare() + + const defaultRootPath = isPublicEndpoint ? '/' : davRootPath + + const defaultRemoteUrl = computed(() => { + if (isPublicEndpoint) { + return generateRemoteUrl('webdav').replace('/remote.php', '/public.php') + } + return davRemoteURL + }) + /** * The WebDAV client */ - const client = davGetClient() + const client = computed(() => { + if (isPublicEndpoint) { + 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, defaultRemoteUrl.value) + + const getRecentNodes = (): CancelablePromise => { + const controller = new AbortController() + // unix timestamp in seconds, two weeks ago + const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14) + return new CancelablePromise(async (resolve, reject, onCancel) => { + onCancel(() => controller.abort()) + try { + const { data } = await client.value.search('/', { + signal: controller.signal, + details: true, + data: davGetRecentSearch(lastTwoWeek), + }) as ResponseDataDetailed + const nodes = data.results.map(resultToNode) + resolve(nodes) + } catch (error) { + reject(error) + } + }) + } + + const getNodes = (): CancelablePromise => { + const controller = new AbortController() + return new CancelablePromise(async (resolve, reject, onCancel) => { + onCancel(() => controller.abort()) + try { + const results = await client.value.getDirectoryContents(`${defaultRootPath}${currentPath.value}`, { + signal: controller.signal, + details: true, + data: davGetDefaultPropfind(), + }) as ResponseDataDetailed + let nodes = results.data.map(resultToNode) + // Hack for the public endpoint which always returns folder itself + if (isPublicEndpoint) { + nodes = nodes.filter((file) => file.path !== currentPath.value) + } + resolve(nodes) + } catch (error) { + reject(error) + } + }) + } /** * All files in current view and path @@ -51,17 +119,33 @@ export const useDAVFiles = function( * Create a new directory in the current path * The directory will be added to the current file list * @param name Name of the new directory - * @return {Promise} The created directory + * @return The created directory */ async function createDirectory(name: string): Promise { const path = join(currentPath.value, name) - await client.createDirectory(join(davRootPath, path)) - const directory = await getFile(client, path) as Folder + await client.value.createDirectory(join(defaultRootPath, path)) + const directory = await getFile(path) as Folder files.value = [...files.value, 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: string|undefined = undefined) { + rootPath = rootPath ?? defaultRootPath + + const { data } = await client.value.stat(`${rootPath}${path}`, { + details: true, + data: davGetDefaultPropfind(), + }) as ResponseDataDetailed + return resultToNode(data) + } + /** * Force reload files using the DAV client */ @@ -72,11 +156,11 @@ export const useDAVFiles = function( isLoading.value = true if (currentView.value === 'favorites') { - promise.value = getFavoriteNodes(client, currentPath.value) + promise.value = getFavoriteNodes(client.value, currentPath.value, defaultRootPath) } else if (currentView.value === 'recent') { - promise.value = getRecentNodes(client) + promise.value = getRecentNodes() } else { - promise.value = getNodes(client, currentPath.value) + promise.value = getNodes() } const content = await promise.value if ('folder' in content) { diff --git a/lib/composables/filesSettings.spec.ts b/lib/composables/filesSettings.spec.ts index 5c8ff99b..cb924beb 100644 --- a/lib/composables/filesSettings.spec.ts +++ b/lib/composables/filesSettings.spec.ts @@ -11,10 +11,10 @@ import { useFilesSettings } from './filesSettings' const axios = vi.hoisted(() => ({ get: vi.fn(), })) -const isPublic = vi.hoisted(() => ({ value: false })) +const nextcloudSharing = vi.hoisted(() => ({ isPublicShare: vi.fn(() => false) })) vi.mock('@nextcloud/axios', () => ({ default: axios })) -vi.mock('./isPublic', () => ({ useIsPublic: () => ({ isPublic }) })) +vi.mock('@nextcloud/sharing/public', () => nextcloudSharing) const TestComponent = defineComponent({ setup() { diff --git a/lib/toast.ts b/lib/toast.ts index 2b55a259..8e345b90 100644 --- a/lib/toast.ts +++ b/lib/toast.ts @@ -214,7 +214,7 @@ export function showUndo(text: string, onUndo: (e: MouseEvent) => void, options? // force 10 seconds of timeout timeout: TOAST_UNDO_TIMEOUT, // remove close button - close: false + close: false, }) // Generate undo layout diff --git a/lib/utils/dav.spec.ts b/lib/utils/dav.spec.ts index a0cfff79..553ddf79 100644 --- a/lib/utils/dav.spec.ts +++ b/lib/utils/dav.spec.ts @@ -31,4 +31,4 @@ describe('DAV utils', () => { expect(client.stat).toBeCalledWith(`${nextcloudFiles.davRootPath}/some/path/file.ext`, { details: true, data: 'propfind content' }) expect(nextcloudFiles.davResultToNode).toBeCalledWith({ path: `${nextcloudFiles.davRootPath}/some/path/file.ext` }) }) -}) \ No newline at end of file +}) diff --git a/lib/utils/dialogs.ts b/lib/utils/dialogs.ts index d17512e5..0bb4d061 100644 --- a/lib/utils/dialogs.ts +++ b/lib/utils/dialogs.ts @@ -13,6 +13,7 @@ import Vue, { toRaw } from 'vue' * @param props Properties to pass to the dialog * @param onClose Callback when the dialog is closed */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const spawnDialog = (dialog: Component | AsyncComponent, props: any, onClose: (...rest: unknown[]) => void = () => {}): Vue => { const el = document.createElement('div') diff --git a/tsconfig.json b/tsconfig.json index afe5d1e0..992d5b37 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,9 @@ "lib": ["DOM", "ESNext"], "outDir": "./dist", "rootDir": "lib/", + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ESNext", "sourceMap": true, "plugins": [ { "name": "typescript-plugin-css-modules" }