uploader.js 10 KB

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