diff --git a/CHANGELOG.md b/CHANGELOG.md index 189fecf4..570a01b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. * The support for Nextcloud versions below 30 has been removed, this means some functions like filename validation will now only work with the capabilities provided by Nextcloud 30 or newer. +* The Node API was changed, the `root` property of any node, + including `File` or `Folder`, is now required. #### DAV related export The DAV related exports from the main entry point were deprecated diff --git a/__tests__/files/cloning.spec.ts b/__tests__/files/cloning.spec.ts index ddc4eabb..14e2f037 100644 --- a/__tests__/files/cloning.spec.ts +++ b/__tests__/files/cloning.spec.ts @@ -39,6 +39,7 @@ describe('File cloning', () => { test('Clone preserves attributes', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', attributes: { @@ -59,6 +60,7 @@ describe('File cloning', () => { test('Clone is independent from original', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', size: 100, @@ -85,6 +87,7 @@ describe('File cloning', () => { test('Clone works with minimal file', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/file.txt', + root: '/files/emma', owner: 'emma', }) @@ -99,6 +102,7 @@ describe('File cloning', () => { test('Clone works with remote file', () => { const file = new File({ source: 'https://domain.com/Photos/picture.jpg', + root: '/', mime: 'image/jpeg', owner: null, }) @@ -145,6 +149,7 @@ describe('File serialization and deserialization', () => { test('toString and JSON.parse preserves attributes', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', attributes: { @@ -166,6 +171,7 @@ describe('File serialization and deserialization', () => { test('toString and JSON.parse works with minimal file', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/file.txt', + root: '/files/emma', owner: 'emma', }) @@ -181,6 +187,7 @@ describe('File serialization and deserialization', () => { test('toString and JSON.parse is independent from original', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', size: 100, @@ -206,6 +213,7 @@ describe('File serialization and deserialization', () => { test('toString and JSON.parse preserves displayname', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', displayname: 'My Vacation Photo', @@ -221,6 +229,7 @@ describe('File serialization and deserialization', () => { test('toString and JSON.parse works with remote file', () => { const file = new File({ source: 'https://domain.com/Photos/picture.jpg', + root: '/', mime: 'image/jpeg', owner: null, }) @@ -245,6 +254,7 @@ describe('File serialization and deserialization', () => { for (const status of statuses) { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/file.txt', + root: '/files/emma', owner: 'emma', status, }) @@ -258,6 +268,7 @@ describe('File serialization and deserialization', () => { test('toString output is valid JSON', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', size: 12345, diff --git a/__tests__/files/file.spec.ts b/__tests__/files/file.spec.ts index ae70d20a..633aeb47 100644 --- a/__tests__/files/file.spec.ts +++ b/__tests__/files/file.spec.ts @@ -11,6 +11,7 @@ describe('File creation', () => { test('Valid dav file', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma/Photos', mime: 'image/jpeg', owner: 'emma', mtime: new Date(Date.UTC(2023, 0, 1, 0, 0, 0)), @@ -45,9 +46,9 @@ describe('File creation', () => { test('Valid dav file with root', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', - root: '/files/emma', }) expect(file).toBeInstanceOf(File) @@ -72,6 +73,7 @@ describe('File creation', () => { test('Valid remote file', () => { const file = new File({ source: 'https://domain.com/Photos/picture.jpg', + root: '/', mime: 'image/jpeg', owner: null, }) @@ -89,7 +91,7 @@ describe('File creation', () => { expect(file.basename).toBe('picture.jpg') expect(file.extension).toBe('.jpg') expect(file.dirname).toBe('/Photos') - expect(file.root).toBeNull() + expect(file.root).toBe('/') expect(file.isDavResource).toBe(false) expect(file.permissions).toBe(Permission.READ) }) @@ -98,45 +100,48 @@ describe('File creation', () => { describe('File data change', () => { test('Rename a file', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) expect(file.basename).toBe('picture.jpg') expect(file.dirname).toBe('/') - expect(file.root).toBe('/files/emma/Photos') + expect(file.root).toBe('/files/emma') file.rename('picture-old.jpg') expect(file.basename).toBe('picture-old.jpg') expect(file.dirname).toBe('/') - expect(file.source).toBe('https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture-old.jpg') - expect(file.root).toBe('/files/emma/Photos') + expect(file.source).toBe('https://cloud.domain.com/remote.php/dav/files/emma/picture-old.jpg') + expect(file.root).toBe('/files/emma') }) test('Moving a file', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) expect(file.basename).toBe('picture.jpg') - expect(file.dirname).toBe('/') - expect(file.root).toBe('/files/emma/Photos') + expect(file.dirname).toBe('/Photos') + expect(file.root).toBe('/files/emma') file.move('https://cloud.domain.com/remote.php/dav/files/emma/Pictures/picture-old.jpg') expect(file.basename).toBe('picture-old.jpg') - expect(file.dirname).toBe('/') + expect(file.dirname).toBe('/Pictures') expect(file.source).toBe('https://cloud.domain.com/remote.php/dav/files/emma/Pictures/picture-old.jpg') - expect(file.root).toBe('/files/emma/Pictures') + expect(file.root).toBe('/files/emma') }) test('Moving a file to an invalid destination throws', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime: new Date(Date.UTC(2023, 0, 1, 0, 0, 0)), @@ -184,6 +189,7 @@ describe('Altering attributes does NOT updates mtime', () => { test('mtime is updated on existing attribute', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime: new Date(Date.UTC(1990, 0, 1, 0, 0, 0)), @@ -202,6 +208,7 @@ describe('Altering attributes does NOT updates mtime', () => { test('mtime is NOT updated on new attribute', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime: new Date(Date.UTC(1990, 0, 1, 0, 0, 0)), @@ -217,6 +224,7 @@ describe('Altering attributes does NOT updates mtime', () => { test('mtime is NOT updated on deleted attribute', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime: new Date(Date.UTC(1990, 0, 1, 0, 0, 0)), @@ -235,6 +243,7 @@ describe('Altering attributes does NOT updates mtime', () => { test('mtime is NOT updated if not initially defined', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', permissions: Permission.READ, @@ -254,6 +263,7 @@ describe('Altering top-level properties updates mtime', () => { test('mtime is updated on permissions change', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime: new Date(Date.UTC(1990, 0, 1, 0, 0, 0)), @@ -271,6 +281,7 @@ describe('Altering top-level properties updates mtime', () => { test('mtime is updated on size change', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime: new Date(Date.UTC(1990, 0, 1, 0, 0, 0)), diff --git a/__tests__/files/folder.spec.ts b/__tests__/files/folder.spec.ts index b4977754..df58ea1b 100644 --- a/__tests__/files/folder.spec.ts +++ b/__tests__/files/folder.spec.ts @@ -12,6 +12,7 @@ describe('Folder creation', () => { test('Valid dav folder', () => { const folder = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/', + root: '/files/emma', owner: 'emma', }) @@ -61,6 +62,7 @@ describe('Folder creation', () => { test('Valid remote folder', () => { const folder = new Folder({ source: 'https://domain.com/Photos/', + root: '/', owner: null, }) @@ -77,7 +79,7 @@ describe('Folder creation', () => { expect(folder.basename).toBe('Photos') expect(folder.extension).toBeNull() expect(folder.dirname).toBe('/') - expect(folder.root).toBeNull() + expect(folder.root).toBe('/') expect(folder.isDavResource).toBe(false) expect(folder.permissions).toBe(Permission.READ) }) @@ -87,6 +89,7 @@ describe('Folder data change', () => { test('Rename a folder', () => { const folder = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos', + root: '/files/emma', owner: 'emma', }) @@ -105,6 +108,7 @@ describe('Folder data change', () => { test('Moving a folder', () => { const folder = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/', + root: '/files/emma', owner: 'emma', }) diff --git a/__tests__/files/node.spec.ts b/__tests__/files/node.spec.ts index 6a0608f2..029937a6 100644 --- a/__tests__/files/node.spec.ts +++ b/__tests__/files/node.spec.ts @@ -10,18 +10,10 @@ import { File, Folder, NodeStatus } from '../../lib/node/index.ts' import { Permission } from '../../lib/permissions.ts' describe('Node testing', () => { - test('Root null fallback', () => { - const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', - mime: 'image/jpeg', - owner: 'emma', - }) - expect(file.root).toBeNull() - }) - test('Remove source ending slash', () => { const file = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/', + root: '/files/emma', owner: 'emma', }) expect(file.source).toBe('https://cloud.domain.com/remote.php/dav/files/emma/Photos') @@ -30,6 +22,7 @@ describe('Node testing', () => { test('Invalid rename', () => { const file = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/', + root: '/files/emma', owner: 'emma', }) expect(() => file.rename('new/folder')).toThrowError('Invalid basename') @@ -39,7 +32,8 @@ describe('Node testing', () => { describe('FileId attribute', () => { test('FileId undefined', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -48,7 +42,8 @@ describe('FileId attribute', () => { test('FileId definition', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', id: 1234, @@ -59,7 +54,8 @@ describe('FileId attribute', () => { // Mostly used when a node is unavailable test('FileId negative', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', id: -1234, @@ -70,6 +66,7 @@ describe('FileId attribute', () => { test('FileId attributes no fallback', () => { const file = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', attributes: { @@ -83,7 +80,8 @@ describe('FileId attribute', () => { describe('Mime attribute', () => { test('Mime definition', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -92,7 +90,8 @@ describe('Mime attribute', () => { test('Default mime', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', owner: 'emma', }) expect(file.mime).toBe('application/octet-stream') @@ -100,7 +99,8 @@ describe('Mime attribute', () => { test('Changing mime', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -112,7 +112,8 @@ describe('Mime attribute', () => { test('Removing mime', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -127,7 +128,8 @@ describe('Mtime attribute', () => { test('Mtime definition', () => { const mtime = new Date() const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime, @@ -138,7 +140,8 @@ describe('Mtime attribute', () => { test('Mtime manual update', async () => { const mtime = new Date() const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime, @@ -159,7 +162,8 @@ describe('Mtime attribute', () => { test('Mtime method update', async () => { const mtime = new Date() const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', mtime, @@ -181,7 +185,8 @@ describe('Mtime attribute', () => { describe('Size attribute', () => { test('Size definition', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', size: 1234, @@ -192,7 +197,8 @@ describe('Size attribute', () => { test('Size update', async () => { const mtime = new Date() const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', size: 1234, @@ -217,7 +223,8 @@ describe('Size attribute', () => { describe('Permissions attribute', () => { test('Permissions definition', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', permissions: Permission.READ | Permission.UPDATE | Permission.CREATE | Permission.DELETE, @@ -228,7 +235,8 @@ describe('Permissions attribute', () => { test('Permissions update', async () => { const mtime = new Date() const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', permissions: Permission.READ, @@ -253,7 +261,8 @@ describe('Permissions attribute', () => { describe('Displayname attribute', () => { test('Read displayname attribute', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', displayname: 'image.png', @@ -264,7 +273,8 @@ describe('Displayname attribute', () => { test('Fallback displayname attribute', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -274,7 +284,8 @@ describe('Displayname attribute', () => { test('Set displayname attribute', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -290,6 +301,7 @@ describe('Sanity checks', () => { test('Invalid id', () => { expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', id: '1234' as unknown as number, @@ -299,16 +311,19 @@ describe('Sanity checks', () => { test('Invalid source', () => { expect(() => new File({} as unknown as NodeData)).toThrowError('Missing mandatory source') expect(() => new Folder({ - source: 'cloud.domain.com/remote.php/dav/Photos', + source: 'cloud.domain.com/remote.php/dav/files/emma/Photos', + root: '/files/emma', owner: 'emma', })).toThrowError('Invalid source') expect(() => new File({ source: '/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', })).toThrowError('Invalid source format, source must be a valid URL') expect(() => new File({ source: 'ftp://remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', })).toThrowError('Invalid source format, only http(s) is supported') @@ -316,13 +331,15 @@ describe('Sanity checks', () => { test('Invalid displayname', () => { expect(() => new Folder({ - source: 'https://cloud.domain.com/remote.php/dav/Photos', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos', + root: '/files/emma', displayname: true as unknown as string, owner: 'emma', })).toThrowError('Invalid displayname type') const file = new Folder({ - source: 'https://cloud.domain.com/remote.php/dav/Photos', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos', + root: '/files/emma', displayname: 'test', owner: 'emma', }) @@ -332,13 +349,15 @@ describe('Sanity checks', () => { test('Invalid mtime', () => { expect(() => new File({ - source: 'https://cloud.domain.com/remote.php/dav/Photos/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', owner: 'emma', mtime: 'invalid' as unknown as Date, })).toThrowError('Invalid mtime type') const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/Photos/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', owner: 'emma', }) // @ts-expect-error wrong type error check @@ -347,9 +366,10 @@ describe('Sanity checks', () => { test('Invalid crtime', () => { expect(() => new File({ - source: 'https://cloud.domain.com/remote.php/dav/Photos/picture.jpg', + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', owner: 'emma', + root: '/files/emma', crtime: 'invalid' as unknown as Date, })).toThrowError('Invalid crtime type') }) @@ -359,10 +379,12 @@ describe('Sanity checks', () => { source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image', owner: 'emma', + root: '/files/emma', })).toThrowError('Missing or invalid mandatory mime') const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -374,6 +396,7 @@ describe('Sanity checks', () => { test('Invalid attributes', () => { expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', attributes: 'test' as unknown as Attribute, @@ -383,6 +406,7 @@ describe('Sanity checks', () => { test('Invalid permissions', () => { expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', permissions: 324 as unknown as number, @@ -392,6 +416,7 @@ describe('Sanity checks', () => { test('Invalid size', () => { expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', size: 'test' as unknown as number, @@ -399,6 +424,7 @@ describe('Sanity checks', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -409,17 +435,26 @@ describe('Sanity checks', () => { test('Invalid owner', () => { expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: true as unknown as string, })).toThrowError('Invalid owner') }) test('Invalid root', () => { + // @ts-expect-error -- mock so we can test missing root + expect(() => new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + })).toThrowError('Missing mandatory root') + expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', owner: 'emma', - root: true as unknown as string, + // @ts-expect-error -- mock so we can test wrong type + root: true, })).toThrowError('Invalid root type') expect(() => new File({ @@ -447,6 +482,7 @@ describe('Sanity checks', () => { test('Invalid status', () => { expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', status: 'invalid' as unknown as NodeStatus, @@ -454,6 +490,7 @@ describe('Sanity checks', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', status: NodeStatus.LOCKED, @@ -467,6 +504,7 @@ describe('Dav service detection', () => { test('Known dav services', () => { const file1 = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -474,6 +512,7 @@ describe('Dav service detection', () => { const file2 = new File({ source: 'https://cloud.domain.com/remote.php/webdav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -481,6 +520,7 @@ describe('Dav service detection', () => { const file3 = new File({ source: 'https://cloud.domain.com/public.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -488,6 +528,7 @@ describe('Dav service detection', () => { const file4 = new File({ source: 'https://cloud.domain.com/public.php/webdav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -497,6 +538,7 @@ describe('Dav service detection', () => { test('Custom dav service', () => { const file1 = new File({ source: 'https://cloud.domain.com/test.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }, /test\.php\/dav/) @@ -504,6 +546,7 @@ describe('Dav service detection', () => { const file2 = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }, /test\.php\/dav/) @@ -515,6 +558,7 @@ describe('Permissions handling', () => { test('Default permissions', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -524,6 +568,7 @@ describe('Permissions handling', () => { test('Custom permissions', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', permissions: Permission.READ | Permission.UPDATE | Permission.CREATE | Permission.DELETE | Permission.SHARE, @@ -533,16 +578,6 @@ describe('Permissions handling', () => { }) describe('Root and paths detection', () => { - test('Unknown root', () => { - const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', - mime: 'image/jpeg', - owner: 'emma', - }) - expect(file.root).toBe('/files/emma/Photos') - expect(file.dirname).toBe('/') - }) - test('Provided root dav service', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', @@ -609,18 +644,6 @@ describe('Root and paths detection', () => { expect(file.dirname).toBe('/files') expect(file.path).toBe('/files/emma.jpeg') }) - - test('Non dav ressource with undefined root', () => { - const file = new File({ - source: 'https://domain.com/files/images/emma.jpeg', - mime: 'image/jpeg', - owner: 'emma', - }) - expect(file.isDavResource).toBe(false) - expect(file.root).toBe(null) - expect(file.dirname).toBe('/files/images') - expect(file.path).toBe('/files/images/emma.jpeg') - }) }) describe('Move and rename of a node', () => { @@ -687,6 +710,7 @@ describe('Undefined properties are allowed', () => { test('File', () => { expect(() => new File({ source: 'https://domain.com/files/images/emma.jpeg', + root: '/files', owner: 'emma', id: undefined, mtime: undefined, @@ -696,13 +720,13 @@ describe('Undefined properties are allowed', () => { size: undefined, permissions: undefined, attributes: undefined, - root: undefined, })).not.toThrow() }) test('Folder', () => { expect(() => new Folder({ source: 'https://domain.com/files/images/', + root: '/files', owner: 'emma', id: undefined, mtime: undefined, @@ -711,7 +735,6 @@ describe('Undefined properties are allowed', () => { size: undefined, permissions: undefined, attributes: undefined, - root: undefined, })).not.toThrow() }) }) @@ -734,6 +757,7 @@ describe('Status handling', () => { test('Update status', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', }) @@ -746,6 +770,7 @@ describe('Status handling', () => { test('Clear status', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', status: NodeStatus.LOCKED, @@ -761,6 +786,7 @@ describe('Attributes update', () => { test('Update attributes', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', attributes: { @@ -788,6 +814,7 @@ describe('Attributes update', () => { test('Update attributes with protected getters', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', size: 9999, @@ -813,6 +840,7 @@ describe('Attributes update', () => { test('Changing a protected attributes is not possible', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + root: '/files/emma', mime: 'image/jpeg', owner: 'emma', attributes: { diff --git a/__tests__/newFileMenu.spec.ts b/__tests__/newFileMenu.spec.ts index 85c7abe5..3e10066a 100644 --- a/__tests__/newFileMenu.spec.ts +++ b/__tests__/newFileMenu.spec.ts @@ -312,10 +312,11 @@ describe('NewMenu getEntries filter', () => { newFileMenu.registerEntry(entry2) const context = new Folder({ + source: 'https://example.com/remote.php/dav/files/admin/Folder', + root: '/files/admin', id: 56, owner: 'admin', size: 2610077102, - source: 'https://example.com/remote.php/dav/files/admin/Folder', permissions: Permission.ALL, }) @@ -348,10 +349,11 @@ describe('NewMenu getEntries filter', () => { newFileMenu.registerEntry(entry2) const context = new Folder({ + source: 'https://example.com/remote.php/dav/files/admin/Folder', + root: '/files/admin', id: 56, owner: 'admin', size: 2610077102, - source: 'https://example.com/remote.php/dav/files/admin/Folder', permissions: Permission.READ, }) diff --git a/__tests__/utils/fileSorting.spec.ts b/__tests__/utils/fileSorting.spec.ts index cf3c983b..c546384f 100644 --- a/__tests__/utils/fileSorting.spec.ts +++ b/__tests__/utils/fileSorting.spec.ts @@ -4,11 +4,12 @@ */ import type { Attribute } from '../../lib/node/index.ts' -import { ArgumentsType, describe, expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { File, FilesSortingMode, Folder, sortNodes as originalSortNodes } from '../../lib' const file = (name: string, size?: number, modified?: number, favorite = false, attributes: Attribute = {}) => new File({ - source: `https://cloud.domain.com/remote.php/dav/${name}`, + source: `https://cloud.domain.com/remote.php/dav/files/emma/${name}`, + root: '/files/emma', mime: 'text/plain', owner: 'jdoe', mtime: new Date(modified ?? Date.now()), @@ -21,7 +22,8 @@ const file = (name: string, size?: number, modified?: number, favorite = false, }) const folder = (name: string, size?: number, modified?: number, favorite = false) => new Folder({ - source: `https://cloud.domain.com/remote.php/dav/${name}`, + source: `https://cloud.domain.com/remote.php/dav/files/emma/${name}`, + root: '/files/emma', owner: 'jdoe', mtime: new Date(modified ?? Date.now()), size, @@ -32,7 +34,7 @@ const folder = (name: string, size?: number, modified?: number, favorite = false : undefined, }) -const sortNodes = (...args: ArgumentsType) => originalSortNodes(...args).map((node) => node.basename) +const sortNodes = (...args: Parameters) => originalSortNodes(...args).map((node) => node.basename) describe('sortNodes', () => { test('By default files are sorted by name', () => { @@ -89,7 +91,8 @@ describe('sortNodes', () => { owner: 'jdoe', mime: 'text/plain', // Resulting in name "d" - source: 'https://cloud.domain.com/remote.php/dav/d', + source: 'https://cloud.domain.com/remote.php/dav/files/jdoe/d', + root: '/files/jdoe', displayname: 'a', mtime: new Date(100), size: 100, @@ -108,7 +111,8 @@ describe('sortNodes', () => { owner: 'jdoe', mime: 'text/plain', // Resulting in name "d" - source: 'https://cloud.domain.com/remote.php/dav/c', + source: 'https://cloud.domain.com/remote.php/dav/files/jdoe/c', + root: '/files/jdoe', displayname: 'a', mtime: new Date(100), size: 100, @@ -118,7 +122,8 @@ describe('sortNodes', () => { owner: 'jdoe', mime: 'text/plain', // Resulting in name "d" - source: 'https://cloud.domain.com/remote.php/dav/b', + source: 'https://cloud.domain.com/remote.php/dav/files/jdoe/b', + root: '/files/jdoe', displayname: 'a', mtime: new Date(100), size: 100, diff --git a/__tests__/view.spec.ts b/__tests__/view.spec.ts index b6348323..c990efbf 100644 --- a/__tests__/view.spec.ts +++ b/__tests__/view.spec.ts @@ -161,7 +161,11 @@ describe('Invalid View creation', () => { describe('View creation', () => { test('Create a View', async () => { - const folder = new Folder({ source: 'https://example.org', owner: 'admin' }) + const folder = new Folder({ + source: 'https://example.org/dav/files/admin/', + root: '/files/admin', + owner: 'admin', + }) const view = new View({ id: 'test', name: 'Test', diff --git a/lib/node/node.ts b/lib/node/node.ts index 1c1c4f39..9247a19d 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -142,23 +142,7 @@ export abstract class Node { * You can use the rename or move method to change the source. */ get dirname(): string { - if (this.root) { - let source = this.source - if (this.isDavResource) { - // ensure we only work on the real path in case root is not distinct - source = source.split(this._knownDavService).pop()! - } - // Using replace would remove all part matching root - const firstMatch = source.indexOf(this.root) - // Ensure we do not remove the leading slash - const root = this.root.replace(/\/$/, '') - return dirname(source.slice(firstMatch + root.length) || '/') - } - - // This should always be a valid URL - // as this is tested in the constructor - const url = new URL(this.source) - return dirname(url.pathname) + return dirname(this.path) } /** @@ -285,38 +269,26 @@ export abstract class Node { * Get the dav root of this object * There is no setter as the root is not meant to be changed */ - get root(): string|null { - // If provided (recommended), use the root and strip away the ending slash - if (this._data.root) { - return this._data.root.replace(/^(.+)\/$/, '$1') - } - - // Use the source to get the root from the dav service - if (this.isDavResource) { - const root = dirname(this.source) - return root.split(this._knownDavService).pop() || null - } - - return null + get root(): string { + return this._data.root.replace(/^(.+)\/$/, '$1') } /** * Get the absolute path of this object relative to the root */ get path(): string { - if (this.root) { - let source = this.source - if (this.isDavResource) { - // ensure we only work on the real path in case root is not distinct - source = source.split(this._knownDavService).pop()! - } - // Using replace would remove all part matching root - const firstMatch = source.indexOf(this.root) - // Ensure we do not remove the leading slash - const root = this.root.replace(/\/$/, '') - return source.slice(firstMatch + root.length) || '/' + const url = new URL(this.source) + let source = decodeURI(url.pathname) + + if (this.isDavResource) { + // ensure we only work on the real path in case root is not distinct + source = source.split(this._knownDavService).pop()! } - return (this.dirname + '/' + this.basename).replace(/\/\//g, '/') + // Using replace would remove all part matching root + const firstMatch = source.indexOf(this.root) + // Ensure we do not remove the leading slash + const root = this.root.replace(/\/$/, '') + return source.slice(firstMatch + root.length) || '/' } /** diff --git a/lib/node/nodeData.ts b/lib/node/nodeData.ts index 17f5359d..0ae0f139 100644 --- a/lib/node/nodeData.ts +++ b/lib/node/nodeData.ts @@ -12,9 +12,6 @@ import { NodeStatus } from './node' export interface Attribute { [key: string]: any } export interface NodeData { - /** Unique ID */ - id?: number - /** * URL to the resource. * e.g. https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg @@ -22,6 +19,16 @@ export interface NodeData { */ source: string + /** + * The absolute root of the home relative to the service. + * It is highly recommended to provide that information. + * e.g. /files/emma + */ + root: string + + /** Unique ID */ + id?: number + /** Last modified time */ mtime?: Date @@ -46,13 +53,6 @@ export interface NodeData { /** The node attributes */ attributes?: Attribute - /** - * The absolute root of the home relative to the service. - * It is highly recommended to provide that information. - * e.g. /files/emma - */ - root?: string - /** The node status */ status?: NodeStatus } @@ -73,7 +73,7 @@ export const isDavResource = function(source: string, davService: RegExp): boole * @param data The node data * @param davService Pattern to check if source is DAV ressource */ -export const validateData = (data: NodeData, davService: RegExp) => { +export function validateData(data: NodeData, davService: RegExp) { if (data.id && typeof data.id !== 'number') { throw new Error('Invalid id type of value') } @@ -93,6 +93,29 @@ export const validateData = (data: NodeData, davService: RegExp) => { throw new Error('Invalid source format, only http(s) is supported') } + if (!data.root) { + throw new Error('Missing mandatory root') + } + + if (typeof data.root !== 'string') { + throw new Error('Invalid root type') + } + + if (!data.root.startsWith('/')) { + throw new Error('Root must start with a leading slash') + } + + if (!data.source.includes(data.root)) { + throw new Error('Root must be part of the source') + } + + if (isDavResource(data.source, davService)) { + const service = data.source.match(davService)![0] + if (!data.source.includes(join(service, data.root))) { + throw new Error('The root must be relative to the service. e.g /files/emma') + } + } + if (data.displayname && typeof data.displayname !== 'string') { throw new Error('Invalid displayname type') } @@ -135,25 +158,6 @@ export const validateData = (data: NodeData, davService: RegExp) => { throw new Error('Invalid attributes type') } - if (data.root && typeof data.root !== 'string') { - throw new Error('Invalid root type') - } - - if (data.root && !data.root.startsWith('/')) { - throw new Error('Root must start with a leading slash') - } - - if (data.root && !data.source.includes(data.root)) { - throw new Error('Root must be part of the source') - } - - if (data.root && isDavResource(data.source, davService)) { - const service = data.source.match(davService)![0] - if (!data.source.includes(join(service, data.root))) { - throw new Error('The root must be relative to the service. e.g /files/emma') - } - } - if (data.status && !Object.values(NodeStatus).includes(data.status)) { throw new Error('Status must be a valid NodeStatus') }