web-components/storage/d4s-storage.js

756 lines
25 KiB
JavaScript

var D4S_STORAGE_SCRIPT = document.currentScript
/**
* Base class of <d4s-storage-tree> and <d4s-storage-folder>
*/
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
* <d4s-storage-tree
* /@filelist-id: identifier of d4s-storage-folder component
* /@base-url: the base endpoint url [defaults to: https://api.d4science.org/workspace]
* /@show-root: false (or omitted) to show the VRE folder (default), true to show the root folder of the WS
*/
window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlElement {
#storageFilelistId = null
#loadedClass = 'loaded'
#selectedClass = 'selected'
#selectedbgcolor = 'lightgray'
#shadowRoot
constructor() {
super()
this.#shadowRoot = this.attachShadow({mode: 'open'})
}
/*
Notes:
API docs: https://gcube.wiki.gcube-system.org/gcube/StorageHub_REST_API
Preprod: https://accounts.pre.d4science.org
Sample context: %2Fpred4s%2Fpreprod%2FpreVRE
User workspace: https://storagehub.pre.d4science.net/storagehub/workspace
vrefolder: https://storagehub.pre.d4science.net/storagehub/workspace/vrefolder
-> 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 = `
<style>
.selected > *:first-child {
background-color: ${this.#selectedbgcolor};
}
ul, li {
list-style-type: none;
}
li {
cursor: pointer;
}
</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
*
* <d4s-storage-folder
* /@id: specify the id to join this component with storage tree component
* /@base-url: the base endpoint url [defaults to: https://api.d4science.org/workspace]
*/
window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlElement {
#d4sstorageTree = null
#selectedbgcolor = 'lightgray'
#srcbaseurl = null
constructor() {
super()
if (D4S_STORAGE_SCRIPT) {
const d4ssrc = D4S_STORAGE_SCRIPT.src
this.#srcbaseurl = (d4ssrc) ? d4ssrc.substring(0, d4ssrc.lastIndexOf('/')) : ""
}
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'})
this.appendStylesheets(shadowRoot)
const style = document.createElement('style')
style.innerHTML = `
span {
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
`
shadowRoot.appendChild(style)
this.createContainer()
}
createContainer() {
var div = document.createElement('div')
div.classList.add('d4s-folder')
var child = this.shadowRoot.querySelector('.d4s-folder')
if (child) {
this.shadowRoot.removeChild(child)
}
this.shadowRoot.appendChild(div)
return div
}
join(tree) {
if (this.#d4sstorageTree == null) {
this.#d4sstorageTree = tree
}
}
list(folderId) {
this.d4sWorkspace.listFolder(folderId).then(data => {
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 = `<span>${item.name}</span>`
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 = `<img src="${this.#srcbaseurl}/img/file-earmark.svg"></img>`
if (item['@class'].includes('FolderItem')) {
i = `<img src="${this.#srcbaseurl}/img/folder.svg"></img>`
} else if (item['@class'].includes('ImageFile')) {
i = `<img src="${this.#srcbaseurl}/img/image.svg"></img>`
} else if (item['@class'].includes('PDFFileItem')) {
i = `<img src="${this.#srcbaseurl}/img/filetype-pdf.svg"></img>`
}
return '<span class="px-2">' + i + '</span>'
}
// 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 <code>querySelector()</code> 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 <code>FormData</code> object you have to leave the <code>mime</code> attribute as <code>null</code>.
* @param {*} method the method to use for the call, default to <code>GET</code>
* @param {*} uri the uri to invoke, relative to <code>workspaceURL</code> 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 <code>true</code> if the file has been correctly created, <code>false<c/ode> 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 <code>true</code> 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 <code>true</code> if the file has been correctly created, <code>false<c/ode> 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 <code>true</code> if the file has been correctly created, <code>false<c/ode> 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;
});
}
}