diff --git a/__mocks__/@nextcloud/router.js b/__mocks__/@nextcloud/router.js index 155319c0..e658ae9d 100644 --- a/__mocks__/@nextcloud/router.js +++ b/__mocks__/@nextcloud/router.js @@ -1,5 +1,6 @@ -/** +/*! * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + export const generateRemoteUrl = (path) => `https://localhost/${path}` diff --git a/__tests__/dav/dav.spec.ts b/__tests__/dav/dav.spec.ts index 081f9f18..e29d13e0 100644 --- a/__tests__/dav/dav.spec.ts +++ b/__tests__/dav/dav.spec.ts @@ -54,6 +54,7 @@ describe('davResultToNode', () => { test('path does not contain root', () => { const node = davResultToNode(result) expect(node.basename).toBe(result.basename) + expect(node.displayname).toBe(result.props!.displayname) expect(node.extension).toBe('.md') expect(node.source).toBe('https://localhost/dav/files/test/New folder/Neue Textdatei.md') expect(node.root).toBe(davRootPath) @@ -85,6 +86,13 @@ describe('davResultToNode', () => { expect(node.dirname).toBe('/New folder') }) + test('has correct displayname set', () => { + const remoteResult = { ...result, filename: '/root/New folder/Neue Textdatei.md' } + const node = davResultToNode(remoteResult, '/root', 'http://example.com/dav') + expect(node.basename).toBe(remoteResult.basename) + expect(node.displayname).toBe(remoteResult.props!.displayname) + }) + // If owner-id is set, it will be used as owner test('has correct owner set', () => { vi.spyOn(auth, 'getCurrentUser').mockReturnValue({ uid: 'user1', displayName: 'User 1', isAdmin: false }) diff --git a/__tests__/files/node.spec.ts b/__tests__/files/node.spec.ts index c69877c7..5bd61dce 100644 --- a/__tests__/files/node.spec.ts +++ b/__tests__/files/node.spec.ts @@ -208,6 +208,57 @@ describe('Permissions attribute', () => { }) }) +describe('Displayname attribute', () => { + test('legacy displayname attribute', () => { + // TODO: This logic can be removed with next major release (v4) + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + attributes: { + displayname: 'image.png', + }, + }) + expect(file.basename).toBe('picture.jpg') + expect(file.displayname).toBe('image.png') + expect(file.attributes.displayname).toBe('image.png') + }) + + test('Read displayname attribute', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + displayname: 'image.png', + }) + expect(file.basename).toBe('picture.jpg') + expect(file.displayname).toBe('image.png') + }) + + test('Fallback displayname attribute', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + }) + expect(file.basename).toBe('picture.jpg') + expect(file.displayname).toBe('picture.jpg') + }) + + test('Set displayname attribute', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + }) + expect(file.basename).toBe('picture.jpg') + expect(file.displayname).toBe('picture.jpg') + + file.displayname = 'image.png' + expect(file.displayname).toBe('image.png') + }) +}) + describe('Sanity checks', () => { test('Invalid id', () => { expect(() => new File({ @@ -237,6 +288,15 @@ describe('Sanity checks', () => { })).toThrowError('Invalid source format, only http(s) is supported') }) + test('Invalid displayname', () => { + expect(() => new File({ + source: 'https://cloud.domain.com/remote.php/dav/Photos', + mime: 'image', + displayname: true as unknown as string, + owner: 'emma', + })).toThrowError('Invalid displayname type') + }) + test('Invalid mtime', () => { expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/Photos', @@ -529,19 +589,17 @@ describe('Move and rename of a node', () => { test('Move updates the fallback displayname', () => { const file = new File({ source: 'https://cloud.example.com/dav/files/images/emma.jpeg', + displayname: 'emma.jpeg', mime: 'image/jpeg', owner: 'emma', root: '/dav', - attributes: { - displayname: 'emma.jpeg', - }, }) expect(file.path).toBe('/files/images/emma.jpeg') - expect(file.attributes.displayname).toBe('emma.jpeg') + expect(file.displayname).toBe('emma.jpeg') file.move('https://cloud.example.com/dav/files/pictures/jane.jpeg') expect(file.path).toBe('/files/pictures/jane.jpeg') - expect(file.attributes.displayname).toBe('jane.jpeg') + expect(file.displayname).toBe('jane.jpeg') }) test('Move does not updates custom displayname', () => { @@ -550,16 +608,14 @@ describe('Move and rename of a node', () => { mime: 'image/jpeg', owner: 'emma', root: '/dav', - attributes: { - displayname: 'profile.jpeg', - }, + displayname: 'profile.jpeg', }) expect(file.path).toBe('/files/images/emma.jpeg') - expect(file.attributes.displayname).toBe('profile.jpeg') + expect(file.displayname).toBe('profile.jpeg') file.move('https://cloud.example.com/dav/files/pictures/jane.jpeg') expect(file.path).toBe('/files/pictures/jane.jpeg') - expect(file.attributes.displayname).toBe('profile.jpeg') + expect(file.displayname).toBe('profile.jpeg') }) }) diff --git a/__tests__/utils/fileSorting.spec.ts b/__tests__/utils/fileSorting.spec.ts index 0f14ac0c..ce47b8af 100644 --- a/__tests__/utils/fileSorting.spec.ts +++ b/__tests__/utils/fileSorting.spec.ts @@ -71,11 +71,9 @@ describe('sortNodes', () => { mime: 'text/plain', // Resulting in name "d" source: 'https://cloud.domain.com/remote.php/dav/d', + displayname: 'a', mtime: new Date(100), size: 100, - attributes: { - displayname: 'a', - }, }), file('b', 100, 100), file('c', 100, 500), @@ -92,11 +90,9 @@ describe('sortNodes', () => { mime: 'text/plain', // Resulting in name "d" source: 'https://cloud.domain.com/remote.php/dav/c', + displayname: 'a', mtime: new Date(100), size: 100, - attributes: { - displayname: 'a', - }, }), // File with basename "b" but displayname "a" new File({ @@ -104,11 +100,9 @@ describe('sortNodes', () => { mime: 'text/plain', // Resulting in name "d" source: 'https://cloud.domain.com/remote.php/dav/b', + displayname: 'a', mtime: new Date(100), size: 100, - attributes: { - displayname: 'a', - }, }), ] diff --git a/lib/dav/dav.ts b/lib/dav/dav.ts index 5e831a7c..2f6ca335 100644 --- a/lib/dav/dav.ts +++ b/lib/dav/dav.ts @@ -179,6 +179,7 @@ export const davResultToNode = function(node: FileStat, filesRoot = davRootPath, source: `${remoteURL}${node.filename}`, mtime: new Date(Date.parse(node.lastmod)), mime: node.mime || 'application/octet-stream', + displayname: props.displayname, size: props?.size || Number.parseInt(props.getcontentlength || '0'), // The fileid is set to -1 for failed requests status: id < 0 ? NodeStatus.FAILED : undefined, diff --git a/lib/files/node.ts b/lib/files/node.ts index 6e72a7a8..611c7250 100644 --- a/lib/files/node.ts +++ b/lib/files/node.ts @@ -21,9 +21,13 @@ export enum NodeStatus { LOCKED = 'locked', } +interface NodeInternalData extends NodeData { + attributes: Attribute +} + export abstract class Node { - private _data: NodeData + private _data: NodeInternalData private _attributes: Attribute private _knownDavService = /(remote|public)\.php\/(web)?dav/i @@ -62,7 +66,12 @@ export abstract class Node { // Validate data validateData(data, davService || this._knownDavService) - this._data = { ...data, attributes: {} } + this._data = { + // TODO: Remove with next major release, this is just for compatibility + displayname: data.attributes?.displayname, + ...data, + attributes: {}, + } // Proxy the attributes to update the mtime on change this._attributes = new Proxy(this._data.attributes!, this.handler) @@ -102,6 +111,23 @@ export abstract class Node { return basename(this.source) } + /** + * The nodes displayname + * By default the display name and the `basename` are identical, + * but it is possible to have a different name. This happens + * on the files app for example for shared folders. + */ + get displayname(): string { + return this._data.displayname || this.basename + } + + /** + * Set the displayname + */ + set displayname(displayname: string) { + this._data.displayname = displayname + } + /** * Get this object's extension * There is no setter as the source is not meant to be changed manually. @@ -311,12 +337,11 @@ export abstract class Node { this._data.source = destination // Check if the displayname and the (old) basename were the same // meaning no special displayname was set but just a fallback to the basename by Nextclouds WebDAV server - if (this._attributes.displayname - && this._attributes.displayname === oldBasename + if (this.displayname === oldBasename && this.basename !== oldBasename) { // We have to assume that the displayname was not set but just a copy of the basename // this can not be guaranteed, so to be sure users should better refetch the node - this._attributes.displayname = this.basename + this.displayname = this.basename } this.updateMtime() } diff --git a/lib/files/nodeData.ts b/lib/files/nodeData.ts index 0c25c148..9099784c 100644 --- a/lib/files/nodeData.ts +++ b/lib/files/nodeData.ts @@ -39,6 +39,9 @@ export interface NodeData { /** The owner UID of this node */ owner: string|null + /** Optional the displayname of this node */ + displayname?: string + /** The node attributes */ attributes?: Attribute @@ -89,6 +92,10 @@ export const validateData = (data: NodeData, davService: RegExp) => { throw new Error('Invalid source format, only http(s) is supported') } + if (data.displayname && typeof data.displayname !== 'string') { + throw new Error('Invalid displayname type') + } + if (data.mtime && !(data.mtime instanceof Date)) { throw new Error('Invalid mtime type') }