| @@ -2,6 +2,7 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "@fortawesome/fontawesome-free": "^5.12.0", | "@fortawesome/fontawesome-free": "^5.12.0", | ||||
| "bootstrap": "^4.4.1", | "bootstrap": "^4.4.1", | ||||
| "crypto-js": "^3.1.9-1", | |||||
| "filesize": "^6.0.1", | "filesize": "^6.0.1", | ||||
| "jquery": "^3.4.1", | "jquery": "^3.4.1", | ||||
| "js-uuid": "0.0.6", | "js-uuid": "0.0.6", | ||||
| @@ -43,6 +43,8 @@ export default { | |||||
| 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2': 'dist/webfonts/fa-brands-400.woff2', | '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/js-uuid/js-uuid.js': 'dist/js/js-uuid.js', | ||||
| 'node_modules/filesize/lib/filesize.js': 'dist/js/filesize.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 | verbose: true | ||||
| }), | }), | ||||
| buble({jsx: 'h'}), | buble({jsx: 'h'}), | ||||
| @@ -9,6 +9,8 @@ | |||||
| <script language="javascript" src="/js/fontawesome.min.js"></script> | <script language="javascript" src="/js/fontawesome.min.js"></script> | ||||
| <script language="javascript" src="/js/js-uuid.js"></script> | <script language="javascript" src="/js/js-uuid.js"></script> | ||||
| <script language="javascript" src="/js/filesize.js"></script> | <script language="javascript" src="/js/filesize.js"></script> | ||||
| <script language="javascript" src="/js/crypto-js/core.js"></script> | |||||
| <script language="javascript" src="/js/crypto-js/md5.js"></script> | |||||
| </head> | </head> | ||||
| <body> | <body> | ||||
| <script language="javascript" src="/js/app.min.js"></script> | <script language="javascript" src="/js/app.min.js"></script> | ||||
| @@ -3,6 +3,7 @@ import WBTable from 'wb-table'; | |||||
| import WBBreadcrumbs from 'wb-breadcrumbs'; | import WBBreadcrumbs from 'wb-breadcrumbs'; | ||||
| import { WBManifestReader } from 'wb-collection-manifest'; | import { WBManifestReader } from 'wb-collection-manifest'; | ||||
| import makeArvadosRequest from 'make-arvados-request'; | import makeArvadosRequest from 'make-arvados-request'; | ||||
| import wbDownloadFile from 'wb-download-file'; | |||||
| class WBCollectionContent extends Component { | class WBCollectionContent extends Component { | ||||
| constructor(...args) { | constructor(...args) { | ||||
| @@ -41,6 +42,7 @@ class WBCollectionContent extends Component { | |||||
| prepareRows() { | prepareRows() { | ||||
| let { manifestReader } = this.state; | let { manifestReader } = this.state; | ||||
| let { collectionPath } = this.props; | let { collectionPath } = this.props; | ||||
| let { arvHost, arvToken } = this.props.app.state; | |||||
| //path = path.split('/'); | //path = path.split('/'); | ||||
| //path = [ '.' ].concat(path); | //path = [ '.' ].concat(path); | ||||
| @@ -58,7 +60,14 @@ class WBCollectionContent extends Component { | |||||
| (<a href={ this.getUrl({ 'collectionPath': collectionPath + '/' + item[1] }) }>{ item[1] }</a>), | (<a href={ this.getUrl({ 'collectionPath': collectionPath + '/' + item[1] }) }>{ item[1] }</a>), | ||||
| 'File', | 'File', | ||||
| filesize(item[2]), | filesize(item[2]), | ||||
| (<div></div>) | |||||
| (<div> | |||||
| <button class="btn btn-outline-primary mx-1" title="Download" | |||||
| onclick={ () => wbDownloadFile(arvHost, arvToken, manifestReader, | |||||
| '.' + collectionPath + '/' + item[1]) }><i class="fas fa-download"></i></button> | |||||
| <button class="btn btn-outline-primary mx-1" title="View" | |||||
| onclick={ () => wbDownloadFile(arvHost, arvToken, manifestReader, | |||||
| '.' + collectionPath + '/' + item[1]) }><i class="far fa-eye"></i></button> | |||||
| </div>) | |||||
| ] | ] | ||||
| )) | )) | ||||
| }); | }); | ||||
| @@ -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()); | return new Promise((accept, reject) => reject()); | ||||
| let xhr = new XMLHttpRequest(); | 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) | if (data !== null) | ||||
| xhr.setRequestHeader('Content-Type', contentType); | xhr.setRequestHeader('Content-Type', contentType); | ||||
| xhr.responseType = responseType; | xhr.responseType = responseType; | ||||
| @@ -161,6 +161,19 @@ class WBManifestReader { | |||||
| out = b; | 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 }; | export { WBManifestReader }; | ||||
| @@ -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; | |||||