Skip to content
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

Merged
merged 6 commits into from
Dec 8, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 62 additions & 122 deletions src/SolidApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))) {
Copy link
Contributor

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):

# Contents of https://alice.databox.me/docs/file1.acl
@prefix  acl:  <http://www.w3.org/ns/auth/acl#>  .

<#authorization1>
    a             acl:Authorization;
    acl:agent     <https://alice.databox.me/profile/card#me>;  # Alice's WebID
    acl:accessTo  <https://alice.databox.me/docs/file1>;
    acl:mode      acl:Read, 
                  acl:Write, 
                  acl:Control.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resFile.ok is always true here. When a fetch request resolves with response.ok === false, then it will be thrown.

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')
Expand All @@ -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)),
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

folderResponse.ok is always true

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.
Expand Down Expand Up @@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fileItems.push(await getLinks(folderUrl, options.withAcl)) would be more concise.

let files = await rdf.query(folderUrl, { thisDoc: '' }, { ldp: 'contains' })
for (var f in files) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually when coding you don't need the var declaration. It declares the variable globally which isn't desired in most cases. You could use const here (or let if you want to modify it)

let thisFile = files[f].object
Expand All @@ -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
}
}
}
Expand Down Expand Up @@ -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)
}
}
}

/**
Expand Down
50 changes: 45 additions & 5 deletions src/SolidFileClient.js
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'

Expand Down Expand Up @@ -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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to await the response here, you could just return getItemLinks(url) and it has the same effect. (async methods will always return a promise. Hence if you await something and then directly return it, you could also just return it)


/* 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
Expand Down
Loading