Skip to content

Commit

Permalink
Merge pull request #1243 from nextcloud-libraries/fix/stable4-public-…
Browse files Browse the repository at this point in the history
…shares
  • Loading branch information
skjnldsv authored Feb 22, 2024
2 parents c5b6b72 + 78b2af5 commit aae5ecb
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 65 deletions.
25 changes: 15 additions & 10 deletions lib/components/FilePicker/FilePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<FilePickerBreadcrumbs v-if="currentView === 'files'"
:path.sync="currentPath"
:show-menu="allowPickDirectory"
@create-node="onCreateFolder"/>
@create-node="onCreateFolder" />
<div v-else class="file-picker__view">
<h3>{{ viewHeadline }}</h3>
</div>
Expand Down Expand Up @@ -53,13 +53,12 @@ import FileList from './FileList.vue'
import FilePickerBreadcrumbs from './FilePickerBreadcrumbs.vue'
import FilePickerNavigation from './FilePickerNavigation.vue'
import { davRootPath } from '@nextcloud/files'
import { NcEmptyContent } from '@nextcloud/vue'
import { join } from 'path'
import { computed, onMounted, ref, toRef } from 'vue'
import { showError } from '../../toast'
import { useDAVFiles } from '../../usables/dav'
import { useMimeFilter } from '../../usables/mime'
import { useIsPublic } from '../../usables/isPublic'
import { t } from '../../utils/l10n'
const props = withDefaults(defineProps<{
Expand Down Expand Up @@ -118,6 +117,11 @@ const emit = defineEmits<{
(e: 'close', v?: Node[]): void
}>()
/**
* Whether we are on a public endpoint (e.g. public share)
*/
const { isPublic } = useIsPublic()
/**
* Props to be passed to the underlying Dialog component
*/
Expand Down Expand Up @@ -203,7 +207,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())
Expand Down Expand Up @@ -243,13 +247,14 @@ const noFilesDescription = computed(() => {
* Handle creating new folder (breadcrumb menu)
* @param name The new folder name
*/
const onCreateFolder = (name: string) => {
client
.createDirectory(join(davRootPath, currentPath.value, name))
// reload file list
.then(() => loadFiles())
const onCreateFolder = async (name: string) => {
try {
await createDirectory(name)
} catch (error) {
console.warn('Could not create new folder', { name, error })
// show error to user
.catch((e) => showError(t('Could not create the new folder')))
showError(t('Could not create the new folder'))
}
}
</script>

Expand Down
57 changes: 31 additions & 26 deletions lib/components/FilePicker/FilePickerNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,33 @@
<IconClose :size="16" />
</template>
</NcTextField>
<!-- On non collapsed dialogs show the tablist, otherwise a dropdown is shown -->
<ul v-if="!isCollapsed"
class="file-picker__side"
role="tablist"
:aria-label="t('Filepicker sections')">
<li v-for="view in allViews" :key="view.id">
<NcButton :aria-selected="currentView === view.id"
:type="currentView === view.id ? 'primary' : 'tertiary'"
:wide="true"
role="tab"
@click="$emit('update:currentView', view.id)">
<template #icon>
<component :is="view.icon" :size="20" />
</template>
{{ view.label }}
</NcButton>
</li>
</ul>
<NcSelect v-else
:aria-label="t('Current view selector')"
:clearable="false"
:searchable="false"
:options="allViews"
:value="currentViewObject"
@input="v => emit('update:currentView', v.id)" />
<template v-if="!isPublic">
<!-- On non collapsed dialogs show the tablist, otherwise a dropdown is shown -->
<ul v-if="!isCollapsed"
class="file-picker__side"
role="tablist"
:aria-label="t('Filepicker sections')">
<li v-for="view in allViews" :key="view.id">
<NcButton :aria-selected="currentView === view.id"
:type="currentView === view.id ? 'primary' : 'tertiary'"
:wide="true"
role="tab"
@click="$emit('update:currentView', view.id)">
<template #icon>
<component :is="view.icon" :size="20" />
</template>
{{ view.label }}
</NcButton>
</li>
</ul>
<NcSelect v-else
:aria-label="t('Current view selector')"
:clearable="false"
:searchable="false"
:options="allViews"
:value="currentViewObject"
@input="v => emit('update:currentView', v.id)" />
</template>
</Fragment>
</template>

Expand All @@ -48,9 +50,12 @@ import IconMagnify from 'vue-material-design-icons/Magnify.vue'
import IconStar from 'vue-material-design-icons/Star.vue'
import { NcButton, NcSelect, NcTextField } from '@nextcloud/vue'
import { t } from '../../utils/l10n'
import { computed } from 'vue'
import { Fragment } from 'vue-frag'
import { t } from '../../utils/l10n'
import { useIsPublic } from '../../usables/isPublic'
const { isPublic } = useIsPublic()
const allViews = [{
id: 'files',
Expand Down
118 changes: 89 additions & 29 deletions lib/usables/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,76 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node } from '@nextcloud/files'
import type { Folder, Node } from '@nextcloud/files'
import type { ComputedRef, Ref } from 'vue'
import type { FileStat, ResponseDataDetailed, WebDAVClient } from 'webdav'
import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav'

import { davGetClient, davGetDefaultPropfind, davGetFavoritesReport, davGetRecentSearch, davResultToNode, davRootPath } from '@nextcloud/files'
import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files'
import { generateRemoteUrl } from '@nextcloud/router'
import { ref, watch } from 'vue'
import { dirname, join } from 'path'
import { computed, ref, watch } from 'vue'

/**
* Handle file loading using WebDAV
*
* @param currentView Reference to the current files view
* @param currentPath Reference to the current files path
* @param isPublicEndpoint Whether the filepicker is used on a public share
*/
export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites'> | ComputedRef<'files'|'recent'|'favorites'>, currentPath: Ref<string> | ComputedRef<string>): { isLoading: Ref<boolean>, client: WebDAVClient, files: Ref<Node[]>, loadFiles: () => void, getFile: (path: string) => Promise<Node> } {
export const useDAVFiles = function(
currentView: Ref<'files'|'recent'|'favorites'> | ComputedRef<'files'|'recent'|'favorites'>,
currentPath: Ref<string> | ComputedRef<string>,
isPublicEndpoint: Ref<boolean> | ComputedRef<boolean>,
): { isLoading: Ref<boolean>, createDirectory: (name: string) => Promise<Folder>, files: Ref<Node[]>, loadFiles: () => Promise<void>, getFile: (path: string) => Promise<Node> } {

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(generateRemoteUrl('dav'))
const client = computed(() => {
if (isPublicEndpoint.value) {
const token = (document.getElementById('sharingToken')! as HTMLInputElement).value
const autorization = btoa(`${token}:null`)

const client = davGetClient(defaultRemoteUrl.value)
client.setHeaders({ Authorization: `Basic ${autorization}` })
return client
}

return davGetClient()
})

const resultToNode = (result: FileStat) => {
const node = davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value)
// Fixed for @nextcloud/files 3.1.0 but not supported on Nextcloud 27 so patching it
if (isPublicEndpoint.value) {
return new Proxy(node, {
get(node, prop) {
if (prop === 'dirname' || prop === 'path') {
const source = node.source
let path = source.slice(defaultRemoteUrl.value.length)
if (path[0] !== '/') {
path = `/${path}`
}
if (prop === 'dirname') {
return dirname(path)
}
return path
}
return (node as never)[prop]
},
})
}
return node
}

/**
* All queried files
Expand All @@ -49,15 +100,32 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
*/
const isLoading = ref(true)

/**
* Create a new directory in the current path
* @param name Name of the new directory
* @return {Promise<Folder>} The created directory
*/
async function createDirectory(name: string): Promise<Folder> {
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 The dav root path to use (or the default is nothing set)
*/
async function getFile(path: string) {
const result = await client.stat(`${davRootPath}${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<FileStat>
return davResultToNode(result.data)
return resultToNode(data)
}

/**
Expand All @@ -67,34 +135,26 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
isLoading.value = true

if (currentView.value === 'favorites') {
files.value = await client.getDirectoryContents(`${davRootPath}${currentPath.value}`, {
details: true,
data: davGetFavoritesReport(),
headers: {
method: 'REPORT',
},
includeSelf: false,
}).then((result) => (result as ResponseDataDetailed<FileStat[]>).data.map((data) => davResultToNode(data)))
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 results = await client.getDirectoryContents(currentPath.value, {
const { data } = await client.value.search('/', {
details: true,
data: davGetRecentSearch(lastTwoWeek),
headers: {
method: 'SEARCH',
'Content-Type': 'application/xml; charset=utf-8',
},
deep: true,
}) as ResponseDataDetailed<FileStat[]>

files.value = results.data.map((r) => davResultToNode(r))
}) as ResponseDataDetailed<SearchResult>
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<FileStat[]>
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
Expand All @@ -110,6 +170,6 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
files,
loadFiles: () => loadDAVFiles(),
getFile,
client,
createDirectory,
}
}
15 changes: 15 additions & 0 deletions lib/usables/isPublic.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}

0 comments on commit aae5ecb

Please sign in to comment.