diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js
index 1ac0c99..a0a4200 100755
--- a/frontend/rollup.config.js
+++ b/frontend/rollup.config.js
@@ -49,7 +49,7 @@ export default {
'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',
+ 'src/js/thirdparty/StreamSaver.js': 'dist/js/StreamSaver.js',
'node_modules/web-streams-polyfill/dist/ponyfill.js': 'dist/js/web-streams-polyfill/ponyfill.js',
verbose: true
}),
diff --git a/frontend/src/js/page/wb-app.js b/frontend/src/js/page/wb-app.js
index 3abdcd8..a086525 100644
--- a/frontend/src/js/page/wb-app.js
+++ b/frontend/src/js/page/wb-app.js
@@ -93,7 +93,7 @@ class WBApp extends Component {
-
+
);
}
diff --git a/frontend/src/js/page/wb-download-page.js b/frontend/src/js/page/wb-download-page.js
index 8660d64..4b1e748 100644
--- a/frontend/src/js/page/wb-download-page.js
+++ b/frontend/src/js/page/wb-download-page.js
@@ -1,9 +1,21 @@
import { h, Component } from 'preact';
import makeArvadosRequest from 'make-arvados-request';
+function contentTypeFromFilename(name) {
+ let ext = name.split('.');
+ ext = ext[ext.length - 1].toUpperCase();
+ if (ext === 'TXT')
+ return 'text/plain; charset=utf-8';
+ if (ext === 'JPG' || ext === 'JPEG')
+ return 'image/jpeg';
+ if (ext === 'PNG')
+ return 'image/png';
+ return 'application/octet-stream; charset=utf-8';
+}
+
class WBDownloadPage extends Component {
componentDidMount() {
- const { app, blocksBlobUrl } = this.props;
+ const { app, blocksBlobUrl, inline } = this.props;
const { arvHost, arvToken } = app.state;
let prom = new Promise((accept, reject) => {
@@ -39,7 +51,9 @@ class WBDownloadPage extends Component {
[ _, _, name, file ] = JSON.parse(text);
fileStream = streamSaver.createWriteStream(name, {
- size: file[1]
+ size: file[1],
+ inline: inline,
+ contentType: contentTypeFromFilename(name)
});
writer = fileStream.getWriter();
diff --git a/frontend/src/js/thirdparty/StreamSaver.js b/frontend/src/js/thirdparty/StreamSaver.js
new file mode 100644
index 0000000..0a596be
--- /dev/null
+++ b/frontend/src/js/thirdparty/StreamSaver.js
@@ -0,0 +1,296 @@
+/* global chrome location ReadableStream define MessageChannel TransformStream */
+
+;((name, definition) => {
+ typeof module !== 'undefined'
+ ? module.exports = definition()
+ : typeof define === 'function' && typeof define.amd === 'object'
+ ? define(definition)
+ : this[name] = definition()
+})('streamSaver', () => {
+ 'use strict'
+
+ let mitmTransporter = null
+ let supportsTransferable = false
+ const test = fn => { try { fn() } catch (e) {} }
+ const ponyfill = window.WebStreamsPolyfill || {}
+ const isSecureContext = window.isSecureContext
+ let useBlobFallback = /constructor/i.test(window.HTMLElement) || !!window.safari
+ const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style
+ ? 'iframe'
+ : 'navigate'
+
+ const streamSaver = {
+ createWriteStream,
+ WritableStream: window.WritableStream || ponyfill.WritableStream,
+ supported: true,
+ version: { full: '2.0.0', major: 2, minor: 0, dot: 0 },
+ mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0'
+ }
+
+ /**
+ * create a hidden iframe and append it to the DOM (body)
+ *
+ * @param {string} src page to load
+ * @return {HTMLIFrameElement} page to load
+ */
+ function makeIframe (src) {
+ if (!src) throw new Error('meh')
+ const iframe = document.createElement('iframe')
+ iframe.hidden = true
+ iframe.src = src
+ iframe.loaded = false
+ iframe.name = 'iframe'
+ iframe.isIframe = true
+ iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
+ iframe.addEventListener('load', () => {
+ iframe.loaded = true
+ }, { once: true })
+ document.body.appendChild(iframe)
+ return iframe
+ }
+
+ /**
+ * create a popup that simulates the basic things
+ * of what a iframe can do
+ *
+ * @param {string} src page to load
+ * @return {object} iframe like object
+ */
+ function makePopup (src) {
+ const options = 'width=200,height=100'
+ const delegate = document.createDocumentFragment()
+ const popup = {
+ frame: window.open(src, 'popup', options),
+ loaded: false,
+ isIframe: false,
+ isPopup: true,
+ remove () { popup.frame.close() },
+ addEventListener (...args) { delegate.addEventListener(...args) },
+ dispatchEvent (...args) { delegate.dispatchEvent(...args) },
+ removeEventListener (...args) { delegate.removeEventListener(...args) },
+ postMessage (...args) { popup.frame.postMessage(...args) }
+ }
+
+ const onReady = evt => {
+ if (evt.source === popup.frame) {
+ popup.loaded = true
+ window.removeEventListener('message', onReady)
+ popup.dispatchEvent(new Event('load'))
+ }
+ }
+
+ window.addEventListener('message', onReady)
+
+ return popup
+ }
+
+ try {
+ // We can't look for service worker since it may still work on http
+ new Response(new ReadableStream())
+ if (isSecureContext && !('serviceWorker' in navigator)) {
+ useBlobFallback = true
+ }
+ } catch (err) {
+ useBlobFallback = true
+ }
+
+ test(() => {
+ // Transfariable stream was first enabled in chrome v73 behind a flag
+ const { readable } = new TransformStream()
+ const mc = new MessageChannel()
+ mc.port1.postMessage(readable, [readable])
+ mc.port1.close()
+ mc.port2.close()
+ supportsTransferable = true
+ // Freeze TransformStream object (can only work with native)
+ Object.defineProperty(streamSaver, 'TransformStream', {
+ configurable: false,
+ writable: false,
+ value: TransformStream
+ })
+ })
+
+ function loadTransporter () {
+ if (!mitmTransporter) {
+ mitmTransporter = isSecureContext
+ ? makeIframe(streamSaver.mitm)
+ : makePopup(streamSaver.mitm)
+ }
+ }
+
+ /**
+ * @param {string} filename filename that should be used
+ * @param {object} options [description]
+ * @param {number} size depricated
+ * @return {WritableStream}
+ */
+ function createWriteStream (filename, options, size) {
+ let opts = {
+ size: null,
+ pathname: null,
+ writableStrategy: undefined,
+ readableStrategy: undefined
+ }
+
+ // normalize arguments
+ if (Number.isFinite(options)) {
+ [ size, options ] = [ options, size ]
+ console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream')
+ opts.size = size
+ opts.writableStrategy = options
+ } else if (options && options.highWaterMark) {
+ console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream')
+ opts.size = size
+ opts.writableStrategy = options
+ } else {
+ opts = options || {}
+ }
+ if (!useBlobFallback) {
+ loadTransporter()
+
+ var bytesWritten = 0 // by StreamSaver.js (not the service worker)
+ var downloadUrl = null
+ var channel = new MessageChannel()
+
+ // Make filename RFC5987 compatible
+ filename = encodeURIComponent(filename.replace(/\//g, ':'))
+ .replace(/['()]/g, escape)
+ .replace(/\*/g, '%2A')
+
+ const response = {
+ transferringReadable: supportsTransferable,
+ pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,
+ headers: {
+ 'Content-Type': (options.contentType ? options.contentType :
+ 'application/octet-stream; charset=utf-8'),
+ 'Content-Disposition': (options.inline ? 'inline' :
+ ("attachment; filename*=UTF-8''" + filename))
+ }
+ }
+
+ if (opts.size) {
+ response.headers['Content-Length'] = opts.size
+ }
+
+ const args = [ response, '*', [ channel.port2 ] ]
+
+ if (supportsTransferable) {
+ const transformer = downloadStrategy === 'iframe' ? undefined : {
+ // This transformer & flush method is only used by insecure context.
+ transform (chunk, controller) {
+ bytesWritten += chunk.length
+ controller.enqueue(chunk)
+
+ if (downloadUrl) {
+ location.href = downloadUrl
+ downloadUrl = null
+ }
+ },
+ flush () {
+ if (downloadUrl) {
+ location.href = downloadUrl
+ }
+ }
+ }
+ var ts = new streamSaver.TransformStream(
+ transformer,
+ opts.writableStrategy,
+ opts.readableStrategy
+ )
+ const readableStream = ts.readable
+
+ channel.port1.postMessage({ readableStream }, [ readableStream ])
+ }
+
+ channel.port1.onmessage = evt => {
+ // Service worker sent us a link that we should open.
+ if (evt.data.download) {
+ // Special treatment for popup...
+ if (downloadStrategy === 'navigate') {
+ mitmTransporter.remove()
+ mitmTransporter = null
+ if (bytesWritten) {
+ location.href = evt.data.download
+ } else {
+ downloadUrl = evt.data.download
+ }
+ } else {
+ if (mitmTransporter.isPopup) {
+ mitmTransporter.remove()
+ // Special case for firefox, they can keep sw alive with fetch
+ if (downloadStrategy === 'iframe') {
+ makeIframe(streamSaver.mitm)
+ }
+ }
+
+ // We never remove this iframes b/c it can interrupt saving
+ makeIframe(evt.data.download)
+ }
+ }
+ }
+
+ if (mitmTransporter.loaded) {
+ mitmTransporter.postMessage(...args)
+ } else {
+ mitmTransporter.addEventListener('load', () => {
+ mitmTransporter.postMessage(...args)
+ }, { once: true })
+ }
+ }
+
+ let chunks = []
+
+ return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
+ write (chunk) {
+ if (useBlobFallback) {
+ // Safari... The new IE6
+ // https://github.com/jimmywarting/StreamSaver.js/issues/69
+ //
+ // even doe it has everything it fails to download anything
+ // that comes from the service worker..!
+ chunks.push(chunk)
+ return
+ }
+
+ // is called when a new chunk of data is ready to be written
+ // to the underlying sink. It can return a promise to signal
+ // success or failure of the write operation. The stream
+ // implementation guarantees that this method will be called
+ // only after previous writes have succeeded, and never after
+ // close or abort is called.
+
+ // TODO: Kind of important that service worker respond back when
+ // it has been written. Otherwise we can't handle backpressure
+ // EDIT: Transfarable streams solvs this...
+ channel.port1.postMessage(chunk)
+ bytesWritten += chunk.length
+
+ if (downloadUrl) {
+ location.href = downloadUrl
+ downloadUrl = null
+ }
+ },
+ close () {
+ if (useBlobFallback) {
+ const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
+ const link = document.createElement('a')
+ link.href = URL.createObjectURL(blob)
+ link.download = filename
+ link.click()
+ } else {
+ channel.port1.postMessage('end')
+ }
+ },
+ abort () {
+ chunks = []
+ channel.port1.postMessage('abort')
+ channel.port1.onmessage = null
+ channel.port1.close()
+ channel.port2.close()
+ channel = null
+ }
+ }, opts.writableStrategy)
+ }
+
+ return streamSaver
+})