aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src')
-rw-r--r--apps/files_sharing/src/init-public.ts28
-rw-r--r--apps/files_sharing/src/router/index.ts54
-rw-r--r--apps/files_sharing/src/services/SharingService.spec.ts4
-rw-r--r--apps/files_sharing/src/services/SharingService.ts13
-rw-r--r--apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue67
-rw-r--r--apps/files_sharing/src/views/publicFileDrop.ts59
-rw-r--r--apps/files_sharing/src/views/publicFileShare.ts67
-rw-r--r--apps/files_sharing/src/views/publicShare.ts28
8 files changed, 310 insertions, 10 deletions
diff --git a/apps/files_sharing/src/init-public.ts b/apps/files_sharing/src/init-public.ts
new file mode 100644
index 00000000000..400ee73d2a1
--- /dev/null
+++ b/apps/files_sharing/src/init-public.ts
@@ -0,0 +1,28 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { getNavigation, registerDavProperty } from '@nextcloud/files'
+import { loadState } from '@nextcloud/initial-state'
+import registerFileDropView from './views/publicFileDrop.ts'
+import registerPublicShareView from './views/publicShare.ts'
+import registerPublicFileShareView from './views/publicFileShare.ts'
+import RouterService from '../../files/src/services/RouterService'
+import router from './router'
+
+registerFileDropView()
+registerPublicShareView()
+registerPublicFileShareView()
+
+registerDavProperty('nc:share-attributes', { nc: 'http://nextcloud.org/ns' })
+registerDavProperty('oc:share-types', { oc: 'http://owncloud.org/ns' })
+registerDavProperty('ocs:share-permissions', { ocs: 'http://open-collaboration-services.org/ns' })
+
+// Get the current view from state and set it active
+const view = loadState<string>('files_sharing', 'view')
+const navigation = getNavigation()
+navigation.setActive(navigation.views.find(({ id }) => id === view) ?? null)
+
+// Force our own router
+window.OCP.Files = window.OCP.Files ?? {}
+window.OCP.Files.Router = new RouterService(router)
diff --git a/apps/files_sharing/src/router/index.ts b/apps/files_sharing/src/router/index.ts
new file mode 100644
index 00000000000..6a417975e32
--- /dev/null
+++ b/apps/files_sharing/src/router/index.ts
@@ -0,0 +1,54 @@
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { RawLocation, Route } from 'vue-router'
+import type { ErrorHandler } from 'vue-router/types/router.d.ts'
+
+import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+import queryString from 'query-string'
+import Router from 'vue-router'
+import Vue from 'vue'
+
+const view = loadState<string>('files_sharing', 'view')
+const sharingToken = loadState<string>('files_sharing', 'sharingToken')
+
+Vue.use(Router)
+
+// Prevent router from throwing errors when we're already on the page we're trying to go to
+const originalPush = Router.prototype.push as (to, onComplete?, onAbort?) => Promise<Route>
+Router.prototype.push = function push(to: RawLocation, onComplete?: ((route: Route) => void) | undefined, onAbort?: ErrorHandler | undefined): Promise<Route> {
+ if (onComplete || onAbort) return originalPush.call(this, to, onComplete, onAbort)
+ return originalPush.call(this, to).catch(err => err)
+}
+
+const router = new Router({
+ mode: 'history',
+
+ // if index.php is in the url AND we got this far, then it's working:
+ // let's keep using index.php in the url
+ base: generateUrl('/s'),
+ linkActiveClass: 'active',
+
+ routes: [
+ {
+ path: '/',
+ // Pretending we're using the default view
+ redirect: { name: 'filelist', params: { view, token: sharingToken } },
+ },
+ {
+ path: '/:token',
+ name: 'filelist',
+ props: true,
+ },
+ ],
+
+ // Custom stringifyQuery to prevent encoding of slashes in the url
+ stringifyQuery(query) {
+ const result = queryString.stringify(query).replace(/%2F/gmi, '/')
+ return result ? ('?' + result) : ''
+ },
+})
+
+export default router
diff --git a/apps/files_sharing/src/services/SharingService.spec.ts b/apps/files_sharing/src/services/SharingService.spec.ts
index ab0a5163618..daba81bd4f2 100644
--- a/apps/files_sharing/src/services/SharingService.spec.ts
+++ b/apps/files_sharing/src/services/SharingService.spec.ts
@@ -18,14 +18,12 @@ const axios = vi.hoisted(() => ({ get: vi.fn() }))
vi.mock('@nextcloud/auth')
vi.mock('@nextcloud/axios', () => ({ default: axios }))
-// Mock web root variable
+// Mock TAG
beforeAll(() => {
window.OC = {
...window.OC,
TAG_FAVORITE,
}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- ;(window as any)._oc_webroot = ''
})
describe('SharingService methods definitions', () => {
diff --git a/apps/files_sharing/src/services/SharingService.ts b/apps/files_sharing/src/services/SharingService.ts
index e168f202fba..2f8144e216e 100644
--- a/apps/files_sharing/src/services/SharingService.ts
+++ b/apps/files_sharing/src/services/SharingService.ts
@@ -6,18 +6,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { AxiosPromise } from '@nextcloud/axios'
+import type { ContentsWithRoot } from '@nextcloud/files'
import type { OCSResponse } from '@nextcloud/typings/ocs'
import type { ShareAttribute } from '../sharing'
-import { Folder, File, type ContentsWithRoot, Permission } from '@nextcloud/files'
-import { generateOcsUrl, generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
+import { Folder, File, Permission, davRemoteURL, davRootPath } from '@nextcloud/files'
+import { generateOcsUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import logger from './logger'
-export const rootPath = `/files/${getCurrentUser()?.uid}`
-
const headers = {
'Content-Type': 'application/json',
}
@@ -57,7 +56,7 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu
// Generate path and strip double slashes
const path = ocsEntry.path || ocsEntry.file_target || ocsEntry.name
- const source = generateRemoteUrl(`dav/${rootPath}/${path}`.replaceAll(/\/\//gm, '/'))
+ const source = `${davRemoteURL}${davRootPath}/${path.replace(/^\/+/, '')}`
let mtime = ocsEntry.item_mtime ? new Date((ocsEntry.item_mtime) * 1000) : undefined
// Prefer share time if more recent than item mtime
@@ -73,7 +72,7 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu
mtime,
size: ocsEntry?.item_size,
permissions: ocsEntry?.item_permissions || ocsEntry?.permissions,
- root: rootPath,
+ root: davRootPath,
attributes: {
...ocsEntry,
'has-preview': hasPreview,
@@ -217,7 +216,7 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true,
return {
folder: new Folder({
id: 0,
- source: generateRemoteUrl('dav' + rootPath),
+ source: `${davRemoteURL}${davRootPath}`,
owner: getCurrentUser()?.uid || null,
}),
contents,
diff --git a/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
new file mode 100644
index 00000000000..538927623ed
--- /dev/null
+++ b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
@@ -0,0 +1,67 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcEmptyContent class="file-drop-empty-content"
+ data-cy-files-sharing-file-drop
+ :name="t('files_sharing', 'File drop')">
+ <template #icon>
+ <NcIconSvgWrapper :svg="svgCloudUpload" />
+ </template>
+ <template #description>
+ {{ t('files_sharing', 'Upload files to {foldername}.', { foldername }) }}
+ {{ disclaimer === '' ? '' : t('files_sharing', 'By uploading files, you agree to the terms of service.') }}
+ </template>
+ <template #action>
+ <template v-if="disclaimer">
+ <!-- Terms of service if enabled -->
+ <NcButton type="primary" @click="showDialog = true">
+ {{ t('files_sharing', 'View terms of service') }}
+ </NcButton>
+ <NcDialog close-on-click-outside
+ content-classes="terms-of-service-dialog"
+ :open.sync="showDialog"
+ :name="t('files_sharing', 'Terms of service')"
+ :message="disclaimer" />
+ </template>
+ <UploadPicker allow-folders
+ :content="() => []"
+ no-menu
+ :destination="uploadDestination"
+ multiple />
+ </template>
+ </NcEmptyContent>
+</template>
+
+<script setup lang="ts">
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
+import { getUploader, UploadPicker } from '@nextcloud/upload'
+import { ref } from 'vue'
+
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
+import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw'
+
+defineProps<{
+ foldername: string
+}>()
+
+const disclaimer = loadState<string>('files_sharing', 'disclaimer', '')
+const showDialog = ref(false)
+const uploadDestination = getUploader().destination
+</script>
+
+<style scoped>
+:deep(.terms-of-service-dialog) {
+ min-height: min(100px, 20vh);
+}
+/* TODO fix in library */
+.file-drop-empty-content :deep(.empty-content__action) {
+ display: flex;
+ gap: var(--default-grid-baseline);
+}
+</style>
diff --git a/apps/files_sharing/src/views/publicFileDrop.ts b/apps/files_sharing/src/views/publicFileDrop.ts
new file mode 100644
index 00000000000..0d782d48fc7
--- /dev/null
+++ b/apps/files_sharing/src/views/publicFileDrop.ts
@@ -0,0 +1,59 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { VueConstructor } from 'vue'
+
+import { Folder, Permission, View, davRemoteURL, davRootPath, getNavigation } from '@nextcloud/files'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
+import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw'
+import Vue from 'vue'
+
+export default () => {
+ const foldername = loadState<string>('files_sharing', 'filename')
+
+ let FilesViewFileDropEmptyContent: VueConstructor
+ let fileDropEmptyContentInstance: Vue
+
+ const view = new View({
+ id: 'public-file-drop',
+ name: t('files_sharing', 'File drop'),
+ caption: t('files_sharing', 'Upload files to {foldername}', { foldername }),
+ icon: svgCloudUpload,
+ order: 1,
+
+ emptyView: async (div: HTMLDivElement) => {
+ if (FilesViewFileDropEmptyContent === undefined) {
+ const { default: component } = await import('../views/FilesViewFileDropEmptyContent.vue')
+ FilesViewFileDropEmptyContent = Vue.extend(component)
+ }
+ if (fileDropEmptyContentInstance) {
+ fileDropEmptyContentInstance.$destroy()
+ }
+ fileDropEmptyContentInstance = new FilesViewFileDropEmptyContent({
+ propsData: {
+ foldername,
+ },
+ })
+ fileDropEmptyContentInstance.$mount(div)
+ },
+
+ getContents: async () => {
+ return {
+ contents: [],
+ // Fake a writeonly folder as root
+ folder: new Folder({
+ id: 0,
+ source: `${davRemoteURL}${davRootPath}`,
+ root: davRootPath,
+ owner: null,
+ permissions: Permission.CREATE,
+ }),
+ }
+ },
+ })
+
+ const Navigation = getNavigation()
+ Navigation.register(view)
+}
diff --git a/apps/files_sharing/src/views/publicFileShare.ts b/apps/files_sharing/src/views/publicFileShare.ts
new file mode 100644
index 00000000000..b2b9de9ea5f
--- /dev/null
+++ b/apps/files_sharing/src/views/publicFileShare.ts
@@ -0,0 +1,67 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { FileStat, ResponseDataDetailed } from 'webdav'
+import { Folder, Permission, View, davGetDefaultPropfind, davRemoteURL, davRootPath, getNavigation } from '@nextcloud/files'
+import { translate as t } from '@nextcloud/l10n'
+import { CancelablePromise } from 'cancelable-promise'
+import LinkSvg from '@mdi/svg/svg/link.svg?raw'
+
+import { resultToNode } from '../../../files/src/services/Files'
+import { client } from '../../../files/src/services/WebdavClient'
+import logger from '../services/logger'
+
+export default () => {
+ const view = new View({
+ id: 'public-file-share',
+ name: t('files_sharing', 'Public file share'),
+ caption: t('files_sharing', 'Public shared file.'),
+
+ emptyTitle: t('files_sharing', 'No file'),
+ emptyCaption: t('files_sharing', 'The file shared with you will show up here'),
+
+ icon: LinkSvg,
+ order: 1,
+
+ getContents: () => {
+ return new CancelablePromise(async (resolve, reject, onCancel) => {
+ const abort = new AbortController()
+ onCancel(() => abort.abort())
+ try {
+ const node = await client.stat(
+ davRootPath,
+ {
+ data: davGetDefaultPropfind(),
+ details: true,
+ signal: abort.signal,
+ },
+ ) as ResponseDataDetailed<FileStat>
+
+ resolve({
+ // We only have one file as the content
+ contents: [resultToNode(node.data)],
+ // Fake a readonly folder as root
+ folder: new Folder({
+ id: 0,
+ source: `${davRemoteURL}${davRootPath}`,
+ root: davRootPath,
+ owner: null,
+ permissions: Permission.READ,
+ attributes: {
+ // Ensure the share note is set on the root
+ note: node.data.props?.note,
+ },
+ }),
+ })
+ } catch (e) {
+ logger.error(e as Error)
+ reject(e as Error)
+ }
+ })
+ },
+ })
+
+ const Navigation = getNavigation()
+ Navigation.register(view)
+}
diff --git a/apps/files_sharing/src/views/publicShare.ts b/apps/files_sharing/src/views/publicShare.ts
new file mode 100644
index 00000000000..118973f54f5
--- /dev/null
+++ b/apps/files_sharing/src/views/publicShare.ts
@@ -0,0 +1,28 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { translate as t } from '@nextcloud/l10n'
+import { View, getNavigation } from '@nextcloud/files'
+import LinkSvg from '@mdi/svg/svg/link.svg?raw'
+
+import { getContents } from '../../../files/src/services/Files'
+
+export default () => {
+ const view = new View({
+ id: 'public-share',
+ name: t('files_sharing', 'Public share'),
+ caption: t('files_sharing', 'Public shared files.'),
+
+ emptyTitle: t('files_sharing', 'No files'),
+ emptyCaption: t('files_sharing', 'Files and folders shared with you will show up here'),
+
+ icon: LinkSvg,
+ order: 1,
+
+ getContents,
+ })
+
+ const Navigation = getNavigation()
+ Navigation.register(view)
+}