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 progressReceived = 0
  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. progressReceived = message.payload.bytesUploaded
  67. onProgress(message)
  68. })
  69. socketClient.onUploadSuccess(uploadToken, onUploadSuccess)
  70. await promise
  71. await uploader.tryUploadStream(stream)
  72. expect(progressReceived).toBe(fileContent.length)
  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. const uploadToken = uploader.token
  100. expect(uploadToken).toBeTruthy()
  101. return new Promise((resolve, reject) => {
  102. // validate that the test is resolved on socket connection
  103. uploader.awaitReady(60000).then(() => {
  104. uploader.tryUploadStream(stream).then(() => {
  105. try {
  106. expect(fs.existsSync(uploader.path)).toBe(false)
  107. resolve()
  108. } catch (err) {
  109. reject(err)
  110. }
  111. })
  112. })
  113. let progressReceived = 0
  114. // emulate socket connection
  115. socketClient.connect(uploadToken)
  116. socketClient.onProgress(uploadToken, (message) => {
  117. // validate that the file has been downloaded and saved into the file path
  118. try {
  119. progressReceived = message.payload.bytesUploaded
  120. if (progressReceived === fileContent.length) {
  121. const fileInfo = fs.statSync(uploader.tmpPath)
  122. expect(fileInfo.isFile()).toBe(true)
  123. expect(fileInfo.size).toBe(fileContent.length)
  124. expect(message.payload.bytesTotal).toBe(fileContent.length)
  125. }
  126. } catch (err) {
  127. reject(err)
  128. }
  129. })
  130. socketClient.onUploadSuccess(uploadToken, (message) => {
  131. try {
  132. expect(progressReceived).toBe(fileContent.length)
  133. // see __mocks__/tus-js-client.js
  134. expect(message.payload.url).toBe('https://tus.endpoint/files/foo-bar')
  135. } catch (err) {
  136. reject(err)
  137. }
  138. })
  139. })
  140. })
  141. async function runMultipartTest ({ metadata, useFormData, includeSize = true } = {}) {
  142. const fileContent = Buffer.from('Some file content')
  143. const stream = intoStream(fileContent)
  144. const opts = {
  145. companionOptions,
  146. endpoint: 'http://localhost',
  147. protocol: 'multipart',
  148. size: includeSize ? fileContent.length : undefined,
  149. metadata,
  150. pathPrefix: companionOptions.filePath,
  151. useFormData,
  152. }
  153. const uploader = new Uploader(opts)
  154. return uploader.uploadStream(stream)
  155. }
  156. test('upload functions with xhr protocol', async () => {
  157. nock('http://localhost').post('/').reply(200)
  158. const ret = await runMultipartTest()
  159. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  160. })
  161. // eslint-disable-next-line max-len
  162. 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$/
  163. test('upload functions with xhr formdata', async () => {
  164. nock('http://localhost').post('/', formDataNoMetaMatch)
  165. .reply(200)
  166. const ret = await runMultipartTest({ useFormData: true })
  167. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  168. })
  169. test('upload functions with unknown file size', async () => {
  170. // eslint-disable-next-line max-len
  171. nock('http://localhost').post('/', formDataNoMetaMatch)
  172. .reply(200)
  173. const ret = await runMultipartTest({ useFormData: true, includeSize: false })
  174. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  175. })
  176. // https://github.com/transloadit/uppy/issues/3477
  177. test('upload functions with xhr formdata and metadata', async () => {
  178. // eslint-disable-next-line max-len
  179. 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$/)
  180. .reply(200)
  181. const metadata = {
  182. key1: null, key2: true, key3: 1234, key4: {}, key5: () => {}, key6: Symbol(''),
  183. }
  184. const ret = await runMultipartTest({ useFormData: true, metadata })
  185. expect(ret).toMatchObject({ url: null, extraData: { response: expect.anything(), bytesUploaded: 17 } })
  186. })
  187. test('uploader checks metadata', () => {
  188. const opts = {
  189. companionOptions,
  190. endpoint: 'http://localhost',
  191. }
  192. // eslint-disable-next-line no-new
  193. new Uploader({ ...opts, metadata: { key: 'string value' } })
  194. expect(() => new Uploader({ ...opts, metadata: '' })).toThrow(new Uploader.ValidationError('metadata must be an object'))
  195. })
  196. test('uploader respects maxFileSize', async () => {
  197. const opts = {
  198. endpoint: 'http://url.myendpoint.com/files',
  199. companionOptions: { ...companionOptions, maxFileSize: 100 },
  200. size: 101,
  201. }
  202. expect(() => new Uploader(opts)).toThrow(new Uploader.ValidationError('maxFileSize exceeded'))
  203. })
  204. test('uploader respects maxFileSize correctly', async () => {
  205. const opts = {
  206. endpoint: 'http://url.myendpoint.com/files',
  207. companionOptions: { ...companionOptions, maxFileSize: 100 },
  208. size: 99,
  209. }
  210. // eslint-disable-next-line no-new
  211. new Uploader(opts) // no validation error
  212. })
  213. test('uploader respects maxFileSize with unknown size', async () => {
  214. const fileContent = Buffer.alloc(10000)
  215. const stream = intoStream(fileContent)
  216. const opts = {
  217. companionOptions: { ...companionOptions, maxFileSize: 1000 },
  218. endpoint: 'http://url.myendpoint.com/files',
  219. protocol: 'tus',
  220. size: null,
  221. pathPrefix: companionOptions.filePath,
  222. }
  223. const uploader = new Uploader(opts)
  224. const uploadToken = uploader.token
  225. // validate that the test is resolved on socket connection
  226. uploader.awaitReady(60000).then(() => uploader.tryUploadStream(stream))
  227. socketClient.connect(uploadToken)
  228. return new Promise((resolve, reject) => {
  229. socketClient.onUploadError(uploadToken, (message) => {
  230. try {
  231. expect(message).toMatchObject({ payload: { error: { message: 'maxFileSize exceeded' } } })
  232. resolve()
  233. } catch (err) {
  234. reject(err)
  235. }
  236. })
  237. })
  238. })
  239. })