Нема описа
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

StreamSaver.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /*! streamsaver. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
  2. /* global chrome location ReadableStream define MessageChannel TransformStream */
  3. ;((name, definition) => {
  4. typeof module !== 'undefined'
  5. ? module.exports = definition()
  6. : typeof define === 'function' && typeof define.amd === 'object'
  7. ? define(definition)
  8. : this[name] = definition()
  9. })('streamSaver', () => {
  10. 'use strict'
  11. const global = typeof window === 'object' ? window : this
  12. if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread')
  13. let mitmTransporter = null
  14. let supportsTransferable = false
  15. const test = fn => { try { fn() } catch (e) {} }
  16. const ponyfill = global.WebStreamsPolyfill || {}
  17. const isSecureContext = global.isSecureContext
  18. // TODO: Must come up with a real detection test (#69)
  19. let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint
  20. const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style
  21. ? 'iframe'
  22. : 'navigate'
  23. const streamSaver = {
  24. createWriteStream,
  25. WritableStream: global.WritableStream || ponyfill.WritableStream,
  26. supported: true,
  27. version: { full: '2.0.5', major: 2, minor: 0, dot: 5 },
  28. mitm: 'https://www.zc10000.com/template/pc/js/down/mitm.html?version=2.0.0'
  29. }
  30. /**
  31. * create a hidden iframe and append it to the DOM (body)
  32. *
  33. * @param {string} src page to load
  34. * @return {HTMLIFrameElement} page to load
  35. */
  36. function makeIframe (src) {
  37. if (!src) throw new Error('meh')
  38. const iframe = document.createElement('iframe')
  39. iframe.hidden = true
  40. iframe.src = src
  41. iframe.loaded = false
  42. iframe.name = 'iframe'
  43. iframe.isIframe = true
  44. iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
  45. iframe.addEventListener('load', () => {
  46. iframe.loaded = true
  47. }, { once: true })
  48. document.body.appendChild(iframe)
  49. return iframe
  50. }
  51. /**
  52. * create a popup that simulates the basic things
  53. * of what a iframe can do
  54. *
  55. * @param {string} src page to load
  56. * @return {object} iframe like object
  57. */
  58. function makePopup (src) {
  59. const options = 'width=200,height=100'
  60. const delegate = document.createDocumentFragment()
  61. const popup = {
  62. frame: global.open(src, 'popup', options),
  63. loaded: false,
  64. isIframe: false,
  65. isPopup: true,
  66. remove () { popup.frame.close() },
  67. addEventListener (...args) { delegate.addEventListener(...args) },
  68. dispatchEvent (...args) { delegate.dispatchEvent(...args) },
  69. removeEventListener (...args) { delegate.removeEventListener(...args) },
  70. postMessage (...args) { popup.frame.postMessage(...args) }
  71. }
  72. const onReady = evt => {
  73. if (evt.source === popup.frame) {
  74. popup.loaded = true
  75. global.removeEventListener('message', onReady)
  76. popup.dispatchEvent(new Event('load'))
  77. }
  78. }
  79. global.addEventListener('message', onReady)
  80. return popup
  81. }
  82. try {
  83. // We can't look for service worker since it may still work on http
  84. new Response(new ReadableStream())
  85. if (isSecureContext && !('serviceWorker' in navigator)) {
  86. useBlobFallback = true
  87. }
  88. } catch (err) {
  89. useBlobFallback = true
  90. }
  91. test(() => {
  92. // Transferable stream was first enabled in chrome v73 behind a flag
  93. const { readable } = new TransformStream()
  94. const mc = new MessageChannel()
  95. mc.port1.postMessage(readable, [readable])
  96. mc.port1.close()
  97. mc.port2.close()
  98. supportsTransferable = true
  99. // Freeze TransformStream object (can only work with native)
  100. Object.defineProperty(streamSaver, 'TransformStream', {
  101. configurable: false,
  102. writable: false,
  103. value: TransformStream
  104. })
  105. })
  106. function loadTransporter () {
  107. if (!mitmTransporter) {
  108. mitmTransporter = isSecureContext
  109. ? makeIframe(streamSaver.mitm)
  110. : makePopup(streamSaver.mitm)
  111. }
  112. }
  113. /**
  114. * @param {string} filename filename that should be used
  115. * @param {object} options [description]
  116. * @param {number} size deprecated
  117. * @return {WritableStream<Uint8Array>}
  118. */
  119. function createWriteStream (filename, options, size) {
  120. let opts = {
  121. size: null,
  122. pathname: null,
  123. writableStrategy: undefined,
  124. readableStrategy: undefined
  125. }
  126. let bytesWritten = 0 // by StreamSaver.js (not the service worker)
  127. let downloadUrl = null
  128. let channel = null
  129. let ts = null
  130. // normalize arguments
  131. if (Number.isFinite(options)) {
  132. [ size, options ] = [ options, size ]
  133. console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')
  134. opts.size = size
  135. opts.writableStrategy = options
  136. } else if (options && options.highWaterMark) {
  137. console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')
  138. opts.size = size
  139. opts.writableStrategy = options
  140. } else {
  141. opts = options || {}
  142. }
  143. if (!useBlobFallback) {
  144. loadTransporter()
  145. channel = new MessageChannel()
  146. // Make filename RFC5987 compatible
  147. filename = encodeURIComponent(filename.replace(/\//g, ':'))
  148. .replace(/['()]/g, escape)
  149. .replace(/\*/g, '%2A')
  150. const response = {
  151. transferringReadable: supportsTransferable,
  152. pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,
  153. headers: {
  154. 'Content-Type': 'application/octet-stream; charset=utf-8',
  155. 'Content-Disposition': "attachment; filename*=UTF-8''" + filename
  156. }
  157. }
  158. if (opts.size) {
  159. response.headers['Content-Length'] = opts.size
  160. }
  161. const args = [ response, '*', [ channel.port2 ] ]
  162. if (supportsTransferable) {
  163. const transformer = downloadStrategy === 'iframe' ? undefined : {
  164. // This transformer & flush method is only used by insecure context.
  165. transform (chunk, controller) {
  166. if (!(chunk instanceof Uint8Array)) {
  167. throw new TypeError('Can only write Uint8Arrays')
  168. }
  169. bytesWritten += chunk.length
  170. controller.enqueue(chunk)
  171. if (downloadUrl) {
  172. location.href = downloadUrl
  173. downloadUrl = null
  174. }
  175. },
  176. flush () {
  177. if (downloadUrl) {
  178. location.href = downloadUrl
  179. }
  180. }
  181. }
  182. ts = new streamSaver.TransformStream(
  183. transformer,
  184. opts.writableStrategy,
  185. opts.readableStrategy
  186. )
  187. const readableStream = ts.readable
  188. channel.port1.postMessage({ readableStream }, [ readableStream ])
  189. }
  190. channel.port1.onmessage = evt => {
  191. // Service worker sent us a link that we should open.
  192. if (evt.data.download) {
  193. // Special treatment for popup...
  194. if (downloadStrategy === 'navigate') {
  195. mitmTransporter.remove()
  196. mitmTransporter = null
  197. if (bytesWritten) {
  198. location.href = evt.data.download
  199. } else {
  200. downloadUrl = evt.data.download
  201. }
  202. } else {
  203. if (mitmTransporter.isPopup) {
  204. mitmTransporter.remove()
  205. mitmTransporter = null
  206. // Special case for firefox, they can keep sw alive with fetch
  207. if (downloadStrategy === 'iframe') {
  208. makeIframe(streamSaver.mitm)
  209. }
  210. }
  211. // We never remove this iframes b/c it can interrupt saving
  212. makeIframe(evt.data.download)
  213. }
  214. } else if (evt.data.abort) {
  215. chunks = []
  216. channel.port1.postMessage('abort') //send back so controller is aborted
  217. channel.port1.onmessage = null
  218. channel.port1.close()
  219. channel.port2.close()
  220. channel = null
  221. }
  222. }
  223. if (mitmTransporter.loaded) {
  224. mitmTransporter.postMessage(...args)
  225. } else {
  226. mitmTransporter.addEventListener('load', () => {
  227. mitmTransporter.postMessage(...args)
  228. }, { once: true })
  229. }
  230. }
  231. let chunks = []
  232. return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
  233. write (chunk) {
  234. if (!(chunk instanceof Uint8Array)) {
  235. throw new TypeError('Can only write Uint8Arrays')
  236. }
  237. if (useBlobFallback) {
  238. // Safari... The new IE6
  239. // https://github.com/jimmywarting/StreamSaver.js/issues/69
  240. //
  241. // even though it has everything it fails to download anything
  242. // that comes from the service worker..!
  243. chunks.push(chunk)
  244. return
  245. }
  246. // is called when a new chunk of data is ready to be written
  247. // to the underlying sink. It can return a promise to signal
  248. // success or failure of the write operation. The stream
  249. // implementation guarantees that this method will be called
  250. // only after previous writes have succeeded, and never after
  251. // close or abort is called.
  252. // TODO: Kind of important that service worker respond back when
  253. // it has been written. Otherwise we can't handle backpressure
  254. // EDIT: Transferable streams solves this...
  255. channel.port1.postMessage(chunk)
  256. bytesWritten += chunk.length
  257. if (downloadUrl) {
  258. location.href = downloadUrl
  259. downloadUrl = null
  260. }
  261. },
  262. close () {
  263. if (useBlobFallback) {
  264. const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
  265. const link = document.createElement('a')
  266. link.href = URL.createObjectURL(blob)
  267. link.download = filename
  268. link.click()
  269. } else {
  270. channel.port1.postMessage('end')
  271. }
  272. },
  273. abort () {
  274. chunks = []
  275. channel.port1.postMessage('abort')
  276. channel.port1.onmessage = null
  277. channel.port1.close()
  278. channel.port2.close()
  279. channel = null
  280. }
  281. }, opts.writableStrategy)
  282. }
  283. return streamSaver
  284. })