@@ -47,6 +47,10 @@ export default { | |||
'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', | |||
'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 | |||
}), | |||
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/all.min.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"> | |||
window.process = { 'env': { 'NODE_ENV': 'production' } }; | |||
</script> | |||
@@ -15,6 +16,7 @@ | |||
<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/js-yaml.min.js"></script> | |||
<script language="javascript" src="/js/StreamSaver.js"></script> | |||
</head> | |||
<body> | |||
<script language="javascript" src="/js/app.min.js"></script> | |||
@@ -1,9 +1,8 @@ | |||
import { h, Component } from 'preact'; | |||
import WBTable from 'wb-table'; | |||
import WBBreadcrumbs from 'wb-breadcrumbs'; | |||
// import { WBManifestReader } from 'wb-collection-manifest'; | |||
// import WBManifestReader from 'wb-manifest-reader'; | |||
import WBPagination from 'wb-pagination'; | |||
import WBRootDirWrapper from 'wb-rootdir-wrapper'; | |||
import makeArvadosRequest from 'make-arvados-request'; | |||
import wbDownloadFile from 'wb-download-file'; | |||
@@ -23,6 +22,7 @@ class WBCollectionContent extends Component { | |||
this.state.mode = 'manifestDownload'; | |||
this.state.parsedStreams = 0; | |||
this.state.totalStreams = 1; | |||
this.state.rootDirWrapper = null; | |||
} | |||
getUrl(params) { | |||
@@ -95,10 +95,20 @@ class WBCollectionContent extends Component { | |||
} | |||
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({ | |||
'mode': 'browsingReady' | |||
}); | |||
this.prepareRows(); | |||
this.prepareRows(this.state.rootDirWrapper.listDirectory('.' + | |||
this.props.collectionPath)); | |||
}); | |||
return prom_1; | |||
@@ -106,38 +116,22 @@ class WBCollectionContent extends Component { | |||
} | |||
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) { | |||
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); | |||
listing = listing.slice(page * itemsPerPage, | |||
page * itemsPerPage + itemsPerPage); | |||
@@ -158,25 +152,17 @@ class WBCollectionContent extends Component { | |||
<div> | |||
<button class="btn btn-outline-primary mx-1" title="Download" | |||
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> | |||
<button class="btn btn-outline-primary mx-1" title="View" | |||
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> | |||
</div> | |||
) : null) | |||
@@ -16,6 +16,9 @@ onmessage = function(e) { | |||
const lst = listDirectory(rootDir, e.data[1], e.data[2]); | |||
postMessage([ 'listDirectoryResult', lst ]) | |||
break; } | |||
case 'getData': { | |||
postMessage([ 'getDataResult', rootDir, streams ]); | |||
break; } | |||
default: | |||
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 WBWorkflowView from 'wb-workflow-view'; | |||
import WBLaunchWorkflowPage from 'wb-launch-workflow-page'; | |||
import WBDownloadPage from 'wb-download-page'; | |||
import arvadosTypeName from 'arvados-type-name'; | |||
class WBApp extends Component { | |||
@@ -91,6 +92,8 @@ class WBApp extends Component { | |||
<WBWorkflowView path="/workflow/:uuid" app={ this } /> | |||
<WBLaunchWorkflowPage path="/workflow-launch/:workflowUuid" app={ this } /> | |||
<WBDownloadPage path="/download/:blocksBlobUrl" app={ this } /> | |||
</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; |