uploader.js 10 KB

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