| @@ -47,6 +47,10 @@ export default { | |||||
| 'node_modules/crypto-js/md5.js': 'dist/js/crypto-js/md5.js', | 'node_modules/crypto-js/md5.js': 'dist/js/crypto-js/md5.js', | ||||
| 'src/js/misc/wb-manifest-worker.js': 'dist/js/wb-manifest-worker.js', | 'src/js/misc/wb-manifest-worker.js': 'dist/js/wb-manifest-worker.js', | ||||
| 'node_modules/js-yaml/dist/js-yaml.min.js': 'dist/js/js-yaml.min.js', | 'node_modules/js-yaml/dist/js-yaml.min.js': 'dist/js/js-yaml.min.js', | ||||
| 'node_modules/streamsaver/mitm.html': 'dist/mitm.html', | |||||
| 'node_modules/streamsaver/sw.js': 'dist/sw.js', | |||||
| 'node_modules/streamsaver/StreamSaver.js': 'dist/js/StreamSaver.js', | |||||
| 'node_modules/web-streams-polyfill/dist/ponyfill.js': 'dist/js/web-streams-polyfill/ponyfill.js', | |||||
| verbose: true | verbose: true | ||||
| }), | }), | ||||
| buble({jsx: 'h'}), | buble({jsx: 'h'}), | ||||
| @@ -4,6 +4,7 @@ | |||||
| <link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" /> | <link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" /> | ||||
| <link rel="stylesheet" type="text/css" href="/css/all.min.css" /> | <link rel="stylesheet" type="text/css" href="/css/all.min.css" /> | ||||
| <link rel="stylesheet" type="text/css" href="/css/index.css" /> | <link rel="stylesheet" type="text/css" href="/css/index.css" /> | ||||
| <script language="javascript" src="/js/web-streams-polyfill/ponyfill.js"></script> | |||||
| <script language="javascript"> | <script language="javascript"> | ||||
| window.process = { 'env': { 'NODE_ENV': 'production' } }; | window.process = { 'env': { 'NODE_ENV': 'production' } }; | ||||
| </script> | </script> | ||||
| @@ -15,6 +16,7 @@ | |||||
| <script language="javascript" src="/js/crypto-js/core.js"></script> | <script language="javascript" src="/js/crypto-js/core.js"></script> | ||||
| <script language="javascript" src="/js/crypto-js/md5.js"></script> | <script language="javascript" src="/js/crypto-js/md5.js"></script> | ||||
| <script language="javascript" src="/js/js-yaml.min.js"></script> | <script language="javascript" src="/js/js-yaml.min.js"></script> | ||||
| <script language="javascript" src="/js/StreamSaver.js"></script> | |||||
| </head> | </head> | ||||
| <body> | <body> | ||||
| <script language="javascript" src="/js/app.min.js"></script> | <script language="javascript" src="/js/app.min.js"></script> | ||||
| @@ -1,9 +1,8 @@ | |||||
| import { h, Component } from 'preact'; | import { h, Component } from 'preact'; | ||||
| import WBTable from 'wb-table'; | 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-manifest-reader'; | |||||
| import WBPagination from 'wb-pagination'; | import WBPagination from 'wb-pagination'; | ||||
| import WBRootDirWrapper from 'wb-rootdir-wrapper'; | |||||
| import makeArvadosRequest from 'make-arvados-request'; | import makeArvadosRequest from 'make-arvados-request'; | ||||
| import wbDownloadFile from 'wb-download-file'; | import wbDownloadFile from 'wb-download-file'; | ||||
| @@ -23,6 +22,7 @@ class WBCollectionContent extends Component { | |||||
| this.state.mode = 'manifestDownload'; | this.state.mode = 'manifestDownload'; | ||||
| this.state.parsedStreams = 0; | this.state.parsedStreams = 0; | ||||
| this.state.totalStreams = 1; | this.state.totalStreams = 1; | ||||
| this.state.rootDirWrapper = null; | |||||
| } | } | ||||
| getUrl(params) { | getUrl(params) { | ||||
| @@ -95,10 +95,20 @@ class WBCollectionContent extends Component { | |||||
| } | } | ||||
| prom_1 = prom_1.then(() => { | prom_1 = prom_1.then(() => { | ||||
| const prom_2 = new Promise(accept => { | |||||
| manifestWorker.onmessage = e => accept(e); | |||||
| manifestWorker.postMessage([ 'getData' ]); | |||||
| }); | |||||
| return prom_2; | |||||
| }); | |||||
| prom_1 = prom_1.then(e => { | |||||
| this.state.rootDirWrapper = new WBRootDirWrapper(e.data[1], e.data[2]); | |||||
| this.setState({ | this.setState({ | ||||
| 'mode': 'browsingReady' | 'mode': 'browsingReady' | ||||
| }); | }); | ||||
| this.prepareRows(); | |||||
| this.prepareRows(this.state.rootDirWrapper.listDirectory('.' + | |||||
| this.props.collectionPath)); | |||||
| }); | }); | ||||
| return prom_1; | return prom_1; | ||||
| @@ -106,38 +116,22 @@ class WBCollectionContent extends Component { | |||||
| } | } | ||||
| componentWillReceiveProps(nextProps) { | componentWillReceiveProps(nextProps) { | ||||
| const { manifestWorker, mode } = this.state; | |||||
| const { collectionPath } = nextProps; | |||||
| if (mode === 'browsingReady') { | |||||
| this.state.mode = 'waitForListing'; | |||||
| let prom = new Promise(accept => { | |||||
| manifestWorker.onmessage = (e) => accept(e); | |||||
| manifestWorker.postMessage([ 'listDirectory', '.' + collectionPath ]); | |||||
| }); | |||||
| this.props = nextProps; | |||||
| prom = prom.then(e => { | |||||
| this.state.mode = 'browsingReady'; | |||||
| this.prepareRows(e.data[1]); | |||||
| }); | |||||
| const { rootDirWrapper, mode } = this.state; | |||||
| const { collectionPath } = this.props; | |||||
| if (mode === 'browsingReady') { | |||||
| const listing = rootDirWrapper.listDirectory('.' + collectionPath); | |||||
| this.prepareRows(listing); | |||||
| } | } | ||||
| this.props = nextProps; | |||||
| // this.prepareRows(); | |||||
| } | } | ||||
| prepareRows(listing) { | prepareRows(listing) { | ||||
| if (listing) | |||||
| this.state.listing = listing; | |||||
| else | |||||
| listing = this.state.listing; | |||||
| let { manifestReader, mode } = this.state; | |||||
| let { collectionPath, page, itemsPerPage } = this.props; | |||||
| let { arvHost, arvToken } = this.props.app.state; | |||||
| //path = path.split('/'); | |||||
| //path = [ '.' ].concat(path); | |||||
| let { rootDirWrapper, mode } = this.state; | |||||
| let { collectionPath, page, itemsPerPage, app } = this.props; | |||||
| let { arvHost, arvToken } = app.state; | |||||
| //let listing = manifestReader.listDirectory('.' + collectionPath) | |||||
| const numPages = Math.ceil(listing.length / itemsPerPage); | const numPages = Math.ceil(listing.length / itemsPerPage); | ||||
| listing = listing.slice(page * itemsPerPage, | listing = listing.slice(page * itemsPerPage, | ||||
| page * itemsPerPage + itemsPerPage); | page * itemsPerPage + itemsPerPage); | ||||
| @@ -158,25 +152,17 @@ class WBCollectionContent extends Component { | |||||
| <div> | <div> | ||||
| <button class="btn btn-outline-primary mx-1" title="Download" | <button class="btn btn-outline-primary mx-1" title="Download" | ||||
| onclick={ () => { | onclick={ () => { | ||||
| let prom = wbDownloadFile(arvHost, arvToken, manifestReader, | |||||
| '.' + collectionPath + '/' + item[1]); | |||||
| prom = prom.then(blocks => { | |||||
| const blob = new Blob(blocks); | |||||
| const a = document.createElement('a'); | |||||
| a.name = item[1]; | |||||
| a.href = window.URL.createObjectURL(blob); | |||||
| a.click(); | |||||
| }); | |||||
| const file = rootDirWrapper.getFile('.' + collectionPath + '/' + item[1]); | |||||
| const blob = new Blob([ | |||||
| JSON.stringify([ arvHost, arvToken, item[1], file ]) | |||||
| ]); | |||||
| const blocksBlobUrl = URL.createObjectURL(blob); | |||||
| window.open('/download/' + encodeURIComponent(blocksBlobUrl), '_blank'); | |||||
| } }><i class="fas fa-download"></i></button> | } }><i class="fas fa-download"></i></button> | ||||
| <button class="btn btn-outline-primary mx-1" title="View" | <button class="btn btn-outline-primary mx-1" title="View" | ||||
| onclick={ () => { | onclick={ () => { | ||||
| let prom = wbDownloadFile(arvHost, arvToken, manifestReader, | |||||
| '.' + collectionPath + '/' + item[1]); | |||||
| prom = prom.then(blocks => { | |||||
| const blob = new Blob(blocks); | |||||
| window.open(window.URL.createObjectURL(blob)); | |||||
| }); | |||||
| alert('Not implemented.') | |||||
| } }><i class="far fa-eye"></i></button> | } }><i class="far fa-eye"></i></button> | ||||
| </div> | </div> | ||||
| ) : null) | ) : null) | ||||
| @@ -16,6 +16,9 @@ onmessage = function(e) { | |||||
| const lst = listDirectory(rootDir, e.data[1], e.data[2]); | const lst = listDirectory(rootDir, e.data[1], e.data[2]); | ||||
| postMessage([ 'listDirectoryResult', lst ]) | postMessage([ 'listDirectoryResult', lst ]) | ||||
| break; } | break; } | ||||
| case 'getData': { | |||||
| postMessage([ 'getDataResult', rootDir, streams ]); | |||||
| break; } | |||||
| default: | default: | ||||
| throw Error('Unknown verb: ' + e.data[0]); | throw Error('Unknown verb: ' + e.data[0]); | ||||
| } | } | ||||
| @@ -0,0 +1,82 @@ | |||||
| class WBRootDirWrapper { | |||||
| constructor(rootDir, streams) { | |||||
| this.rootDir = rootDir; | |||||
| this.streams = streams; | |||||
| } | |||||
| findDir(path) { | |||||
| if (typeof(path) === 'string') | |||||
| path = path.split('/'); | |||||
| if (path[0] !== '.') | |||||
| throw Error('Path must begin with a dot component'); | |||||
| let dir = this.rootDir; | |||||
| for (let i = 1; i < path.length; i++) { | |||||
| if (!(path[i] in dir)) | |||||
| throw Error('Directory not found'); | |||||
| if (dir[path[i]] instanceof Array) | |||||
| throw Error('Path is a file not directory'); | |||||
| dir = dir[path[i]]; | |||||
| } | |||||
| return dir; | |||||
| } | |||||
| listDirectory(path) { | |||||
| let dir = this.findDir(path); | |||||
| let keys = Object.keys(dir); | |||||
| keys.sort(); | |||||
| let subdirs = keys.filter(k => !(dir[k] instanceof Array)); | |||||
| let files = keys.filter(k => (dir[k] instanceof Array)); | |||||
| let res = subdirs.map(k => [ 'd', k, null ]); | |||||
| res = res.concat(files.map(k => [ 'f', k, dir[k][1] ])); | |||||
| return res; | |||||
| } | |||||
| unescapeName(name) { | |||||
| return name.replace(/(\\\\|\\[0-9]{3})/g, (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8)))); | |||||
| } | |||||
| escapeName(name) { | |||||
| return name.replace(/ /g, '\\040'); | |||||
| } | |||||
| getFile(path) { | |||||
| if (typeof(path) === 'string') | |||||
| path = path.split('/'); | |||||
| if (path.length < 2) | |||||
| throw Error('Invalid file path'); | |||||
| const name = path[path.length - 1]; | |||||
| const 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'); | |||||
| const streams = this.streams; | |||||
| let file = dir[name]; | |||||
| file = [ file[0].map(seg => { | |||||
| const stm = streams[seg[0]]; | |||||
| const used = stm.map(loc => !( loc[2] <= seg[1] || loc[1] >= seg[1] + seg[2] ) ); | |||||
| const start = used.indexOf(true); | |||||
| const end = used.lastIndexOf(true) + 1; | |||||
| if (start === -1) | |||||
| return []; | |||||
| const res = []; | |||||
| for (let i = start; i < end; i++) { | |||||
| const loc = stm[i]; | |||||
| res.push([ loc[0], Math.max(0, seg[1] - loc[1]), | |||||
| Math.min(loc[2], seg[1] + seg[2] - loc[1]) ]); | |||||
| } | |||||
| return res; | |||||
| }), file[1] ]; | |||||
| file[0] = file[0].reduce((a, b) => a.concat(b)); | |||||
| return file; | |||||
| } | |||||
| } | |||||
| export default WBRootDirWrapper; | |||||
| @@ -11,6 +11,7 @@ import WBCollectionBrowse from 'wb-collection-browse'; | |||||
| import WBUsersPage from 'wb-users-page'; | import WBUsersPage from 'wb-users-page'; | ||||
| import WBWorkflowView from 'wb-workflow-view'; | import WBWorkflowView from 'wb-workflow-view'; | ||||
| import WBLaunchWorkflowPage from 'wb-launch-workflow-page'; | import WBLaunchWorkflowPage from 'wb-launch-workflow-page'; | ||||
| import WBDownloadPage from 'wb-download-page'; | |||||
| import arvadosTypeName from 'arvados-type-name'; | import arvadosTypeName from 'arvados-type-name'; | ||||
| class WBApp extends Component { | class WBApp extends Component { | ||||
| @@ -91,6 +92,8 @@ class WBApp extends Component { | |||||
| <WBWorkflowView path="/workflow/:uuid" app={ this } /> | <WBWorkflowView path="/workflow/:uuid" app={ this } /> | ||||
| <WBLaunchWorkflowPage path="/workflow-launch/:workflowUuid" app={ this } /> | <WBLaunchWorkflowPage path="/workflow-launch/:workflowUuid" app={ this } /> | ||||
| <WBDownloadPage path="/download/:blocksBlobUrl" app={ this } /> | |||||
| </Router> | </Router> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -0,0 +1,112 @@ | |||||
| import { h, Component } from 'preact'; | |||||
| import makeArvadosRequest from 'make-arvados-request'; | |||||
| class WBDownloadPage extends Component { | |||||
| componentDidMount() { | |||||
| const { app, blocksBlobUrl } = this.props; | |||||
| const { arvHost, arvToken } = app.state; | |||||
| let prom = new Promise((accept, reject) => { | |||||
| const xhr = new XMLHttpRequest(); | |||||
| xhr.open('GET', blocksBlobUrl); | |||||
| xhr.onreadystatechange = () => { | |||||
| if (xhr.readyState !== 4) | |||||
| return; | |||||
| if (xhr.status !== 200) | |||||
| reject(xhr); | |||||
| else | |||||
| accept(xhr); | |||||
| }; | |||||
| xhr.responseType = 'blob'; | |||||
| xhr.send(); | |||||
| }); | |||||
| prom = prom.then(xhr => xhr.response.text()); | |||||
| let name, file; | |||||
| const { streamSaver, location } = window; | |||||
| streamSaver.mitm = location.protocol + '//' + | |||||
| location.hostname + (location.port ? | |||||
| ':' + location.port : '') + '/mitm.html'; | |||||
| let fileStream; | |||||
| let writer; | |||||
| let done = false; | |||||
| prom = prom.then(text => { | |||||
| let _; | |||||
| [ _, _, name, file ] = JSON.parse(text); | |||||
| fileStream = streamSaver.createWriteStream(name, { | |||||
| size: file[1] | |||||
| }); | |||||
| writer = fileStream.getWriter(); | |||||
| window.onunload = () => { | |||||
| writer.abort() | |||||
| }; | |||||
| window.onbeforeunload = evt => { | |||||
| if (!done) { | |||||
| evt.returnValue = `Are you sure you want to leave?`; | |||||
| } | |||||
| }; | |||||
| const filters = [ | |||||
| ['service_type', '=', 'proxy'] | |||||
| ]; | |||||
| return makeArvadosRequest(arvHost, arvToken, | |||||
| '/arvados/v1/keep_services?filters=' + | |||||
| encodeURIComponent(JSON.stringify(filters))); | |||||
| }); | |||||
| prom = prom.then(xhr => { | |||||
| const services = xhr.response.items; | |||||
| const i = Math.floor(Math.random() * services.length); | |||||
| const proxy = services[i]; | |||||
| let prom_1 = new Promise(accept => accept()); | |||||
| for (let k = 0; k < file[0].length; k++) { | |||||
| const loc = file[0][k]; | |||||
| prom_1 = prom_1.then(() => makeArvadosRequest( | |||||
| proxy.service_host + ':' + proxy.service_port, | |||||
| arvToken, | |||||
| '/' + loc[0], | |||||
| { 'useSsl': proxy.service_ssl_flag, | |||||
| 'responseType': 'arraybuffer' } | |||||
| )); | |||||
| prom_1 = prom_1.then(xhr_1 => { | |||||
| const blk = xhr_1.response.slice(loc[1], loc[2]); | |||||
| // const r = new Response(blk); | |||||
| // r.body.pipeTo(fileStream); | |||||
| writer.write(new Uint8Array(blk)); | |||||
| }); | |||||
| } | |||||
| return prom_1; | |||||
| }); | |||||
| prom = prom.then(() => { | |||||
| writer.close(); | |||||
| done = true; | |||||
| }); | |||||
| } | |||||
| render() { | |||||
| return ( | |||||
| <div class="container-fluid"> | |||||
| <div class="card my-3"> | |||||
| <div class="card-body"> | |||||
| Downloading, please wait... | |||||
| </div> | |||||
| </div> | |||||
| <div class="alert alert-danger" role="alert"> | |||||
| Do not close this window until the download is finished. | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| } | |||||
| export default WBDownloadPage; | |||||