Skip to content

Commit 5bde7f8

Browse files
committed
Unified Move Function
The new `move` function that builds upon the unified `copy` and `remove` functions It avoids the unnecessary duplication of work that resulted from running copy and delete separately.
1 parent b14fabc commit 5bde7f8

File tree

3 files changed

+171
-14
lines changed

3 files changed

+171
-14
lines changed

src/SolidApi.js

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import errorUtils from './utils/errorUtils'
66
import linksUtils from './utils/linksUtils'
77

88
const fetchLog = debug('solid-file-client:fetch')
9-
const { getRootUrl, getParentUrl, getItemName, areFolders, areFiles, LINK } = apiUtils
9+
const { getRootUrl, getParentUrl, getItemName, areFolders, /* areFiles, */ LINK } = apiUtils
1010
const { FetchError, assertResponseOk, composedFetch, toFetchError } = errorUtils
1111
const { getLinksFromResponse } = linksUtils
1212
const { parseFolderResponse } = folderUtils
@@ -974,25 +974,94 @@ class SolidAPI {
974974
}
975975

976976
/**
977-
* Move a file (url ending with file name) or folder (url ending with "/").
978-
* Per default existing folders will be deleted before moving and links will be moved.
979-
* Shortcut for copying and deleting items
980-
* @param {string} from
977+
* Moves Folder contents.
978+
* @param {Promise<Response>} getResoponse
981979
* @param {string} to
982980
* @param {WriteOptions} [options]
983-
* @returns {Promise<Response[]>} Resolves with an array of creation (copy) responses.
981+
* @returns {Promise<Response[]>} Resolves to an array of responses to copy further or paste.
982+
* @private
983+
*/
984+
async _moveFolderContents (getResponse, to, options) {
985+
const from = getResponse.url
986+
987+
const { folders, files } = await parseFolderResponse(getResponse)
988+
const folderResponse = await this.createFolder(to, options)
989+
990+
await this._copyLinksForItem(getResponse, folderResponse, options)
991+
992+
const foldersCreation = folders.map(async ({ name }) => {
993+
const folderResp = await this.get(`${from}${name}/`, { headers: { Accept: 'text/turtle' } })
994+
return this._moveFolderContents(folderResp, `${to}${name}/`, options)
995+
})
996+
997+
const filesCreation = files.map(async ({ name }) => {
998+
let fileResp
999+
let copyResponse
1000+
try {
1001+
fileResp = await this.get(`${from}${name}`)
1002+
copyResponse = await this._pasteFile(fileResp, `${to}${name}`, options)
1003+
} catch (error) {
1004+
if (error.message.includes('already existed')) {
1005+
copyResponse = error // Don't throw when merge=KEEP_TARGET and it tried to overwrite a file
1006+
} else {
1007+
throw toFetchError(error)
1008+
}
1009+
}
1010+
await this._removeItemWithLinks(fileResp)
1011+
return copyResponse
1012+
})
1013+
1014+
const creationResults = await composedFetch([
1015+
...foldersCreation,
1016+
...filesCreation
1017+
]).then(responses => responses.filter(item => !(item instanceof FetchError)))
1018+
1019+
await this._removeItemWithLinks(getResponse)
1020+
1021+
return [folderResponse, ...creationResults] // Alternative to Array.prototype.flat
1022+
}
1023+
1024+
/**
1025+
* Move a file or folder.
1026+
* @param {string} from source
1027+
* @param {string} to destination
1028+
* @param {WriteOptions} [options]
1029+
* @returns {Promise<Response[]>} Resolves with an array of creation responses.
9841030
* The first one will be the folder specified by "to".
9851031
* If it is a folder, the others will be creation responses from the contents in arbitrary order.
9861032
*/
987-
move (from, to, options) {
988-
// TBD: Rewrite to detect folders not by url (ie remove areFolders)
989-
if (areFolders(from, to)) {
990-
return this.moveFolder(from, to, options)
1033+
async move (from, to, options) {
1034+
const moveOptions = {
1035+
...defaultWriteOptions,
1036+
...options
9911037
}
992-
if (areFiles(from, to)) {
993-
return this.moveFile(from, to, options)
1038+
_checkInputs('move', from, to)
1039+
1040+
let fromItem = await this.get(from)
1041+
// This check works only with a strict implementation of solid standards
1042+
// A bug in NSS prevents 'content-type' to be reported correctly in response to HEAD
1043+
// https://github.com/solid/node-solid-server/issues/454
1044+
// TBD: Obtain item type from the link header instead
1045+
const fromItemType = fromItem.url.endsWith('/') ? 'Container' : 'Resource'
1046+
1047+
if (fromItemType === 'Resource') {
1048+
if (to.endsWith('/')) {
1049+
throw toFetchError(new Error('May not move file to a folder'))
1050+
}
1051+
const copyResponse = await this._pasteFile(fromItem, to, moveOptions)
1052+
await this._removeItemWithLinks(fromItem)
1053+
return copyResponse
1054+
} else if (fromItemType === 'Container') {
1055+
// TBD: Add additional check to see if response can be converted to turtle
1056+
// and avoid this additional fetch.
1057+
if (fromItem.headers.get('content-type') !== 'text/turtle') {
1058+
fromItem = await this.get(fromItem.url, { headers: { Accept: 'text/turtle' } })
1059+
}
1060+
to = to.endsWith('/') ? to : `${to}/`
1061+
return this._moveFolderContents(fromItem, to, moveOptions)
1062+
} else {
1063+
throw toFetchError(new Error(`Unrecognized item type ${fromItemType}`))
9941064
}
995-
toFetchError(new Error('Cannot copy from a folder url to a file url or vice versa'))
9961065
}
9971066

9981067
/**
@@ -1042,7 +1111,7 @@ function catchError (callback) {
10421111
}
10431112

10441113
/**
1045-
* Check Input Arguments for Copy
1114+
* Check Input Arguments for Copy & Move
10461115
* Used for error messages
10471116
* @param {string} op operation to tailor the message to
10481117
* @param {any} from

tests/SolidApi.composed.test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,79 @@ describe('composed methods', () => {
359359
})
360360

361361
describe('move', () => {
362+
describe('unified move', () => {
363+
test('rejects if source and destination are not strings', async () => {
364+
const responses = api.move(null, null)
365+
await expect(responses).rejects.toThrowError('Invalid parameters: source and destination must be strings.')
366+
})
367+
test('rejects with 404 on inexistent item', () => {
368+
return rejectsWithStatuses(api.move(inexistentFile.url, filePlaceholder.url), [404])
369+
})
370+
test('rejects copying to self', async () => {
371+
const responses = api.move(parentFolder.url, parentFolder.url)
372+
await expect(responses).rejects.toThrowError('Invalid parameters: cannot move source to itself.')
373+
})
374+
test('rejects moving a folder to its child', async () => {
375+
const responses = api.move(parentFolder.url, folderPlaceholder.url)
376+
await expect(responses).rejects.toThrowError('Invalid parameters: cannot move source inside itself.')
377+
})
378+
test('resolves with 201 moving existing to inexistent file', async () => {
379+
const res = await api.move(childFile.url, filePlaceholder.url)
380+
expect(res).toHaveProperty('status', 201)
381+
expect(res).toHaveProperty('url', filePlaceholder.url)
382+
})
383+
test('resolves with 201 moving empty folder to placeholder', async () => {
384+
const responses = await api.move(emptyFolder.url, folderPlaceholder.url)
385+
expect(responses).toHaveLength(1)
386+
expect(responses[0]).toHaveProperty('status', 201)
387+
expect(responses[0]).toHaveProperty('url', apiUtils.getParentUrl(folderPlaceholder.url))
388+
})
389+
test('resolves with 201 moving a folder with depth 2 to placeholder', async () => {
390+
const responses = await api.move(parentFolder.url, parentPlaceholder.url)
391+
expect(responses).toHaveLength(parentFolder.contents.length + 1)
392+
expect(responses[0]).toHaveProperty('status', 201)
393+
expect(responses[0]).toHaveProperty('url', apiUtils.getParentUrl(parentPlaceholder.url))
394+
})
395+
test('resolves moving existing to existing file', () => {
396+
return expect(api.move(childFile.url, parentFile.url)).resolves.toBeDefined()
397+
})
398+
test('resolves moving folder with depth 1 to folder with depth 1', () => {
399+
return expect(api.move(childTwo.url, childOne.url)).resolves.toBeDefined()
400+
})
401+
402+
test('resolves moving folder to existing folder with similar contents with merge=KEEP_TARGET', async () => {
403+
await expect(api.move(childTwo.url, childOne.url, { merge: MERGE.KEEP_TARGET })).resolves.toBeDefined()
404+
await expect(api.itemExists(childTwo.url)).resolves.toBe(false)
405+
})
406+
407+
test('rejects moving existing to existing file with merge=KEEP_TARGET', async () => {
408+
await expect(api.move(childFile.url, parentFile.url, { merge: MERGE.KEEP_TARGET })).rejects.toThrowError('already existed')
409+
await expect(api.itemExists(childFile.url)).resolves.toBe(true)
410+
})
411+
test('overwrites new location and deletes old one', async () => {
412+
await expect(api.move(childFile.url, parentFile.url)).resolves.toBeDefined()
413+
414+
await expect(api.itemExists(childFile.url)).resolves.toBe(false)
415+
await expect(api.itemExists(parentFile.url)).resolves.toBe(true)
416+
const res = await api.get(parentFile.url)
417+
const content = await res.text()
418+
await expect(content).toEqual(childFile.content)
419+
})
420+
421+
test('overwrites new folder contents and deletes old one', async () => {
422+
await expect(api.move(childOne.url, childTwo.url)).resolves.toBeDefined()
423+
424+
await expect(api.itemExists(childOne.url)).resolves.toBe(false)
425+
await expect(api.itemExists(childTwo.url)).resolves.toBe(true)
426+
await expect(api.itemExists(childFileTwo.url)).resolves.toBe(true)
427+
await expect(api.itemExists(`${childTwo.url}${emptyFolder.name}/`)).resolves.toBe(true)
428+
429+
const fileResponse = await api.get(childFileTwo.url)
430+
const content = await fileResponse.text()
431+
expect(content).toEqual(childFile.content)
432+
})
433+
})
434+
362435
describe('moving file', () => {
363436
test('rejects with 404 on inexistent file', () => {
364437
return rejectsWithStatuses(api.move(inexistentFile.url, filePlaceholder.url), [404])

tests/SolidApi.links.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,4 +445,19 @@ describe('recursive', () => {
445445
})
446446
})
447447

448+
describe('unified move', () => {
449+
test('moves folder with all links', async () => {
450+
await api.move(source.url, target.url)
451+
const results = await Promise.all(target.contentsAndPlaceholders
452+
.map(({ url }) => api.itemExists(url).then(exists => [url, exists])))
453+
results.forEach(res => expect(res).toEqual([expect.any(String), true]))
454+
})
455+
test('deletes source completely', async () => {
456+
await api.move(source.url, target.url)
457+
const results = await Promise.all(target.contentsAndPlaceholders
458+
.map(({ url }) => api.itemExists(url).then(exists => [url, exists])))
459+
results.forEach(res => expect(res).toEqual([expect.any(String), true]))
460+
})
461+
})
462+
448463
})

0 commit comments

Comments
 (0)