Skip to content

Commit

Permalink
fix!: Revert breaking changes in DAV endpoint handling
Browse files Browse the repository at this point in the history
5.3.3 introduced breaking changes as it did no longer support Nextcloud 28 and older. Revert those changes.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Aug 16, 2024
1 parent 1c61723 commit b72553c
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 20 deletions.
6 changes: 1 addition & 5 deletions lib/composables/dav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const waitRefLoaded = (isLoading: Ref<boolean>) => 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 {
Expand All @@ -60,7 +60,6 @@ describe('dav composable', () => {
propsData: {
currentView: 'files',
currentPath: '/',
isPublic: false,
},
})
// Loading is set to true
Expand All @@ -85,7 +84,6 @@ describe('dav composable', () => {
propsData: {
currentView: 'files',
currentPath: '/',
isPublic: false,
},
})

Expand All @@ -105,7 +103,6 @@ describe('dav composable', () => {
propsData: {
currentView: 'files',
currentPath: '/',
isPublic: false,
},
})

Expand Down Expand Up @@ -133,7 +130,6 @@ describe('dav composable', () => {
propsData: {
currentView: 'files',
currentPath: '/',
isPublic: false,
},
})

Expand Down
106 changes: 95 additions & 11 deletions lib/composables/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,10 +24,76 @@ export const useDAVFiles = function(
currentPath: Ref<string> | ComputedRef<string>,
) {

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<Node[]> => {
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<SearchResult>
const nodes = data.results.map(resultToNode)
resolve(nodes)
} catch (error) {
reject(error)
}
})
}

const getNodes = (): CancelablePromise<Node[]> => {
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<FileStat[]>
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
Expand All @@ -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<Folder>} The created directory
* @return The created directory
*/
async function createDirectory(name: string): Promise<Folder> {
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<FileStat>
return resultToNode(data)
}

/**
* Force reload files using the DAV client
*/
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions lib/composables/filesSettings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion lib/toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/dav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` })
})
})
})
1 change: 1 addition & 0 deletions lib/utils/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down

0 comments on commit b72553c

Please sign in to comment.