Browse Source

引入streamsaver和web-streams-polyfill实现前端超大文件的下载,即不等待所有数据流传输结束再执行保存,而是实现了实时的下载和进度查看

docker-svn
witersen 3 years ago
parent
commit
f3c914bf5a
  1. 14
      01.web/package-lock.json
  2. 2
      01.web/package.json
  3. 317
      01.web/src/libs/streamsaver/StreamSaver.js
  4. 129
      01.web/src/libs/streamsaver/sw.js
  5. 2
      01.web/src/main.js
  6. 62
      01.web/src/views/repositoryInfo/index.vue

14
01.web/package-lock.json generated

@ -4,6 +4,12 @@ @@ -4,6 +4,12 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/streamsaver": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/@types/streamsaver/-/streamsaver-2.0.1.tgz",
"integrity": "sha512-I49NtT8w6syBI3Zg3ixCyygTHoTVMY0z2TMRcTgccdIsVd2MwlKk7ITLHLsJtgchUHcOd7QEARG9h0ifcA6l2Q==",
"dev": true
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -8099,7 +8105,8 @@ @@ -8099,7 +8105,8 @@
"streamsaver": {
"version": "2.0.6",
"resolved": "https://registry.npmmirror.com/streamsaver/-/streamsaver-2.0.6.tgz",
"integrity": "sha512-LK4e7TfCV8HzuM0PKXuVUfKyCB1FtT9L0EGxsFk5Up8njj0bXK8pJM9+Wq2Nya7/jslmCQwRK39LFm55h7NBTw=="
"integrity": "sha512-LK4e7TfCV8HzuM0PKXuVUfKyCB1FtT9L0EGxsFk5Up8njj0bXK8pJM9+Wq2Nya7/jslmCQwRK39LFm55h7NBTw==",
"dev": true
},
"strict-uri-encode": {
"version": "1.1.0",
@ -9133,6 +9140,11 @@ @@ -9133,6 +9140,11 @@
"minimalistic-assert": "^1.0.0"
}
},
"web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
},
"webpack": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-2.7.0.tgz",

2
01.web/package.json

@ -18,11 +18,11 @@ @@ -18,11 +18,11 @@
"axios": "^0.25.0",
"babel-polyfill": "^6.26.0",
"file-saver": "^2.0.5",
"streamsaver": "^2.0.6",
"view-design": "^4.7.0",
"vue": "^2.5.16",
"vue-clipboard2": "^0.3.3",
"vue-router": "^2.8.1",
"web-streams-polyfill": "^3.2.1",
"xlsx": "^0.18.5"
},
"devDependencies": {

317
01.web/src/libs/streamsaver/StreamSaver.js

@ -0,0 +1,317 @@ @@ -0,0 +1,317 @@
/*! streamsaver. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
/* 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'
const global = typeof window === 'object' ? window : this
if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread')
let mitmTransporter = null
let supportsTransferable = false
const test = fn => { try { fn() } catch (e) {} }
const ponyfill = global.WebStreamsPolyfill || {}
const isSecureContext = global.isSecureContext
// TODO: Must come up with a real detection test (#69)
let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint
const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style
? 'iframe'
: 'navigate'
const streamSaver = {
createWriteStream,
WritableStream: global.WritableStream || ponyfill.WritableStream,
supported: true,
version: { full: '2.0.5', major: 2, minor: 0, dot: 5 },
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: global.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
global.removeEventListener('message', onReady)
popup.dispatchEvent(new Event('load'))
}
}
global.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(() => {
// Transferable 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 deprecated
* @return {WritableStream<Uint8Array>}
*/
function createWriteStream (filename, options, size) {
let opts = {
size: null,
pathname: null,
writableStrategy: undefined,
readableStrategy: undefined
}
let bytesWritten = 0 // by StreamSaver.js (not the service worker)
let downloadUrl = null
let channel = null
let ts = null
// normalize arguments
if (Number.isFinite(options)) {
[ size, options ] = [ options, size ]
console.warn('[StreamSaver] Deprecated 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] Deprecated pass an object as 2nd argument when creating a write stream')
opts.size = size
opts.writableStrategy = options
} else {
opts = options || {}
}
if (!useBlobFallback) {
loadTransporter()
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': 'application/octet-stream; charset=utf-8',
'Content-Disposition': "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) {
if (!(chunk instanceof Uint8Array)) {
throw new TypeError('Can only write Uint8Arrays')
}
bytesWritten += chunk.length
controller.enqueue(chunk)
if (downloadUrl) {
location.href = downloadUrl
downloadUrl = null
}
},
flush () {
if (downloadUrl) {
location.href = downloadUrl
}
}
}
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()
mitmTransporter = null
// 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)
}
} else if (evt.data.abort) {
chunks = []
channel.port1.postMessage('abort') //send back so controller is aborted
channel.port1.onmessage = null
channel.port1.close()
channel.port2.close()
channel = null
}
}
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 (!(chunk instanceof Uint8Array)) {
throw new TypeError('Can only write Uint8Arrays')
}
if (useBlobFallback) {
// Safari... The new IE6
// https://github.com/jimmywarting/StreamSaver.js/issues/69
//
// even though 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: Transferable streams solves 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
})

129
01.web/src/libs/streamsaver/sw.js

@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
/* global self ReadableStream Response */
self.addEventListener('install', () => {
self.skipWaiting()
})
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim())
})
const map = new Map()
// This should be called once per download
// Each event has a dataChannel that the data will be piped through
self.onmessage = event => {
// We send a heartbeat every x second to keep the
// service worker alive if a transferable stream is not sent
if (event.data === 'ping') {
return
}
const data = event.data
const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename)
const port = event.ports[0]
const metadata = new Array(3) // [stream, data, port]
metadata[1] = data
metadata[2] = port
// Note to self:
// old streamsaver v1.2.0 might still use `readableStream`...
// but v2.0.0 will always transfer the stream through MessageChannel #94
if (event.data.readableStream) {
metadata[0] = event.data.readableStream
} else if (event.data.transferringReadable) {
port.onmessage = evt => {
port.onmessage = null
metadata[0] = evt.data.readableStream
}
} else {
metadata[0] = createStream(port)
}
map.set(downloadUrl, metadata)
port.postMessage({ download: downloadUrl })
}
function createStream (port) {
// ReadableStream is only supported by chrome 52
return new ReadableStream({
start (controller) {
// When we receive data on the messageChannel, we write
port.onmessage = ({ data }) => {
if (data === 'end') {
return controller.close()
}
if (data === 'abort') {
controller.error('Aborted the download')
return
}
controller.enqueue(data)
}
},
cancel (reason) {
console.log('user aborted', reason)
port.postMessage({ abort: true })
}
})
}
self.onfetch = event => {
const url = event.request.url
// this only works for Firefox
if (url.endsWith('/ping')) {
return event.respondWith(new Response('pong'))
}
const hijacke = map.get(url)
if (!hijacke) return null
const [ stream, data, port ] = hijacke
map.delete(url)
// Not comfortable letting any user control all headers
// so we only copy over the length & disposition
const responseHeaders = new Headers({
'Content-Type': 'application/octet-stream; charset=utf-8',
// To be on the safe side, The link can be opened in a iframe.
// but octet-stream should stop it.
'Content-Security-Policy': "default-src 'none'",
'X-Content-Security-Policy': "default-src 'none'",
'X-WebKit-CSP': "default-src 'none'",
'X-XSS-Protection': '1; mode=block'
})
let headers = new Headers(data.headers || {})
if (headers.has('Content-Length')) {
responseHeaders.set('Content-Length', headers.get('Content-Length'))
}
if (headers.has('Content-Disposition')) {
responseHeaders.set('Content-Disposition', headers.get('Content-Disposition'))
}
// data, data.filename and size should not be used anymore
if (data.size) {
console.warn('Depricated')
responseHeaders.set('Content-Length', data.size)
}
let fileName = typeof data === 'string' ? data : data.filename
if (fileName) {
console.warn('Depricated')
// Make filename RFC5987 compatible
fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A')
responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName)
}
event.respondWith(new Response(stream, { headers: responseHeaders }))
port.postMessage({ debug: 'Download started' })
}

2
01.web/src/main.js

@ -23,8 +23,6 @@ Vue.use(VueClipboard) @@ -23,8 +23,6 @@ Vue.use(VueClipboard)
Vue.use(VueRouter);
Vue.use(ViewUI);
// import streamSaver from 'streamsaver'
// 路由配置
const RouterConfig = {
mode: 'hash',

62
01.web/src/views/repositoryInfo/index.vue

@ -590,6 +590,7 @@ @@ -590,6 +590,7 @@
</template>
<script>
import { WritableStream } from "web-streams-polyfill/ponyfill";
export default {
data() {
return {
@ -1554,32 +1555,45 @@ export default { @@ -1554,32 +1555,45 @@ export default {
*/
DownloadRepBackup(fileName) {
var that = this;
var data = {
const streamSaver = require("../../libs/streamsaver/StreamSaver");
fetch("/api/Svnrep/DownloadRepBackup?t=web", {
headers: {
"Content-Type": "application/json",
token: that.token,
},
method: "POST",
body: JSON.stringify({
fileName: fileName,
};
that.$axios.setAttribute;
that.$axios
.post("/api/Svnrep/DownloadRepBackup?t=web", data, {
responseType: "blob",
})
.then(function (response) {
let url = window.URL.createObjectURL(
new Blob([response.data], { type: "application/octet-stream" })
}),
}).then((response) => {
// These code section is adapted from an example of the StreamSaver.js
// https://jimmywarting.github.io/StreamSaver.js/examples/fetch.html
// If the WritableStream is not available (Firefox, Safari), take it from the ponyfill
if (!window.WritableStream) {
streamSaver.WritableStream = WritableStream;
window.WritableStream = WritableStream;
}
const fileStream = streamSaver.createWriteStream(fileName);
const readableStream = response.body;
// More optimized
if (readableStream.pipeTo) {
return readableStream.pipeTo(fileStream);
}
window.writer = fileStream.getWriter();
const reader = response.body.getReader();
const pump = () =>
reader
.read()
.then((res) =>
res.done ? writer.close() : writer.write(res.value).then(pump)
);
let link = document.createElement("a");
link.style.display = "none";
link.href = url;
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
//url
window.URL.revokeObjectURL(url);
//
document.body.removeChild(link);
})
.catch(function (error) {
console.log(error);
that.$Message.error("出错了 请联系管理员!");
pump();
});
},
/**

Loading…
Cancel
Save