summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/files/lib/Controller/ViewController.php1
-rw-r--r--apps/files/src/actions/sidebarAction.ts2
-rw-r--r--apps/files/src/main.ts4
-rw-r--r--apps/files/src/services/DavProperties.ts128
-rw-r--r--apps/files/src/services/Favorites.ts100
-rw-r--r--apps/files/src/services/WebdavClient.ts51
-rw-r--r--apps/files/src/views/favorites.ts114
-rw-r--r--apps/files_trashbin/lib/Controller/PreviewController.php2
-rw-r--r--apps/files_trashbin/src/services/trashbin.ts16
-rw-r--r--apps/files_trashbin/tests/Controller/PreviewControllerTest.php2
10 files changed, 406 insertions, 14 deletions
diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php
index 70e0fd48456..149ce242502 100644
--- a/apps/files/lib/Controller/ViewController.php
+++ b/apps/files/lib/Controller/ViewController.php
@@ -253,6 +253,7 @@ class ViewController extends Controller {
$this->initialState->provideInitialState('navigation', $navItems);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
+ $this->initialState->provideInitialState('favoriteFolders', $favElements['folders'] ?? []);
// File sorting user config
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts
index 4766d2e90df..09a18480790 100644
--- a/apps/files/src/actions/sidebarAction.ts
+++ b/apps/files/src/actions/sidebarAction.ts
@@ -30,7 +30,7 @@ export const ACTION_DETAILS = 'details'
export const action = new FileAction({
id: ACTION_DETAILS,
- displayName: () => t('files', 'Details'),
+ displayName: () => t('files', 'Open details'),
iconSvgInline: () => InformationSvg,
// Sidebar currently supports user folder only, /files/USER
diff --git a/apps/files/src/main.ts b/apps/files/src/main.ts
index 1d96c2f6eaa..be04f559372 100644
--- a/apps/files/src/main.ts
+++ b/apps/files/src/main.ts
@@ -1,6 +1,8 @@
import './templates.js'
import './legacy/filelistSearch.js'
+
import './actions/deleteAction'
+import './actions/favoriteAction'
import './actions/openFolderAction'
import './actions/sidebarAction'
@@ -11,6 +13,7 @@ import FilesListView from './views/FilesList.vue'
import NavigationService from './services/Navigation'
import NavigationView from './views/Navigation.vue'
import processLegacyFilesViews from './legacy/navigationMapper.js'
+import registerFavoritesView from './views/favorites'
import registerPreviewServiceWorker from './services/ServiceWorker.js'
import router from './router/router.js'
import RouterService from './services/RouterService'
@@ -70,6 +73,7 @@ FilesList.$mount('#app-content-vue')
// Init legacy and new files views
processLegacyFilesViews()
+registerFavoritesView()
// Register preview service worker
registerPreviewServiceWorker()
diff --git a/apps/files/src/services/DavProperties.ts b/apps/files/src/services/DavProperties.ts
new file mode 100644
index 00000000000..598807511ca
--- /dev/null
+++ b/apps/files/src/services/DavProperties.ts
@@ -0,0 +1,128 @@
+/**
+ * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import logger from '../logger'
+
+type DavProperty = { [key: string]: string }
+
+declare global {
+ interface Window {
+ OC: any;
+ _nc_dav_properties: string[];
+ _nc_dav_namespaces: DavProperty;
+ }
+}
+
+const defaultDavProperties = [
+ 'd:getcontentlength',
+ 'd:getcontenttype',
+ 'd:getetag',
+ 'd:getlastmodified',
+ 'd:quota-available-bytes',
+ 'd:resourcetype',
+ 'nc:has-preview',
+ 'nc:is-encrypted',
+ 'nc:mount-type',
+ 'nc:share-attributes',
+ 'oc:comments-unread',
+ 'oc:favorite',
+ 'oc:fileid',
+ 'oc:owner-display-name',
+ 'oc:owner-id',
+ 'oc:permissions',
+ 'oc:share-types',
+ 'oc:size',
+ 'ocs:share-permissions',
+]
+
+const defaultDavNamespaces = {
+ d: 'DAV:',
+ nc: 'http://nextcloud.org/ns',
+ oc: 'http://owncloud.org/ns',
+ ocs: 'http://open-collaboration-services.org/ns',
+}
+
+/**
+ * TODO: remove and move to @nextcloud/files
+ */
+export const registerDavProperty = function(prop: string, namespace: DavProperty = { nc: 'http://nextcloud.org/ns' }): void {
+ if (typeof window._nc_dav_properties === 'undefined') {
+ window._nc_dav_properties = defaultDavProperties
+ window._nc_dav_namespaces = defaultDavNamespaces
+ }
+
+ const namespaces = { ...window._nc_dav_namespaces, ...namespace }
+
+ // Check duplicates
+ if (window._nc_dav_properties.find(search => search === prop)) {
+ logger.error(`${prop} already registered`, { prop })
+ return
+ }
+
+ if (prop.startsWith('<') || prop.split(':').length !== 2) {
+ logger.error(`${prop} is not valid. See example: 'oc:fileid'`, { prop })
+ return
+ }
+
+ const ns = prop.split(':')[0]
+ if (!namespaces[ns]) {
+ logger.error(`${prop} namespace unknown`, { prop, namespaces })
+ return
+ }
+
+ window._nc_dav_properties.push(prop)
+ window._nc_dav_namespaces = namespaces
+}
+
+/**
+ * Get the registered dav properties
+ */
+export const getDavProperties = function(): string {
+ if (typeof window._nc_dav_properties === 'undefined') {
+ window._nc_dav_properties = defaultDavProperties
+ }
+
+ return window._nc_dav_properties.map(prop => `<${prop} />`).join(' ')
+}
+
+/**
+ * Get the registered dav namespaces
+ */
+export const getDavNameSpaces = function(): string {
+ if (typeof window._nc_dav_namespaces === 'undefined') {
+ window._nc_dav_namespaces = defaultDavNamespaces
+ }
+
+ return Object.keys(window._nc_dav_namespaces).map(ns => `xmlns:${ns}="${window._nc_dav_namespaces[ns]}"`).join(' ')
+}
+
+/**
+ * Get the default PROPFIND request payload
+ */
+export const getDefaultPropfind = function() {
+ return `<?xml version="1.0"?>
+ <d:propfind ${getDavNameSpaces()}>
+ <d:prop>
+ ${getDavProperties()}
+ </d:prop>
+ </d:propfind>`
+}
diff --git a/apps/files/src/services/Favorites.ts b/apps/files/src/services/Favorites.ts
new file mode 100644
index 00000000000..3837bb221b5
--- /dev/null
+++ b/apps/files/src/services/Favorites.ts
@@ -0,0 +1,100 @@
+/**
+ * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+import { File, Folder, parseWebdavPermissions } from '@nextcloud/files'
+import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
+import { getClient, rootPath } from './WebdavClient'
+import { getCurrentUser } from '@nextcloud/auth'
+import { getDavNameSpaces, getDavProperties, getDefaultPropfind } from './DavProperties'
+import type { ContentsWithRoot } from './Navigation'
+import type { FileStat, ResponseDataDetailed } from 'webdav'
+
+const client = getClient()
+
+const reportPayload = `<?xml version="1.0"?>
+<oc:filter-files ${getDavNameSpaces()}>
+ <d:prop>
+ ${getDavProperties()}
+ </d:prop>
+ <oc:filter-rules>
+ <oc:favorite>1</oc:favorite>
+ </oc:filter-rules>
+</oc:filter-files>`
+
+const resultToNode = function(node: FileStat): File | Folder {
+ const permissions = parseWebdavPermissions(node.props?.permissions)
+ const owner = getCurrentUser()?.uid as string
+ const previewUrl = generateUrl('/core/preview?fileId={fileid}&x=32&y=32&forceIcon=0', node.props)
+
+ const nodeData = {
+ id: node.props?.fileid as number || 0,
+ source: generateRemoteUrl('dav' + rootPath + node.filename),
+ mtime: new Date(node.lastmod),
+ mime: node.mime as string,
+ size: node.props?.size as number || 0,
+ permissions,
+ owner,
+ root: rootPath,
+ attributes: {
+ ...node,
+ ...node.props,
+ previewUrl,
+ },
+ }
+
+ delete nodeData.attributes.props
+
+ return node.type === 'file'
+ ? new File(nodeData)
+ : new Folder(nodeData)
+}
+
+export const getContents = async (path = '/'): Promise<ContentsWithRoot> => {
+ const propfindPayload = getDefaultPropfind()
+
+ // Get root folder
+ let rootResponse
+ if (path === '/') {
+ rootResponse = await client.stat(path, {
+ details: true,
+ data: getDefaultPropfind(),
+ }) as ResponseDataDetailed<FileStat>
+ }
+
+ const contentsResponse = await client.getDirectoryContents(path, {
+ details: true,
+ // Only filter favorites if we're at the root
+ data: path === '/' ? reportPayload : propfindPayload,
+ headers: {
+ // Patched in WebdavClient.ts
+ method: path === '/' ? 'REPORT' : 'PROPFIND',
+ },
+ includeSelf: true,
+ }) as ResponseDataDetailed<FileStat[]>
+
+ const root = rootResponse?.data || contentsResponse.data[0]
+ const contents = contentsResponse.data.filter(node => node.filename !== path)
+
+ return {
+ folder: resultToNode(root) as Folder,
+ contents: contents.map(resultToNode),
+ }
+}
diff --git a/apps/files/src/services/WebdavClient.ts b/apps/files/src/services/WebdavClient.ts
new file mode 100644
index 00000000000..0630e695afa
--- /dev/null
+++ b/apps/files/src/services/WebdavClient.ts
@@ -0,0 +1,51 @@
+/**
+ * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+import { createClient, getPatcher, RequestOptions } from 'webdav'
+import { request } from '../../../../node_modules/webdav/dist/node/request.js'
+import { generateRemoteUrl } from '@nextcloud/router'
+import { getCurrentUser, getRequestToken } from '@nextcloud/auth'
+
+export const rootPath = `/files/${getCurrentUser()?.uid}`
+export const defaultRootUrl = generateRemoteUrl('dav' + rootPath)
+
+export const getClient = (rootUrl = defaultRootUrl) => {
+ const client = createClient(rootUrl, {
+ headers: {
+ requesttoken: getRequestToken() || '',
+ },
+ })
+
+ /**
+ * Allow to override the METHOD to support dav REPORT
+ *
+ * @see https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/request.ts
+ */
+ const patcher = getPatcher()
+ patcher.patch('request', (options: RequestOptions) => {
+ if (options.headers?.method) {
+ options.method = options.headers.method
+ delete options.headers.method
+ }
+ return request(options)
+ })
+ return client
+}
diff --git a/apps/files/src/views/favorites.ts b/apps/files/src/views/favorites.ts
new file mode 100644
index 00000000000..731fbe14db7
--- /dev/null
+++ b/apps/files/src/views/favorites.ts
@@ -0,0 +1,114 @@
+/**
+ * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+import type NavigationService from '../services/Navigation.ts'
+import type { Navigation } from '../services/Navigation.ts'
+import { translate as t } from '@nextcloud/l10n'
+import StarSvg from '@mdi/svg/svg/star.svg?raw'
+import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
+
+import { getContents } from '../services/Favorites.ts'
+import { loadState } from '@nextcloud/initial-state'
+import { basename } from 'path'
+import { hashCode } from '../utils/hashUtils'
+import { subscribe } from '@nextcloud/event-bus'
+import { Node, FileType } from '@nextcloud/files'
+import logger from '../logger'
+
+const favoriteFolders = loadState('files', 'favoriteFolders', [])
+
+export default () => {
+ const Navigation = window.OCP.Files.Navigation as NavigationService
+ Navigation.register({
+ id: 'favorites',
+ name: t('files', 'Favorites'),
+ caption: t('files', 'List of favorites files and folders.'),
+
+ icon: StarSvg,
+ order: 5,
+
+ columns: [],
+
+ getContents,
+ } as Navigation)
+
+ favoriteFolders.forEach((folder) => {
+ Navigation.register(generateFolderView(folder))
+ })
+
+ /**
+ * Update favourites navigation when a new folder is added
+ */
+ subscribe('files:favorites:added', (node: Node) => {
+ if (node.type !== FileType.Folder) {
+ return
+ }
+
+ // Sanity check
+ if (node.path === null || !node.root?.startsWith('/files')) {
+ logger.error('Favorite folder is not within user files root', { node })
+ return
+ }
+
+ Navigation.register(generateFolderView(node.path))
+ })
+
+ /**
+ * Remove favourites navigation when a folder is removed
+ */
+ subscribe('files:favorites:removed', (node: Node) => {
+ if (node.type !== FileType.Folder) {
+ return
+ }
+
+ // Sanity check
+ if (node.path === null || !node.root?.startsWith('/files')) {
+ logger.error('Favorite folder is not within user files root', { node })
+ return
+ }
+
+ Navigation.remove(generateIdFromPath(node.path))
+ })
+}
+
+const generateFolderView = function(folder: string): Navigation {
+ return {
+ id: generateIdFromPath(folder),
+ name: basename(folder),
+
+ icon: FolderSvg,
+ order: -100, // always first
+ params: {
+ dir: folder,
+ view: 'favorites',
+ },
+
+ parent: 'favorites',
+
+ columns: [],
+
+ getContents,
+ } as Navigation
+}
+
+const generateIdFromPath = function(path: string): string {
+ return `favorite-${hashCode(path)}`
+}
diff --git a/apps/files_trashbin/lib/Controller/PreviewController.php b/apps/files_trashbin/lib/Controller/PreviewController.php
index 652570dccd7..9f60cc8b0b2 100644
--- a/apps/files_trashbin/lib/Controller/PreviewController.php
+++ b/apps/files_trashbin/lib/Controller/PreviewController.php
@@ -119,7 +119,7 @@ class PreviewController extends Controller {
$mimeType = $this->mimeTypeDetector->detectPath($file->getName());
}
- $f = $this->previewManager->getPreview($file, $x, $y, $a, IPreview::MODE_FILL, $mimeType);
+ $f = $this->previewManager->getPreview($file, $x, $y, !$a, IPreview::MODE_FILL, $mimeType);
$response = new Http\FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
// Cache previews for 24H
diff --git a/apps/files_trashbin/src/services/trashbin.ts b/apps/files_trashbin/src/services/trashbin.ts
index 9982750ba5c..ab48863317a 100644
--- a/apps/files_trashbin/src/services/trashbin.ts
+++ b/apps/files_trashbin/src/services/trashbin.ts
@@ -25,27 +25,19 @@ import { File, Folder, parseWebdavPermissions } from '@nextcloud/files'
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
import type { FileStat, ResponseDataDetailed } from 'webdav'
+import { getDavNameSpaces, getDavProperties } from '../../../files/src/services/DavProperties'
import type { ContentsWithRoot } from '../../../files/src/services/Navigation.ts'
import client, { rootPath } from './client'
const data = `<?xml version="1.0"?>
-<d:propfind xmlns:d="DAV:"
- xmlns:oc="http://owncloud.org/ns"
- xmlns:nc="http://nextcloud.org/ns">
+<d:propfind ${getDavNameSpaces()}>
<d:prop>
<nc:trashbin-filename />
<nc:trashbin-deletion-time />
<nc:trashbin-original-location />
<nc:trashbin-title />
- <d:getlastmodified />
- <d:getetag />
- <d:getcontenttype />
- <d:resourcetype />
- <oc:fileid />
- <oc:permissions />
- <oc:size />
- <d:getcontentlength />
+ ${getDavProperties()}
</d:prop>
</d:propfind>`
@@ -73,6 +65,8 @@ const resultToNode = function(node: FileStat): File | Folder {
},
}
+ delete nodeData.attributes.props
+
return node.type === 'file'
? new File(nodeData)
: new Folder(nodeData)
diff --git a/apps/files_trashbin/tests/Controller/PreviewControllerTest.php b/apps/files_trashbin/tests/Controller/PreviewControllerTest.php
index 4db3d4f613c..3b114484c7f 100644
--- a/apps/files_trashbin/tests/Controller/PreviewControllerTest.php
+++ b/apps/files_trashbin/tests/Controller/PreviewControllerTest.php
@@ -157,7 +157,7 @@ class PreviewControllerTest extends TestCase {
$this->overwriteService(ITimeFactory::class, $this->time);
- $res = $this->controller->getPreview(42, 10, 10, true);
+ $res = $this->controller->getPreview(42, 10, 10, false);
$expected = new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => 'previewMime']);
$expected->cacheFor(3600 * 24);