var D4S_STORAGE_SCRIPT = document.currentScript /** * Base class of and */ class D4SStorageHtmlElement extends HTMLElement { #d4smissmsg = 'Required d4s-boot-2 component not found' #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() { if (this.#boot == null) { this.#boot = document.querySelector('d4s-boot-2') if (!this.#boot) { throw this.#d4smissmsg } } return this.#boot } appendStylesheets(root) { const linkElem1 = document.createElement('link') linkElem1.setAttribute('rel', 'stylesheet') linkElem1.setAttribute('href', 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css') root.appendChild(linkElem1) } buildListUrl(id) { return this.#baseurl + '/items/' + id + '/children' } get baseUrl() { return this.#baseurl } 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; } } /** * Builds storagehub VREFolder tree * and if "id": "e0cd15d2-0071-43ca-bc42-aef8d80660fe", the tree dir of vrefolder: https://storagehub.pre.d4science.net/storagehub/workspace/items/e0cd15d2-0071-43ca-bc42-aef8d80660fe/children download of a file (domenica.jpg): https://storagehub.pre.d4science.net/storagehub/workspace/items/a5086811-59d1-4230-99fa-98431e820cf1/download */ connectedCallback() { this.appendStylesheets(this.#shadowRoot) var div = document.createElement('div') const style = ` ` div.innerHTML = style 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, 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 { 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); } } createListItem(label, id) { var li = document.createElement('li') li.setAttribute('data-id', id) var child = document.createElement('span') child.innerHTML = label li.appendChild(child) li.addEventListener('click', (ev) => { ev.stopPropagation() this.select(ev.currentTarget.getAttribute('data-id')) }) return li } select(id) { var li = this.shadowRoot.querySelector(`*[data-id='${id}']`) this.displayPath(li) // if it was already selected then do nothing if (li.classList.contains(this.#selectedClass)) { this.toggleULDisplay(li) return } // switch selected and lists folder contents if (!li.classList.contains(this.#selectedClass)) { const selected = this.shadowRoot.querySelector('.' + this.#selectedClass) if (selected) { selected.classList.remove(this.#selectedClass) } li.classList.add(this.#selectedClass) if (this.#storageFilelistId) { var folder = document.getElementById(this.#storageFilelistId) folder.join(this) folder.list(id) } } if (li.classList.contains(this.#loadedClass)) { return } this.d4sWorkspace.listFolder(id).then(data => { this.parseResponse(li, data) }).catch(err => console.error(err)) } displayPath(li) { var curr = li while (curr.parentElement != null) { if (curr.parentElement.tagName == 'UL') { curr.parentElement.style.display = 'block' } curr = curr.parentElement } } toggleULDisplay(li) { const ul = li.querySelector('ul') if (ul) { ul.style.display = (ul.style.display === 'none') ? 'block' : 'none' } } static get observedAttributes() { return ["base-url", "filelist-id", "show-root"] } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { switch (name) { case "filelist-id": this.#storageFilelistId = newValue break case "base-url": this.baseUrl = newValue break case "show-root": this.showRoot = newValue; } } } get filelistId() { return this.#storageFilelistId } set filelistId(id) { this.#storageFilelistId = id this.setAttribute("filelist-id", id) } }) /** * Lists elements in selected folder * * { this.parseResponse(data); }).catch(err => console.error(err)); } parseResponse(data) { const root = this.createContainer(); if (Array.isArray(data)) { data.forEach(item => root.appendChild(this.createFileRow(item))); } else { console.error("Returned data is not an array: " + data); } } createFileRow(item) { var div = document.createElement('div') div.classList.add('row') var filename = document.createElement('div') filename.classList.add('col') var name = `${item.name}` filename.innerHTML = this.iconTag(item) + name const isFolder = item['@class'].includes('FolderItem') filename.addEventListener('click', (ev) => { ev.stopPropagation() if (isFolder) { const span = ev.currentTarget.querySelector(':nth-child(2)') if (span) { span.style = 'background-color: ' + this.#selectedbgcolor } if (this.#d4sstorageTree != null) { this.#d4sstorageTree.select(item.id) } } else { console.info("Download of " + item.id) this.d4sWorkspace.download(item.id, item.name).catch(err => {alert(err)}); } }) div.appendChild(filename) return div } iconTag(item) { var i = `` if (item['@class'].includes('FolderItem')) { i = `` } else if (item['@class'].includes('ImageFile')) { i = `` } else if (item['@class'].includes('PDFFileItem')) { i = `` } return '' + i + '' } // buildDownloadUrl(id) { // return this.baseUrl+ '/items/' + id + '/download' // } // 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) // }).catch(err => console.error(err)) // } static get observedAttributes() { return ["base-url"]; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { switch (name) { case "base-url": this.baseUrl = newValue break } } } }) /** * 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; }); } }