uploader.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. 'use strict'
  2. jest.mock('tus-js-client')
  3. const intoStream = require('into-stream')
  4. const fs = require('fs')
  5. const nock = require('nock')
  6. const Uploader = require('../../src/server/Uploader')
  7. const socketClient = require('../mocksocket')
  8. const standalone = require('../../src/standalone')
  9. const Emitter = require('../../src/server/emitter')
  10. afterAll(() => {
  11. nock.cleanAll()
  12. nock.restore()
  13. })
  14. process.env.COMPANION_DATADIR = './test/output'
  15. process.env.COMPANION_DOMAIN = 'localhost:3020'
  16. const { companionOptions } = standalone()
  17. const protocol = 'tus'
  18. describe('uploader with tus protocol', () => {
  19. test('uploader respects uploadUrls', async () => {
  20. const opts = {
  21. endpoint: 'http://localhost/files',
  22. protocol,
  23. companionOptions: { ...companionOptions, uploadUrls: [/^http:\/\/url.myendpoint.com\//] },
  24. }
  25. expect(() => new Uploader(opts)).toThrow(new Uploader.ValidationError('upload destination does not match any allowed destinations'))
  26. })
  27. test('uploader respects uploadUrls, valid', async () => {
  28. const opts = {
  29. endpoint: 'http://url.myendpoint.com/files',
  30. protocol,
  31. companionOptions: { ...companionOptions, uploadUrls: [/^http:\/\/url.myendpoint.com\//] },
  32. }
  33. // eslint-disable-next-line no-new
  34. new Uploader(opts) // no validation error
  35. })
  36. test('uploader respects uploadUrls, localhost', async () => {
  37. const opts = {
  38. endpoint: 'http://localhost:1337/',
  39. protocol,
  40. companionOptions: { ...companionOptions, uploadUrls: [/^http:\/\/localhost:1337\//] },
  41. }
  42. // eslint-disable-next-line no-new
  43. new Uploader(opts) // no validation error
  44. })
  45. test('upload functions with tus protocol', async () => {
  46. const fileContent = Buffer.from('Some file content')
  47. const stream = intoStream(fileContent)
  48. const opts = {
  49. companionOptions,
  50. endpoint: 'http://url.myendpoint.com/files',
  51. protocol: 'tus',
  52. size: fileContent.length,
  53. pathPrefix: companionOptions.filePath,
  54. }
  55. const uploader = new Uploader(opts)
  56. const uploadToken = uploader.token
  57. expect(uploadToken).toBeTruthy()
  58. let progressReceived = 0
  59. const onProgress = jest.fn()
  60. const onUploadSuccess = jest.fn()
  61. const onBeginUploadEvent = jest.fn()
  62. const onUploadEvent = jest.fn()
  63. const emitter = Emitter()
  64. emitter.on('upload-start', onBeginUploadEvent)
  65. emitter.on(uploadToken, onUploadEvent)
  66. const promise = uploader.awaitReady(60000)
  67. // emulate socket connection
  68. socketClient.connect(uploadToken)
  69. socketClient.onProgress(uploadToken, (message) => {
  70. progressReceived = message.payload.bytesUploaded
  71. onProgress(message)
  72. })
  73. socketClient.onUploadSuccess(uploadToken, onUploadSuccess)
  74. await promise
  75. await uploader.tryUploadStream(stream)
  76. expect(progressReceived).toBe(fileContent.length)
  77. expect(onProgress).toHaveBeenLastCalledWith(expect.objectContaining({
  78. payload: expect.objectContaining({
  79. bytesTotal: fileContent.length,
  80. }),
  81. }))
  82. const expectedPayload = expect.objectContaining({
  83. // see __mocks__/tus-js-client.js
  84. url: 'https://tus.endpoint/files/foo-bar',
  85. })
  86. expect(onUploadSuccess).toHaveBeenCalledWith(expect.objectContaining({
  87. payload: expectedPayload,
  88. }))
  89. expect(onBeginUploadEvent).toHaveBeenCalledWith({ token: uploadToken })
  90. expect(onUploadEvent).toHaveBeenLastCalledWith({ action: 'success', payload: expectedPayload })
  91. })
  92. test('upload functions with tus protocol without size', async () => {
  93. const fileContent = Buffer.alloc(1e6)
  94. const stream = intoStream(fileContent)
  95. const opts = {
  96. companionOptions,
  97. endpoint: 'http://url.myendpoint.com/files',
  98. protocol: 'tus',
  99. size: null,
  100. pathPrefix: companionOptions.filePath,
  101. }
  102. const uploader = new Uploader(opts)
  103. const uploadToken = uploader.token
  104. expect(uploadToken).toBeTruthy()
  105. return new Promise((resolve, reject) => {
  106. // validate that the test is resolved on socket connection
  107. uploader.awaitReady(60000).then(() => {
  108. uploader.tryUploadStream(stream).then(() => {
  109. try {
  110. expect(fs.existsSync(uploader.path)).toBe(false)
  111. resolve()
  112. } catch (err) {
  113. reject(err)
  114. }
  115. })
  116. })
  117. let progressReceived = 0
  118. // emulate socket connection
  119. socketClient.connect(uploadToken)
  120. socketClient.onProgress(uploadToken, (message) => {
  121. // validate that the file has been downloaded and saved into the file path
  122. try {
  123. progressReceived = message.payload.bytesUploaded
  124. if (progressReceived === fileContent.length) {
  125. const fileInfo = fs.statSync(uploader.tmpPath)
  126. expect(fileInfo.isFile()).toBe(true)
  127. expect(fileInfo.size).toBe(fileContent.length)
  128. expect(message.payload.bytesTotal).toBe(fileContent.length)
  129. }
  130. } catch (err) {
  131. reject(err)
  132. }
  133. })
  134. socketClient.onUploadSuccess(uploadToken, (message) => {
  135. try {
  136. expect(progressReceived).toBe(fileContent.length)
  137. // see __mocks__/tus-js-client.js
  138. expect(message.payload.url).toBe('https://tus.endpoint/files/foo-bar')
  139. } catch (err) {
  140. reject(err)
  141. }
  142. })
  143. })
  144. })
  145. async function runMultipartTest ({ metadata, useFormData, includeSize = true } = {}) {
  146. const fileContent = Buffer.from('Some file content')
  147. const stream = intoStream(fileContent)
  148. const opts = {
  149. companionOptions,
  150. endpoint: 'http://localhost',
  151. protocol: 'multipart',
  152. size: includeSize ? fileContent.length : undefined,
  153. metadata,
  154. pathPrefix: companionOptions.filePath,
  155. useFormData,
  156. }
  157. const uploader = new Uploader(opts)
  158. return uploader.uploadStream(stream)
  159. }
  160. test('upload functions with xhr protocol', async () => {
  161. nock('http://localhost').post('/').reply(200)
  162. const ret = await runMultipartTest()
  163. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  164. })
  165. // eslint-disable-next-line max-len
  166. const formDataNoMetaMatch = /^----------------------------\d+\r\nContent-Disposition: form-data; name="files\[\]"; filename="uppy-file-[^"]+"\r\nContent-Type: application\/octet-stream\r\n\r\nSome file content\r\n----------------------------\d+--\r\n$/
  167. test('upload functions with xhr formdata', async () => {
  168. nock('http://localhost').post('/', formDataNoMetaMatch)
  169. .reply(200)
  170. const ret = await runMultipartTest({ useFormData: true })
  171. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  172. })
  173. test('upload functions with unknown file size', async () => {
  174. // eslint-disable-next-line max-len
  175. nock('http://localhost').post('/', formDataNoMetaMatch)
  176. .reply(200)
  177. const ret = await runMultipartTest({ useFormData: true, includeSize: false })
  178. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  179. })
  180. // https://github.com/transloadit/uppy/issues/3477
  181. test('upload functions with xhr formdata and metadata', async () => {
  182. // eslint-disable-next-line max-len
  183. nock('http://localhost').post('/', /^----------------------------\d+\r\nContent-Disposition: form-data; name="key1"\r\n\r\nnull\r\n----------------------------\d+\r\nContent-Disposition: form-data; name="key2"\r\n\r\ntrue\r\n----------------------------\d+\r\nContent-Disposition: form-data; name="key3"\r\n\r\n\d+\r\n----------------------------\d+\r\nContent-Disposition: form-data; name="key4"\r\n\r\n\[object Object\]\r\n----------------------------\d+\r\nContent-Disposition: form-data; name="key5"\r\n\r\n\(\) => {}\r\n----------------------------\d+\r\nContent-Disposition: form-data; name="key6"\r\n\r\nSymbol\(\)\r\n----------------------------\d+\r\nContent-Disposition: form-data; name="files\[\]"; filename="uppy-file-[^"]+"\r\nContent-Type: application\/octet-stream\r\n\r\nSome file content\r\n----------------------------\d+--\r\n$/)
  184. .reply(200)
  185. const metadata = {
  186. key1: null, key2: true, key3: 1234, key4: {}, key5: () => {}, key6: Symbol(''),
  187. }
  188. const ret = await runMultipartTest({ useFormData: true, metadata })
  189. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  190. })
  191. test('uploader checks metadata', () => {
  192. const opts = {
  193. companionOptions,
  194. endpoint: 'http://localhost',
  195. protocol,
  196. }
  197. // eslint-disable-next-line no-new
  198. new Uploader({ ...opts, metadata: { key: 'string value' } })
  199. expect(() => new Uploader({ ...opts, metadata: '' })).toThrow(new Uploader.ValidationError('metadata must be an object'))
  200. })
  201. test('uploader respects maxFileSize', async () => {
  202. const opts = {
  203. endpoint: 'http://url.myendpoint.com/files',
  204. companionOptions: { ...companionOptions, maxFileSize: 100 },
  205. size: 101,
  206. }
  207. expect(() => new Uploader(opts)).toThrow(new Uploader.ValidationError('maxFileSize exceeded'))
  208. })
  209. test('uploader respects maxFileSize correctly', async () => {
  210. const opts = {
  211. endpoint: 'http://url.myendpoint.com/files',
  212. protocol,
  213. companionOptions: { ...companionOptions, maxFileSize: 100 },
  214. size: 99,
  215. }
  216. // eslint-disable-next-line no-new
  217. new Uploader(opts) // no validation error
  218. })
  219. test('uploader respects maxFileSize with unknown size', async () => {
  220. const fileContent = Buffer.alloc(10000)
  221. const stream = intoStream(fileContent)
  222. const opts = {
  223. companionOptions: { ...companionOptions, maxFileSize: 1000 },
  224. endpoint: 'http://url.myendpoint.com/files',
  225. protocol: 'tus',
  226. size: null,
  227. pathPrefix: companionOptions.filePath,
  228. }
  229. const uploader = new Uploader(opts)
  230. const uploadToken = uploader.token
  231. // validate that the test is resolved on socket connection
  232. uploader.awaitReady(60000).then(() => uploader.tryUploadStream(stream))
  233. socketClient.connect(uploadToken)
  234. return new Promise((resolve, reject) => {
  235. socketClient.onUploadError(uploadToken, (message) => {
  236. try {
  237. expect(message).toMatchObject({ payload: { error: { message: 'maxFileSize exceeded' } } })
  238. resolve()
  239. } catch (err) {
  240. reject(err)
  241. }
  242. })
  243. })
  244. })
  245. })