-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
move link functions to apiLinks.js and changes to copyFolder #95
Changes from 1 commit
abb0b39
927d065
8ba7f73
9d5a779
cc7eba9
90a30a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,11 +3,15 @@ import apiUtils from './utils/apiUtils' | |
import folderUtils from './utils/folderUtils' | ||
import RdfQuery from './utils/rdf-query' | ||
import errorUtils from './utils/errorUtils' | ||
import apiLinks from './apiLinks' | ||
|
||
|
||
const fetchLog = debug('solid-file-client:fetch') | ||
const { getParentUrl, getItemName, areFolders, areFiles, LINK } = apiUtils | ||
const { _parseLinkHeader, _urlJoin } = folderUtils | ||
const { ComposedFetchError, assertResponseOk, composedFetch, toComposedError } = errorUtils | ||
const {getLinks, getItemLinks} = apiLinks | ||
|
||
|
||
/** | ||
* @typedef {Object} WriteOptions | ||
|
@@ -360,9 +364,28 @@ class SolidAPI { | |
* @throws {ComposedFetchError} | ||
*/ | ||
async copyFile (from, to, options) { | ||
options = { | ||
...defaultWriteOptions, | ||
...options | ||
} | ||
if (typeof from !== 'string' || typeof to !== 'string') { | ||
throw toComposedError(new Error(`The from and to parameters of copyFile must be strings. Found: ${from} and ${to}`)) | ||
} | ||
// need to edit the file.acl | ||
if (options.withAcl && (getItemName(to) !== getItemName(from))) { | ||
throw toComposedError(new Error( `Cannot copyFile with Acl for different filenames. Found : ${getItemName(from)} and ${getItemName(to)}`)) | ||
} | ||
let resFile = await this._copyFile(from, to, options).catch(toComposedError) | ||
if (resFile.ok && options.withAcl) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const fromAcl = await getLinks(from, options.withAcl) | ||
if (fromAcl[0]) { | ||
const toAcl = await getItemLinks(to, options.withAcl) | ||
let resAcl = await this._copyFile(fromAcl[0].url, toAcl.acl, options).catch(toComposedError) | ||
} | ||
} | ||
} | ||
|
||
async _copyFile (from, to, options) { | ||
const response = await this.get(from) | ||
const content = await response.blob() | ||
const contentType = response.headers.get('content-type') | ||
|
@@ -383,11 +406,15 @@ class SolidAPI { | |
* @throws {ComposedFetchError} | ||
*/ | ||
async copyFolder (from, to, options) { | ||
options = { | ||
...defaultWriteOptions, | ||
...options | ||
} | ||
if (typeof from !== 'string' || typeof to !== 'string') { | ||
toComposedError(new Error(`The from and to parameters of copyFile must be strings. Found: ${from} and ${to}`)) | ||
throw toComposedError(new Error(`The from and to parameters of copyFolder must be strings. Found: ${from} and ${to}`)) | ||
} | ||
const { folders, files } = await this.readFolder(from, options).catch(toComposedError) | ||
const folderResponse = await this.createFolder(to, options).catch(toComposedError) | ||
const { folders, files } = await this.readFolder(from, { withAcl: false }).catch(toComposedError) // toFile.acl build by copyFile and _copyFolder | ||
const folderResponse = await this._copyFolder(from, to, options).catch(toComposedError) | ||
|
||
const creationResults = await composedFetch([ | ||
...folders.map(({ name }) => this.copyFolder(`${from}${name}/`, `${to}${name}/`, options)), | ||
|
@@ -397,6 +424,32 @@ class SolidAPI { | |
return [folderResponse].concat(...creationResults) // Alternative to Array.prototype.flat | ||
} | ||
|
||
/** | ||
* non recursive copy of a folder with .acl | ||
* Overwrites files per default. | ||
* Merges folders if already existing | ||
* @param {string} from | ||
* @param {string} to | ||
* @param {WriteOptions} [options] | ||
* @returns {Promise<Response[]>} Resolves with an array of creation responses. | ||
* The first one will be the folder specified by "to". | ||
* @throws {ComposedFetchError} | ||
*/ | ||
async _copyFolder (from, to, options) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would also need to check the acl file of copyFolder for absolute or relative paths which need to be changed, afaik. |
||
options = { | ||
...defaultWriteOptions, | ||
...options | ||
} | ||
const folderResponse = await this.createFolder(to, options).catch(toComposedError) | ||
if (folderResponse.ok && options.withAcl) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const fromAcl = await getLinks(from, options.withAcl) | ||
if (fromAcl[0]) { | ||
const toAcl = await getItemLinks(to, options.withAcl) | ||
let resAcl = await this._copyFile(fromAcl[0].url, toAcl.acl, options).catch(toComposedError) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Copy a file (url ending with file name) or folder (url ending with "/"). | ||
* Overwrites files per default. | ||
|
@@ -527,10 +580,12 @@ async deleteFolderContents (url, options) { | |
* @returns {Promise<FolderData>} | ||
*/ | ||
async readFolder (folderUrl, options = { withAcl: false }) { | ||
if (!folderUrl.endsWith('/')) folderUrl = folderUrl + '/' | ||
console.log('readFolder withAcl ' + options.withAcl) | ||
if (!folderUrl.endsWith('/')) { | ||
throw toComposedError(new Error(`Folder must end with a "\/". Found: ${folderUrl}`)) } | ||
let [rdf, folder, folderItems, fileItems] = [this.rdf, [], [], []] // eslint-disable-line no-unused-vars | ||
// For folders always add to fileItems : .meta file and if options.withAcl === true also add .acl linkFile | ||
fileItems = fileItems.concat(await this._getFolderLinks(folderUrl, options.withAcl)) | ||
// For folders always add to fileItems : .meta file | ||
fileItems = fileItems.concat(await getLinks(folderUrl, options.withAcl)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
let files = await rdf.query(folderUrl, { thisDoc: '' }, { ldp: 'contains' }) | ||
for (var f in files) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually when coding you don't need the |
||
let thisFile = files[f].object | ||
|
@@ -543,7 +598,7 @@ async deleteFolderContents (url, options) { | |
fileItems = fileItems.concat(itemRecord) | ||
// add fileLink acl | ||
if (options.withAcl) { | ||
fileItems = fileItems.concat(await this._getFileLinks(thisFile.value, options.withAcl)) | ||
fileItems = fileItems.concat(await getLinks(thisFile.value, options.withAcl)) // allways { withAcl: false} if copyFile withAcl: true | ||
} | ||
} | ||
} | ||
|
@@ -626,121 +681,6 @@ async deleteFolderContents (url, options) { | |
return returnVal | ||
} | ||
|
||
/** | ||
* @private | ||
* _geFolderLinks (TBD) | ||
*/ | ||
async _getFolderLinks (folderUrl, linkAcl) { | ||
let folder = await this.getLinks(folderUrl, linkAcl) | ||
return folder | ||
} | ||
|
||
/** | ||
* @private | ||
* _geFileLinks (TBD) | ||
*/ | ||
async _getFileLinks (itemUrl, linkAcl) { | ||
let itemWithLinks = await this.getLinks(itemUrl, linkAcl) | ||
return itemWithLinks | ||
} | ||
|
||
/** | ||
* @private // For now | ||
* getLinks (TBD) | ||
* | ||
* returns an array of records related to an item (resource or container) | ||
* 0-2 : the .acl, .meta, and .meta.acl for the item if they exist | ||
* each record includes these fields (see _getLinkObject) | ||
* url | ||
* type (contentType) | ||
* itemType ((AccessControl, or Metadata)) | ||
* name | ||
* parent | ||
*/ | ||
async getLinks (itemUrl, linkAcl) { | ||
let itemLinks = [] | ||
// don't getLinks for .acl files | ||
if (itemUrl.endsWith('.acl')) return [] | ||
let res = await this.fetch(itemUrl, { method: 'HEAD' }) | ||
let linkHeader = await res.headers.get('link') | ||
// linkHeader is null for index.html ?? | ||
if (linkHeader === null) return [] | ||
// get .meta, .acl links | ||
let links = await this._findLinksInHeader(itemUrl, linkHeader, linkAcl) | ||
if (links.acl) itemLinks = itemLinks.concat(links.acl) | ||
if (links.meta) { | ||
itemLinks = itemLinks.concat(links.meta) | ||
// get .meta.acl link | ||
links.metaAcl = await this.getLinks(links.meta.url, linkAcl) | ||
if (links.metaAcl) itemLinks = itemLinks.concat(links.metaAcl) | ||
} | ||
return itemLinks | ||
} | ||
|
||
/** | ||
* @private | ||
* findLinksInHeader (TBD) | ||
* | ||
*/ | ||
async _findLinksInHeader (originalUri, linkHeader, linkAcl) { | ||
let matches = _parseLinkHeader(linkHeader, originalUri) | ||
let final = {} | ||
for (let i = 0; i < matches.length; i++) { | ||
let split = matches[i].split('>') | ||
let href = split[0].substring(1) | ||
if (linkAcl && matches[i].match(/rel="acl"/)) { final.acl = await this._lookForLink('AccessControl', href, originalUri) } | ||
// .meta only for folders | ||
if (originalUri.endsWith('/') && matches[i].match(/rel="describedBy"/)) { | ||
final.meta = await this._lookForLink('Metadata', href, originalUri) | ||
} | ||
} | ||
return final | ||
} | ||
|
||
/** | ||
* @private | ||
* _lookForLink (TBD) | ||
* | ||
* - input | ||
* - linkType = one of AccessControl or Metatdata | ||
* - itemUrl = address of the item associated with the link | ||
* - relative URL from the link's associated item's header (e.g. .acl) | ||
* - creates an absolute Url for the link | ||
* - looks for the link and, if found, returns a link object | ||
* - else returns undefined | ||
*/ | ||
async _lookForLink (linkType, linkRelativeUrl, itemUrl) { | ||
let linkUrl = _urlJoin(linkRelativeUrl, itemUrl) | ||
try { | ||
let res = await this.fetch(linkUrl, { method: 'HEAD' }) | ||
if (typeof res !== 'undefined' && res.ok) { | ||
let contentType = res.headers.get('content-type') | ||
return this._getLinkObject(linkUrl, linkType, contentType, itemUrl) | ||
} | ||
} catch (e) {} // ignore if not found | ||
} | ||
|
||
/** | ||
* @private | ||
* _getLinkObject (TBD) | ||
* | ||
* creates a link object for a container or any item it holds | ||
* type is one of AccessControl, Metatdata | ||
* content-type is from the link's header | ||
* @param {string} linkUrl | ||
* @param {string} contentType | ||
* @param {"AccessControl"|"Metadata"} linkType | ||
* @returns {LinkObject} | ||
*/ | ||
_getLinkObject (linkUrl, linkType, contentType, itemUrl) { | ||
return { | ||
url: linkUrl, | ||
type: contentType, | ||
itemType: linkType, | ||
name: getItemName(linkUrl), | ||
parent: getParentUrl(linkUrl) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
import SolidApi from './SolidApi' | ||
import apiLinks from './apiLinks' | ||
|
||
const {getLinks, getItemLinks} = apiLinks | ||
|
||
const defaultPopupUri = 'https://solid.community/common/popup.html' | ||
|
||
|
@@ -170,21 +173,58 @@ class SolidFileClient extends SolidApi { | |
return res | ||
} | ||
|
||
// TBD object.acl object.meta | ||
async getItemLinks (url) { | ||
let links = await getItemLinks(url) | ||
return links | ||
} | ||
|
||
// TBD array of existings links | ||
async getLinks (url) { | ||
let links = await getLinks(url) | ||
return links | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to |
||
|
||
/* BELOW HERE ARE ALL ALIASES TO SOLID.API FUNCTIONS */ | ||
|
||
readHead (url, options) { return super.head(url, options) } | ||
|
||
deleteFile (url, options) { return this.delete(url, options) } | ||
async deleteFile (url) { | ||
// const urlAcl = await this.getLinks(url, true) | ||
// if (typeof urlAcl[0] === 'object') { let del = await this.delete(urlAcl[0].url) } // TBD throw complex error | ||
let links = await this.getItemLinks(url) | ||
if (links.acl) this.delete(links.acl) | ||
return this.delete(url) | ||
} | ||
|
||
deleteFolder (url, options) { return super.delete(url, options) } | ||
async deleteFolder (url, options) { return super.deleteFolderRecursively(url) } | ||
|
||
updateFile (url, content, contentType) { | ||
async updateFile (url, content, contentType) { | ||
return super.putFile(url, content, contentType) | ||
} | ||
|
||
moveFile (url, options) { return this.move(url, options) } | ||
// async copyFile (from, to, options) { return super.copyFile(from, to, options = { withAcl: true }) } | ||
|
||
async copyFolder (from, to, options) { return super.copyFolder(from, to , options) } | ||
|
||
// TBD error checking | ||
async moveFile (from, to) { | ||
await this.copyFile(from, to, { withAcl: true }) | ||
.then(res => { | ||
if (res.status === '200') { return this.deleteFile(from) } | ||
else { return this.deleteFile(to) } | ||
}) | ||
.catch(toComposedError) | ||
} | ||
|
||
moveFolder (url, options) { return this.move(url, options) } | ||
// TBD error checking | ||
async moveFolder (from, to) { | ||
const res = await this.copyFolder(from, to) | ||
if (res.ok) { | ||
if (res.status === '200') { await this.deleteFolder(from) } | ||
else { await this.deleteFolder(to) } | ||
} | ||
} | ||
|
||
/** | ||
* fetchAndParse | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we would have to check the contents of the acl file for every copyFile withAcl=true. When we take a look at an example from the spec, we see that they can also use absolute paths to specify for which file they grant access. So if we change the name and/or the path we possibly have to change the acl file.
Here's the example (from here):