From daa84c6d919bbe4ad107db7f4c16b5bfc36b806a Mon Sep 17 00:00:00 2001 From: Mauro Mugnaini Date: Wed, 26 Jul 2023 18:14:11 +0200 Subject: [PATCH] Added `D4SWorkspace` class that interacts with WS WEB "rest" API and modificed the WEB components to use it. Some methods of the class, unused by web components at the moment, are still to be tested. --- storage/d4s-storage.js | 522 ++++++++++++++++++++++++++++++++++------- 1 file changed, 443 insertions(+), 79 deletions(-) diff --git a/storage/d4s-storage.js b/storage/d4s-storage.js index 95fff57..8694320 100644 --- a/storage/d4s-storage.js +++ b/storage/d4s-storage.js @@ -9,10 +9,13 @@ class D4SStorageHtmlElement extends HTMLElement { #baseurl = 'https://api.d4science.org/workspace' //#baseurl = 'https://workspace-repository.dev.d4science.org/storagehub/workspace' #boot = null + #d4sworkspace = null; + #showroot = false; // To show WS root folder by default set it to true constructor() { super() this.#boot = document.querySelector('d4s-boot-2') + this.#d4sworkspace = new D4SWorkspace(this.#baseurl, this.#boot) } get boot() { @@ -43,7 +46,21 @@ class D4SStorageHtmlElement extends HTMLElement { set baseUrl(url) { this.#baseurl = url this.setAttribute("baseurl", url) + this.#d4sworkspace = new D4SWorkspace(this.#baseurl, this.#boot) } + + get d4sWorkspace() { + return this.#d4sworkspace; + } + + get showRoot() { + return this.#showroot; + } + + set showRoot(show) { + this.#showroot = show; + } + } @@ -52,6 +69,7 @@ class D4SStorageHtmlElement extends HTMLElement { * { - if (reply.status !== 200) { - throw "Unable to load folder" - } - return reply.json() - - }).then(data => { - this.parseResponse(div, data) - this.#shadowRoot.appendChild(div) - + const initalFolderFunction = (cond) => {if (cond) return this.d4sWorkspace.getWorkspace(); else return this.d4sWorkspace.getVREFolder();} + initalFolderFunction(this.showRoot).then(data => { + this.parseResponse(div, data); + this.#shadowRoot.appendChild(div); }).catch(err => console.error(err)) } - parseResponse(parentElement, jresp) { - parentElement.classList.add(this.#loadedClass) - var ul = document.createElement('ul') - ul.classList.add('nested') - if (jresp.item) { - const itemname = jresp.item.displayName - const itemid = jresp.item.id - const path = this.buildListUrl(itemid) - ul.appendChild(this.createListItem(itemname, itemid)) - parentElement.appendChild(ul) - - } else if (jresp.itemlist) { - jresp - .itemlist - .filter(item => item['@class'].includes('FolderItem')) - .forEach(item => { - const itemname = item.name - const itemid = item.id - const path = this.buildListUrl(itemid) - ul.appendChild(this.createListItem(itemname, itemid)) - }) - parentElement.appendChild(ul) - + parseResponse(parentElement, data) { + parentElement.classList.add(this.#loadedClass); + var ul = document.createElement('ul'); + ul.classList.add('nested'); + if (Array.isArray(data)) { + data + .filter(item => item['@class'].includes('FolderItem')) + .forEach(item => { + const itemName = item.name; + const itemId = item.id; + const path = this.buildListUrl(itemId); + ul.appendChild(this.createListItem(itemName, itemId)); + }) + parentElement.appendChild(ul); } else { - console.error(jresp) + const itemName = data.displayName ? data.displayName : data.name; + const itemId = data.id; + const path = this.buildListUrl(itemId); + ul.appendChild(this.createListItem(itemName, itemId)); + parentElement.appendChild(ul); } } @@ -178,15 +185,8 @@ window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlEle return } - this.boot.secureFetch(this.buildListUrl(id)).then(reply => { - if (reply.status !== 200) { - throw "Unable to load folder" - } - return reply.json() - - }).then(data => { + this.d4sWorkspace.listFolder(id).then(data => { this.parseResponse(li, data) - }).catch(err => console.error(err)) } @@ -208,7 +208,7 @@ window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlEle } static get observedAttributes() { - return ["base-url", "filelist-id"] + return ["base-url", "filelist-id", "show-root"] } attributeChangedCallback(name, oldValue, newValue) { @@ -220,6 +220,8 @@ window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlEle case "base-url": this.baseUrl = newValue break + case "show-root": + this.showRoot = newValue; } } } @@ -290,24 +292,17 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE } list(folderId) { - this.boot.secureFetch(this.buildListUrl(folderId)).then(reply => { - if (reply.status !== 200) { - throw "Unable to build list url" - } - return reply.json() - - }).then(data => { - this.parseResponse(data) - - }).catch(err => console.error(err)) + this.d4sWorkspace.listFolder(folderId).then(data => { + this.parseResponse(data); + }).catch(err => console.error(err)); } - parseResponse(jresp) { - const root = this.createContainer() - if (jresp.itemlist) { - jresp.itemlist.forEach(item => root.appendChild(this.createFileRow(item))) + parseResponse(data) { + const root = this.createContainer(); + if (Array.isArray(data)) { + data.forEach(item => root.appendChild(this.createFileRow(item))); } else { - console.error(jresp) + console.error("Returned data is not an array: " + data); } } @@ -331,7 +326,7 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE } } else { console.info("Download of " + item.id) - this.download(this.buildDownloadUrl(item.id), item.name) + this.d4sWorkspace.download(item.id, item.name).catch(err => {alert(err)}); } }) @@ -351,29 +346,29 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE return '' + i + '' } - buildDownloadUrl(id) { - return this.baseUrl+ '/items/' + id + '/download' - } + // buildDownloadUrl(id) { + // return this.baseUrl+ '/items/' + id + '/download' + // } - download(url, name) { - this.boot.secureFetch(url).then(reply => { - if (reply.status !== 200) { - throw "Unable to download" - } - return reply.blob() + // download(url, name) { + // this.d4sWorkspace.download(url).then(reply => { + // if (reply.status !== 200) { + // throw "Unable to download" + // } + // return reply.blob() - }).then(blob => { - const objectURL = URL.createObjectURL(blob) - var tmplnk = document.createElement("a") - tmplnk.download = name - tmplnk.href = objectURL - //tmplnk.target="_blank" - document.body.appendChild(tmplnk) - tmplnk.click() - document.body.removeChild(tmplnk) + // }).then(blob => { + // const objectURL = URL.createObjectURL(blob) + // var tmplnk = document.createElement("a") + // tmplnk.download = name + // tmplnk.href = objectURL + // //tmplnk.target="_blank" + // document.body.appendChild(tmplnk) + // tmplnk.click() + // document.body.removeChild(tmplnk) - }).catch(err => console.error(err)) - } + // }).catch(err => console.error(err)) + // } static get observedAttributes() { return ["base-url"]; @@ -390,3 +385,372 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE } }) + +/** + * Class that helps the interaction with the D4Science's Workspace via API and user's token as bearer authentication + */ +class D4SWorkspace { + + #d4sboot = null; + #workspaceURL = null; + + /** + * Creates the new D4SWorkspace object, the d4sboot parameter can be null, in this case it is searched in the DOM document via querySelector() method + * @param {string} workspaceURL the WS API base URL + * @param {d4s-boot-2} d4sboot the d4s-boot-2 instance + */ + constructor(workspaceURL, d4sboot) { + this.setWorkspaceURL(workspaceURL) + if (d4sboot) { + this.setD4SBoot(d4sboot) + } else { + this.setD4SBoot(document.querySelector("d4s-boot-2")); + } + } + + /** + * Sets the Workspace API base URL + * @param {string} workspaceURL the WS API base URL + */ + setWorkspaceURL(workspaceURL) { + this.#workspaceURL = workspaceURL; + } + + /** + * Sets the d4s-boot-2 instance to be used to perform the OIDC/UMA autenticated secure fetches + * @param {string} d4sboot the d4s-boot-2 instance + */ + setD4SBoot(d4sboot) { + this.#d4sboot = d4sboot; + } + + /** + * Calls with a secure fetch the workspace with provided data. + * To upload data by using a FormData object you have to leave the mime attribute as null. + * @param {*} method the method to use for the call, default to GET + * @param {*} uri the uri to invoke, relative to workspaceURL provided in the object's constructor + * @param {*} body the payload to send + * @param {*} mime the mime type of the payload + * @param {*} extraHeaders extra HTTP headers to send + * @returns the reponse payload as a promise, JSON data promise if the returned data is "application/json", a String data promise if "text/*" or a blob data promise in other cases + * @throws the error as string, if occurred + */ + #callWorkspace(method, uri, body, mime, extraHeaders) { + let req = { }; + if (method) { + req.method = method; + } + if (body) { + req.body = body; + } + if (extraHeaders) { + req.headers = extraHeaders; + } + if (mime) { + if (req.headers) { + req.headers["Content-Type"] = mime; + } else { + req.headers = { "Content-Type" : mime }; + } + } + let url = this.#workspaceURL; + if (uri) { + url += uri.startsWith('/') ? uri : '/' + uri; + } + console.log("Invoking WS API with: " + url); + return this.#d4sboot.secureFetch(url, req) + .then(resp => { + if (resp.ok) { + const contentType = resp.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return resp.json(); + } else if (contentType && contentType.indexOf("text/") !== -1) { + return resp.text(); + } else { + return resp.blob(); + } + } else { + throw "Cannot invoke workspace via secure fetch for URL: " + url; + } + }).catch(err => { + console.error(err); + throw err; + }); + } + + /** + * Gets the user's Workspace root folder JSON item + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the WS root folder JSON item object + * @throws the error as string, if occurred + */ + getWorkspace(extraHeaders) { + return this.#callWorkspace("GET", null, null, null, extraHeaders) + .then(json => { + return json.item; + }).catch(err => { + const msg = "Cannot get workspace root ID. Error message: " + err; + console.error(msg); + throw msg; + }); + } + + /** + * Gets the user's Workspace root folder id (as string) + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the id of the WS root folder + * @throws the error as string, if occurred + */ + getWorkspaceId(extraHeaders) { + return this.getWorkspace(extraHeaders) + .then(json => { + return json.item.id; + }); + } + + /** + * Gets the contents of a folder, all sub-folders and files. + * @param {string} folderId the WS folder id to list + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the contents of the WS folder as json objects + * @throws the error as string, if occurred + */ + listFolder(folderId, extraHeaders) { + const uri = "/items/" + folderId + "/children"; + return this.#callWorkspace("GET", uri, null, null, extraHeaders) + .then(json => { + return json.itemlist; + }).catch(err => { + const msg = "Cannot get folder's content list. Error message: " + err; + console.error(msg); + throw msg; + }); + } + + /** + * Gets the VRE folder JSON item that belongs to the context where the access token has been issued. + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the id of the WS VRE folder + * @throws the error as string, if occurred + */ + getVREFolder(extraHeaders) { + return this.#callWorkspace("GET", "/vrefolder", null, null, extraHeaders) + .then(json => { + return json.item; + }).catch(err => { + const msg = "Cannot get VRE folder ID. Error message: " + err; + console.error(msg); + throw msg; + }); + } + + /** + * Gets the VRE folder id (as string) that belongs to the context where the access token has been issued. + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the id of the WS VRE folder + * @throws the error as string, if occurred + */ + getVREFolderId(extraHeaders) { + return this.getVREFolder(extraHeaders) + .then(item => { + return item.id; + }); + } + + /** + * @deprecated please use getOrCreateFolder() function instead + * @param {string} parentFolderId the id of the parent folder where search fo the folder + * @param {string} name the name of the folder (characters not allowed: ":", [others still to be found]) + * @param {string} description the description of the folder if it should be creaded + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the id of the folder, if it exists or it has been just created + * @throws the error as string, if occurred + */ + checkOrCreateFolder(parentFolderId, name, description, extraHeaders) { + return this.getOrCreateFolder(parentFolderId, name, description, extraHeaders); + } + + /** + * Gets the id of the folder represented by the name parameter if exists, a new folder is created if it doesn't not exist + * @param {string} parentFolderId the id of the parent folder where search fo the folder + * @param {string} name the name of the folder (characters not allowed: ":", [others still to be found]) + * @param {string} description the description of the folder if it should be creaded + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the id of the folder, if it exists or it has been just created + * @throws the error as string, if occurred + */ + getOrCreateFolder(parentFolderId, name, description, extraHeaders) { + const baseURI = "/items/" + parentFolderId; + console.log("Checking existance of folder: " + name); + let uri = baseURI + "/items/" + name; + return this.#callWorkspace("GET", uri, null, null, extraHeaders) + .then(json => { + if (json.itemlist[0]) { + const id = json.itemlist[0].id; + console.log("'" + name + "' folder exists with and has id: " + id); + return id; + } else { + console.info("'" + name + "' folder doesn't exist, creating it: " + name); + uri = baseURI + "/create/FOLDER"; + const params = new URLSearchParams({ name : name, description : description, hidden : false }); + return this.#callWorkspace("POST", uri, params.toString(), "application/x-www-form-urlencoded", extraHeaders).then(id => { + console.log("New '" + name + "' folder successfully created with id: " + id); + return id + }); + } + }); + }; + + /** + * Deletes a Workspace item (folder or file) represented by its id. + * @param {string} itemId the id of the item do delete + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns true if the file has been correctly created, false otherwise + */ + deleteItem(itemId, extraHeaders) { + return this.#callWorkspace("DELETE", "/workspace/items/" + itemId, null, null, extraHeaders) + .then(results => { + return true; + }).catch(err => { + console.error("Cannot DELETE workspace item with ID: " + itemId + ". Error message: " + err); + return false; + }); + } + + /** + * Downloads a Workspace item (folder or file) represented by its id. + * @param {string} itemId the WS item to download + * @param {string} name the name of the file to use for the download, if null the id is used + * @param {boolean} skipLocalDownload if provided and true returns the data to function caller, otherwise it also downloads it locally. + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the file's content if item is a file or a ZIP compressed archive if the item represents a folder + * @throws the error as string, if occurred + */ + download(itemId, name, skipLocalDownload, extraHeaders) { + return this.#callWorkspace("GET", "/items/" + itemId + "/download", null, null, extraHeaders) + .then(data => { + if (!skipLocalDownload) { + console.log("Donwloads item also locally to browser"); + const objectURL = URL.createObjectURL(data); + var tmplnk = document.createElement("a"); + tmplnk.download = name ? name : itemId; + tmplnk.href = objectURL; + //tmplnk.target="_blank"; + document.body.appendChild(tmplnk); + tmplnk.click(); + document.body.removeChild(tmplnk); + } else { + console.log("Skipping local download"); + } + return data; + }).catch(err => { + const msg = "Cannot download item with ID: " + itemId + ". Error message: " + err; + console.error(msg); + throw msg; + }); + } + + /** + * Uploads a new file into the folder represented by its id + * @param {string} folderId the folder where to create the new file + * @param {string} name the name of the file to crete (characters not allowed: ":", [others still to be found]) + * @param {object} data the archive data + * @param {string} contentType the content type of the file + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns true if the file has been correctly created, false otherwise + */ + uploadFile(folderId, name, description, data, contentType, extraHeaders) { + const uri = "/items/" + folderId + "/create/FILE"; + const request = new FormData(); + request.append("name", name); + request.append("description", description); + request.append( + "file", + new Blob([data], { + type: contentType + }) + ); + return this.#callWorkspace("POST", uri, request, null, extraHeaders) + .then(item => { + const id = item.id; + console.info("File '" + name + "' successfully uploaded and its id is: " + id); + return true; + }).catch(err => { + console.error("Cannot upload file '" + name + "'. Error: " + err); + return false; + }); + }; + + /** + * Uploads a new archive and stores its content into the folder represented by its id + * @param {string} folderId the folder where to create the new file + * @param {string} parentFolderName the name of the folder to create (characters not allowed: ":", [others still to be found]) + * @param {object} data the file's contents + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns true if the file has been correctly created, false otherwise + */ + uploadArchive(folderId, parentFolderName, description, data, extraHeaders) { + const uri = "/items/" + folderId + "/create/ARCHIVE"; + const request = new FormData(); + request.append("parentFolderName", parentFolderName); + request.append( + "file", + new Blob([data], { + type: "application/octet-stream" + }) + ); + return this.#callWorkspace("POST", uri, request, null, extraHeaders) + .then(item => { + const id = item.id; + console.info("Archive file successfully uploaded and extracted into the '" + parentFolderName + "' folder, its id is: " + id); + return true; + }).catch(err => { + console.error("Cannot upload archive file. Error: " + err); + return false; + }); + }; + + /** + * Returns the public URL of a file + * @param {string} itemId the id of the item + * @param {int} version the optional file version + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the public URL of the item + * @throws the error as string, if occurred + */ + getPublicURL(itemId, version, extraHeaders) { + let uri = "/workspace/items/" + itemId + "/publiclink"; + if (version) { + uri += "version=" + version; + } + return this.#callWorkspace("GET", uri, null, null, extraHeaders) + .then(publicURL => { + return publicURL; + }).catch(err => { + const message = "Cannot retrieve the item's public URL. Error message: " + err; + console.error(message); + throw message; + }); + } + + /** + * Finds into a folder all the file files having the name than matches a specific pattern. + * Wildcard (*) can be used at the start or the end of the pattern (eg. starts with *{name}, ends with = {name}*) + * @param {string} folderId the WS folder id where to search + * @param {string} pattern the pattern to use for the search. Wildcard '*' can be used at the start or the end of the pattern (eg. starts with *{name}, ends with = {name}*) + * @param {map} extraHeaders extra HTTP headers to be used for the request + * @returns the contents of the WS folder as json objects + * @throws the error as string, if occurred + */ + findByName(folderId, pattern, extraHeaders) { + const uri = "/items/" + folderId + "/items/" + pattern; + return this.#callWorkspace("GET", uri, null, null, extraHeaders) + .then(json => { + return json.itemlist; + }).catch(err => { + const msg = "Cannot get folder's content list. Error message: " + err; + console.error(msg); + throw msg; + }); + } +} \ No newline at end of file