diff --git a/frontend/package.json b/frontend/package.json index 199ef17..3707664 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.12.0", "bootstrap": "^4.4.1", + "crypto-js": "^3.1.9-1", "filesize": "^6.0.1", "jquery": "^3.4.1", "js-uuid": "0.0.6", diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js index e98dca8..7032717 100755 --- a/frontend/rollup.config.js +++ b/frontend/rollup.config.js @@ -43,6 +43,8 @@ export default { 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2': 'dist/webfonts/fa-brands-400.woff2', 'node_modules/js-uuid/js-uuid.js': 'dist/js/js-uuid.js', 'node_modules/filesize/lib/filesize.js': 'dist/js/filesize.js', + 'node_modules/crypto-js/core.js': 'dist/js/crypto-js/core.js', + 'node_modules/crypto-js/md5.js': 'dist/js/crypto-js/md5.js', verbose: true }), buble({jsx: 'h'}), diff --git a/frontend/src/html/index.html b/frontend/src/html/index.html index 114c934..e22169d 100755 --- a/frontend/src/html/index.html +++ b/frontend/src/html/index.html @@ -9,6 +9,8 @@ + + diff --git a/frontend/src/js/component/wb-collection-content.js b/frontend/src/js/component/wb-collection-content.js index 49c876a..0da5890 100644 --- a/frontend/src/js/component/wb-collection-content.js +++ b/frontend/src/js/component/wb-collection-content.js @@ -3,6 +3,7 @@ import WBTable from 'wb-table'; import WBBreadcrumbs from 'wb-breadcrumbs'; import { WBManifestReader } from 'wb-collection-manifest'; import makeArvadosRequest from 'make-arvados-request'; +import wbDownloadFile from 'wb-download-file'; class WBCollectionContent extends Component { constructor(...args) { @@ -41,6 +42,7 @@ class WBCollectionContent extends Component { prepareRows() { let { manifestReader } = this.state; let { collectionPath } = this.props; + let { arvHost, arvToken } = this.props.app.state; //path = path.split('/'); //path = [ '.' ].concat(path); @@ -58,7 +60,14 @@ class WBCollectionContent extends Component { ({ item[1] }), 'File', filesize(item[2]), - (
) + (
+ + +
) ] )) }); diff --git a/frontend/src/js/misc/make-arvados-request.js b/frontend/src/js/misc/make-arvados-request.js index b9b1e74..7783872 100644 --- a/frontend/src/js/misc/make-arvados-request.js +++ b/frontend/src/js/misc/make-arvados-request.js @@ -1,13 +1,25 @@ -function makeArvadosRequest(arvHost, arvToken, endpoint, method='GET', data=null, - contentType='application/json;charset=utf-8', responseType='json') { +function makeArvadosRequest(arvHost, arvToken, endpoint, params={}) { + const defaultParams = { + 'method': 'GET', + 'data': null, + 'contentType': 'application/json;charset=utf-8', + 'responseType': 'json', + 'useSsl': true, + 'requireToken': true + }; - if (!arvHost || !arvToken) + Object.keys(defaultParams).map(k => (params[k] = + (k in params ? params[k] : defaultParams[k]))); + let { method, data, contentType, responseType, useSsl, requireToken } = params; + + if (!(arvHost && (arvToken || !requireToken))) return new Promise((accept, reject) => reject()); let xhr = new XMLHttpRequest(); - xhr.open(method, 'https://' + arvHost + endpoint); - xhr.setRequestHeader('Authorization', 'OAuth2 ' + arvToken); + xhr.open(method, (useSsl ? 'https://' : 'http://') + arvHost + endpoint); + if (arvToken) + xhr.setRequestHeader('Authorization', 'OAuth2 ' + arvToken); if (data !== null) xhr.setRequestHeader('Content-Type', contentType); xhr.responseType = responseType; diff --git a/frontend/src/js/misc/wb-collection-manifest.js b/frontend/src/js/misc/wb-collection-manifest.js index 904984f..68fb05e 100644 --- a/frontend/src/js/misc/wb-collection-manifest.js +++ b/frontend/src/js/misc/wb-collection-manifest.js @@ -161,6 +161,19 @@ class WBManifestReader { out = b; } }*/ + getFile(path) { + if (typeof(path) === 'string') + path = path.split('/'); + if (path.length < 2) + throw Error('Invalid file path'); + let name = path[path.length - 1]; + let dir = this.findDir(path.slice(0, path.length - 1)); + if (!(name in dir)) + throw Error('File not found'); + if (!(dir[name] instanceof Array)) + throw Error('Path points to a directory not a file'); + return dir[name]; + } } export { WBManifestReader }; diff --git a/frontend/src/js/misc/wb-download-file.js b/frontend/src/js/misc/wb-download-file.js new file mode 100644 index 0000000..c64cfca --- /dev/null +++ b/frontend/src/js/misc/wb-download-file.js @@ -0,0 +1,65 @@ +import makeArvadosRequest from 'make-arvados-request'; + +function rdvHash(serviceId, locator) { + let blockHash = /^[0-9a-f]{32}/.exec(locator); + if (!blockHash) + throw Error('Invalid locator'); + if (typeof(serviceId) !== 'string') + throw Error('Invalid service ID'); + let res = CryptoJS.MD5(serviceId + blockHash).toString(); + return res; +} + +function wbDownloadFile(arvHost, arvToken, + manifestReader, path) { + + const file = manifestReader.getFile(path); + const name = path.split('/').reverse()[0]; + const blockRefs = file[0]; + let services; + + let prom = makeArvadosRequest(arvHost, arvToken, + '/arvados/v1/keep_services/accessible'); + prom = prom.then(xhr => (services = xhr.response['items'])); + + const blocks = []; + for (let i = 0; i < blockRefs.length; i++) { + prom = prom.then(() => { + const [ locator, position, size ] = blockRefs[i]; + const weights = services.map(s => rdvHash(s['uuid'], locator)); + const order = Object.keys(services).sort((a, b) => weights[b].localeCompare(weights[a])); + const orderedServices = order.map(i => services[i]); + + let k = 0; + const cb = () => { + if (k >= orderedServices.length) + throw Error('Block not found'); + + const svc = orderedServices[k]; + k++; + + let prom_1 = makeArvadosRequest(svc.service_host + + ':' + svc.service_port, arvToken, + '/' + locator, { 'useSsl': svc.service_ssl_flag, + 'responseType': 'arraybuffer' }); + //prom_1 = prom_1.then(xhr => xhr.response); + prom_1 = prom_1.catch(cb); + + return prom_1; + }; + + return cb().then(xhr => (blocks.append(xhr.response.slice(position, size)))); + }); + } + + prom = prom.then(() => { + const blob = new Blob(blocks); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = name; + + }); +} + +export default wbDownloadFile;