uploader.js 11 KB

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