index.test.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  2. import 'whatwg-fetch'
  3. import nock from 'nock'
  4. import Core from '@uppy/core'
  5. import AwsS3Multipart from './index.js'
  6. const KB = 1024
  7. const MB = KB * KB
  8. describe('AwsS3Multipart', () => {
  9. beforeEach(() => nock.disableNetConnect())
  10. it('Registers AwsS3Multipart upload plugin', () => {
  11. const core = new Core()
  12. core.use(AwsS3Multipart)
  13. const pluginNames = core[Symbol.for('uppy test: getPlugins')]('uploader').map((plugin) => plugin.constructor.name)
  14. expect(pluginNames).toContain('AwsS3Multipart')
  15. })
  16. describe('companionUrl assertion', () => {
  17. it('Throws an error for main functions if configured without companionUrl', () => {
  18. const core = new Core()
  19. core.use(AwsS3Multipart)
  20. const awsS3Multipart = core.getPlugin('AwsS3Multipart')
  21. const err = 'Expected a `companionUrl` option'
  22. const file = {}
  23. const opts = {}
  24. expect(() => awsS3Multipart.opts.createMultipartUpload(file)).toThrow(
  25. err,
  26. )
  27. expect(() => awsS3Multipart.opts.listParts(file, opts)).toThrow(err)
  28. expect(() => awsS3Multipart.opts.completeMultipartUpload(file, opts)).toThrow(err)
  29. expect(() => awsS3Multipart.opts.abortMultipartUpload(file, opts)).toThrow(err)
  30. expect(() => awsS3Multipart.opts.signPart(file, opts)).toThrow(err)
  31. })
  32. })
  33. describe('non-multipart upload', () => {
  34. it('should handle POST uploads', async () => {
  35. const core = new Core()
  36. core.use(AwsS3Multipart, {
  37. shouldUseMultipart: false,
  38. limit: 0,
  39. getUploadParameters: () => ({
  40. method: 'POST',
  41. url: 'https://bucket.s3.us-east-2.amazonaws.com/',
  42. fields: {},
  43. }),
  44. })
  45. const scope = nock(
  46. 'https://bucket.s3.us-east-2.amazonaws.com',
  47. ).defaultReplyHeaders({
  48. 'access-control-allow-headers': '*',
  49. 'access-control-allow-method': 'POST',
  50. 'access-control-allow-origin': '*',
  51. 'access-control-expose-headers': 'ETag, Location',
  52. })
  53. scope.options('/').reply(204, '')
  54. scope
  55. .post('/')
  56. .reply(201, '', { ETag: 'test', Location: 'http://example.com' })
  57. const fileSize = 1
  58. core.addFile({
  59. source: 'vi',
  60. name: 'multitest.dat',
  61. type: 'application/octet-stream',
  62. data: new File([new Uint8Array(fileSize)], {
  63. type: 'application/octet-stream',
  64. }),
  65. })
  66. const uploadSuccessHandler = vi.fn()
  67. core.on('upload-success', uploadSuccessHandler)
  68. await core.upload()
  69. expect(uploadSuccessHandler.mock.calls).toHaveLength(1)
  70. expect(uploadSuccessHandler.mock.calls[0][1]).toStrictEqual({
  71. body: {
  72. ETag: 'test',
  73. location: 'http://example.com',
  74. },
  75. uploadURL: 'http://example.com',
  76. })
  77. scope.done()
  78. })
  79. })
  80. describe('without companionUrl (custom main functions)', () => {
  81. let core
  82. let awsS3Multipart
  83. beforeEach(() => {
  84. core = new Core()
  85. core.use(AwsS3Multipart, {
  86. limit: 0,
  87. createMultipartUpload: vi.fn(() => {
  88. return {
  89. uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
  90. key: 'test/upload/multitest.dat',
  91. }
  92. }),
  93. completeMultipartUpload: vi.fn(async () => ({ location: 'test' })),
  94. abortMultipartUpload: vi.fn(),
  95. prepareUploadParts: vi.fn(async (file, { parts }) => {
  96. const presignedUrls = {}
  97. parts.forEach(({ number }) => {
  98. presignedUrls[
  99. number
  100. ] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${number}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`
  101. })
  102. return { presignedUrls, headers: { 1: { 'Content-MD5': 'foo' } } }
  103. }),
  104. })
  105. awsS3Multipart = core.getPlugin('AwsS3Multipart')
  106. })
  107. it('Calls the prepareUploadParts function totalChunks / limit times', async () => {
  108. const scope = nock(
  109. 'https://bucket.s3.us-east-2.amazonaws.com',
  110. ).defaultReplyHeaders({
  111. 'access-control-allow-headers': '*',
  112. 'access-control-allow-method': 'PUT',
  113. 'access-control-allow-origin': '*',
  114. 'access-control-expose-headers': 'ETag, Content-MD5',
  115. })
  116. // 6MB file will give us 2 chunks, so there will be 2 PUT and 2 OPTIONS
  117. // calls to the presigned URL from 1 prepareUploadParts calls
  118. const fileSize = 5 * MB + 1 * MB
  119. scope
  120. .options((uri) => uri.includes('test/upload/multitest.dat?partNumber=1'))
  121. .reply(function replyFn () {
  122. expect(this.req.headers['access-control-request-headers']).toEqual('Content-MD5')
  123. return [200, '']
  124. })
  125. scope
  126. .options((uri) => uri.includes('test/upload/multitest.dat?partNumber=2'))
  127. .reply(function replyFn () {
  128. expect(this.req.headers['access-control-request-headers']).toBeUndefined()
  129. return [200, '']
  130. })
  131. scope
  132. .put((uri) => uri.includes('test/upload/multitest.dat?partNumber=1'))
  133. .reply(200, '', { ETag: 'test1' })
  134. scope
  135. .put((uri) => uri.includes('test/upload/multitest.dat?partNumber=2'))
  136. .reply(200, '', { ETag: 'test2' })
  137. core.addFile({
  138. source: 'vi',
  139. name: 'multitest.dat',
  140. type: 'application/octet-stream',
  141. data: new File([new Uint8Array(fileSize)], {
  142. type: 'application/octet-stream',
  143. }),
  144. })
  145. await core.upload()
  146. expect(
  147. awsS3Multipart.opts.prepareUploadParts.mock.calls.length,
  148. ).toEqual(2)
  149. scope.done()
  150. })
  151. it('Calls prepareUploadParts with a Math.ceil(limit / 2) minimum, instead of one at a time for the remaining chunks after the first limit batch', async () => {
  152. const scope = nock(
  153. 'https://bucket.s3.us-east-2.amazonaws.com',
  154. ).defaultReplyHeaders({
  155. 'access-control-allow-headers': '*',
  156. 'access-control-allow-method': 'PUT',
  157. 'access-control-allow-origin': '*',
  158. 'access-control-expose-headers': 'ETag',
  159. })
  160. // 50MB file will give us 10 chunks, so there will be 10 PUT and 10 OPTIONS
  161. // calls to the presigned URL from 3 prepareUploadParts calls
  162. //
  163. // The first prepareUploadParts call will be for 5 parts, the second
  164. // will be for 3 parts, the third will be for 2 parts.
  165. const fileSize = 50 * MB
  166. scope
  167. .options((uri) => uri.includes('test/upload/multitest.dat'))
  168. .reply(200, '')
  169. scope
  170. .put((uri) => uri.includes('test/upload/multitest.dat'))
  171. .reply(200, '', { ETag: 'test' })
  172. scope.persist()
  173. core.addFile({
  174. source: 'vi',
  175. name: 'multitest.dat',
  176. type: 'application/octet-stream',
  177. data: new File([new Uint8Array(fileSize)], {
  178. type: 'application/octet-stream',
  179. }),
  180. })
  181. await core.upload()
  182. function validatePartData ({ parts }, expected) {
  183. expect(parts.map((part) => part.number)).toEqual(expected)
  184. for (const part of parts) {
  185. expect(part.chunk).toBeDefined()
  186. }
  187. }
  188. expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(10)
  189. validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[0][1], [1])
  190. validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[1][1], [2])
  191. validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[2][1], [3])
  192. const completeCall = awsS3Multipart.opts.completeMultipartUpload.mock.calls[0][1]
  193. expect(completeCall.parts).toEqual([
  194. { ETag: 'test', PartNumber: 1 },
  195. { ETag: 'test', PartNumber: 2 },
  196. { ETag: 'test', PartNumber: 3 },
  197. { ETag: 'test', PartNumber: 4 },
  198. { ETag: 'test', PartNumber: 5 },
  199. { ETag: 'test', PartNumber: 6 },
  200. { ETag: 'test', PartNumber: 7 },
  201. { ETag: 'test', PartNumber: 8 },
  202. { ETag: 'test', PartNumber: 9 },
  203. { ETag: 'test', PartNumber: 10 },
  204. ])
  205. })
  206. it('Keeps chunks marked as busy through retries until they complete', async () => {
  207. const scope = nock(
  208. 'https://bucket.s3.us-east-2.amazonaws.com',
  209. ).defaultReplyHeaders({
  210. 'access-control-allow-headers': '*',
  211. 'access-control-allow-method': 'PUT',
  212. 'access-control-allow-origin': '*',
  213. 'access-control-expose-headers': 'ETag',
  214. })
  215. const fileSize = 50 * MB
  216. scope
  217. .options((uri) => uri.includes('test/upload/multitest.dat'))
  218. .reply(200, '')
  219. scope
  220. .put((uri) => uri.includes('test/upload/multitest.dat') && !uri.includes('partNumber=7'))
  221. .reply(200, '', { ETag: 'test' })
  222. // Fail the part 7 upload once, then let it succeed
  223. let calls = 0
  224. scope
  225. .put((uri) => uri.includes('test/upload/multitest.dat') && uri.includes('partNumber=7'))
  226. .reply(() => (calls++ === 0 ? [500] : [200, '', { ETag: 'test' }]))
  227. scope.persist()
  228. // Spy on the busy/done state of the test chunk (part 7, chunk index 6)
  229. let busySpy
  230. let doneSpy
  231. awsS3Multipart.setOptions({
  232. retryDelays: [10],
  233. createMultipartUpload: vi.fn((file) => {
  234. const multipartUploader = awsS3Multipart.uploaders[file.id]
  235. const testChunkState = multipartUploader.chunkState[6]
  236. let busy = false
  237. let done = false
  238. busySpy = vi.fn((value) => { busy = value })
  239. doneSpy = vi.fn((value) => { done = value })
  240. Object.defineProperty(testChunkState, 'busy', { get: () => busy, set: busySpy })
  241. Object.defineProperty(testChunkState, 'done', { get: () => done, set: doneSpy })
  242. return {
  243. uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
  244. key: 'test/upload/multitest.dat',
  245. }
  246. }),
  247. })
  248. core.addFile({
  249. source: 'vi',
  250. name: 'multitest.dat',
  251. type: 'application/octet-stream',
  252. data: new File([new Uint8Array(fileSize)], {
  253. type: 'application/octet-stream',
  254. }),
  255. })
  256. await core.upload()
  257. // The chunk should be marked as done once
  258. expect(doneSpy.mock.calls.length).toEqual(1)
  259. expect(doneSpy.mock.calls[0][0]).toEqual(true)
  260. // Any changes that set busy to false should only happen after the chunk has been marked done,
  261. // otherwise a race condition occurs (see PR #3955)
  262. const doneCallOrderNumber = doneSpy.mock.invocationCallOrder[0]
  263. for (const [index, callArgs] of busySpy.mock.calls.entries()) {
  264. if (callArgs[0] === false) {
  265. expect(busySpy.mock.invocationCallOrder[index]).toBeGreaterThan(doneCallOrderNumber)
  266. }
  267. }
  268. expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(10)
  269. })
  270. })
  271. describe('MultipartUploader', () => {
  272. const createMultipartUpload = vi.fn(() => {
  273. return {
  274. uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
  275. key: 'test/upload/multitest.dat',
  276. }
  277. })
  278. const signPart = vi
  279. .fn(async (file, { partNumber }) => {
  280. return { url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test` }
  281. })
  282. const uploadPartBytes = vi.fn()
  283. afterEach(() => vi.clearAllMocks())
  284. it('retries uploadPartBytes when it fails once', async () => {
  285. const core = new Core()
  286. .use(AwsS3Multipart, {
  287. createMultipartUpload,
  288. completeMultipartUpload: vi.fn(async () => ({ location: 'test' })),
  289. // eslint-disable-next-line no-throw-literal
  290. abortMultipartUpload: vi.fn(() => { throw 'should ignore' }),
  291. signPart,
  292. uploadPartBytes:
  293. uploadPartBytes
  294. // eslint-disable-next-line prefer-promise-reject-errors
  295. .mockImplementationOnce(() => Promise.reject({ source: { status: 500 } })),
  296. })
  297. const awsS3Multipart = core.getPlugin('AwsS3Multipart')
  298. const fileSize = 5 * MB + 1 * MB
  299. core.addFile({
  300. source: 'vi',
  301. name: 'multitest.dat',
  302. type: 'application/octet-stream',
  303. data: new File([new Uint8Array(fileSize)], {
  304. type: 'application/octet-stream',
  305. }),
  306. })
  307. await core.upload()
  308. expect(awsS3Multipart.opts.uploadPartBytes.mock.calls.length).toEqual(3)
  309. })
  310. it('calls `upload-error` when uploadPartBytes fails after all retries', async () => {
  311. const core = new Core()
  312. .use(AwsS3Multipart, {
  313. retryDelays: [10],
  314. createMultipartUpload,
  315. completeMultipartUpload: vi.fn(async () => ({ location: 'test' })),
  316. abortMultipartUpload: vi.fn(),
  317. signPart,
  318. uploadPartBytes: uploadPartBytes
  319. // eslint-disable-next-line prefer-promise-reject-errors
  320. .mockImplementation(() => Promise.reject({ source: { status: 500 } })),
  321. })
  322. const awsS3Multipart = core.getPlugin('AwsS3Multipart')
  323. const fileSize = 5 * MB + 1 * MB
  324. const mock = vi.fn()
  325. core.on('upload-error', mock)
  326. core.addFile({
  327. source: 'vi',
  328. name: 'multitest.dat',
  329. type: 'application/octet-stream',
  330. data: new File([new Uint8Array(fileSize)], {
  331. type: 'application/octet-stream',
  332. }),
  333. })
  334. await expect(core.upload()).rejects.toEqual({ source: { status: 500 } })
  335. expect(awsS3Multipart.opts.uploadPartBytes.mock.calls.length).toEqual(2)
  336. expect(mock.mock.calls.length).toEqual(1)
  337. })
  338. })
  339. describe('dynamic companionHeader', () => {
  340. let core
  341. let awsS3Multipart
  342. const oldToken = 'old token'
  343. const newToken = 'new token'
  344. beforeEach(() => {
  345. core = new Core()
  346. core.use(AwsS3Multipart, {
  347. companionHeaders: {
  348. authorization: oldToken,
  349. },
  350. })
  351. awsS3Multipart = core.getPlugin('AwsS3Multipart')
  352. })
  353. it('companionHeader is updated before uploading file', async () => {
  354. awsS3Multipart.setOptions({
  355. companionHeaders: {
  356. authorization: newToken,
  357. },
  358. })
  359. await core.upload()
  360. const client = awsS3Multipart[Symbol.for('uppy test: getClient')]()
  361. expect(client[Symbol.for('uppy test: getCompanionHeaders')]().authorization).toEqual(newToken)
  362. })
  363. })
  364. describe('dynamic companionHeader using setOption', () => {
  365. let core
  366. let awsS3Multipart
  367. const newToken = 'new token'
  368. it('companionHeader is updated before uploading file', async () => {
  369. core = new Core()
  370. core.use(AwsS3Multipart)
  371. /* Set up preprocessor */
  372. core.addPreProcessor(() => {
  373. awsS3Multipart = core.getPlugin('AwsS3Multipart')
  374. awsS3Multipart.setOptions({
  375. companionHeaders: {
  376. authorization: newToken,
  377. },
  378. })
  379. })
  380. await core.upload()
  381. const client = awsS3Multipart[Symbol.for('uppy test: getClient')]()
  382. expect(client[Symbol.for('uppy test: getCompanionHeaders')]().authorization).toEqual(newToken)
  383. })
  384. })
  385. describe('file metadata across custom main functions', () => {
  386. let core
  387. const createMultipartUpload = vi.fn(file => {
  388. core.setFileMeta(file.id, {
  389. ...file.meta,
  390. createMultipartUpload: true,
  391. })
  392. return {
  393. uploadId: 'upload1234',
  394. key: file.name,
  395. }
  396. })
  397. const signPart = vi.fn((file, partData) => {
  398. expect(file.meta.createMultipartUpload).toBe(true)
  399. core.setFileMeta(file.id, {
  400. ...file.meta,
  401. signPart: true,
  402. [`part${partData.partNumber}`]: partData.partNumber,
  403. })
  404. return {
  405. url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partData.partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`,
  406. }
  407. })
  408. const listParts = vi.fn((file) => {
  409. expect(file.meta.createMultipartUpload).toBe(true)
  410. core.setFileMeta(file.id, {
  411. ...file.meta,
  412. listParts: true,
  413. })
  414. const partKeys = Object.keys(file.meta).filter(metaKey => metaKey.startsWith('part'))
  415. return partKeys.map(metaKey => ({
  416. PartNumber: file.meta[metaKey],
  417. ETag: metaKey,
  418. Size: 5 * MB,
  419. }))
  420. })
  421. const completeMultipartUpload = vi.fn((file) => {
  422. expect(file.meta.createMultipartUpload).toBe(true)
  423. expect(file.meta.signPart).toBe(true)
  424. for (let i = 1; i <= 10; i++) {
  425. expect(file.meta[`part${i}`]).toBe(i)
  426. }
  427. return {}
  428. })
  429. const abortMultipartUpload = vi.fn((file) => {
  430. expect(file.meta.createMultipartUpload).toBe(true)
  431. expect(file.meta.signPart).toBe(true)
  432. expect(file.meta.abortingPart).toBe(5)
  433. return {}
  434. })
  435. beforeEach(() => {
  436. createMultipartUpload.mockClear()
  437. signPart.mockClear()
  438. listParts.mockClear()
  439. abortMultipartUpload.mockClear()
  440. completeMultipartUpload.mockClear()
  441. })
  442. it('preserves file metadata if upload is completed', async () => {
  443. core = new Core()
  444. .use(AwsS3Multipart, {
  445. createMultipartUpload,
  446. signPart,
  447. listParts,
  448. completeMultipartUpload,
  449. abortMultipartUpload,
  450. })
  451. nock('https://bucket.s3.us-east-2.amazonaws.com')
  452. .defaultReplyHeaders({
  453. 'access-control-allow-headers': '*',
  454. 'access-control-allow-method': 'PUT',
  455. 'access-control-allow-origin': '*',
  456. 'access-control-expose-headers': 'ETag',
  457. })
  458. .put((uri) => uri.includes('test/upload/multitest.dat'))
  459. .reply(200, '', { ETag: 'test' })
  460. .persist()
  461. const fileSize = 50 * MB
  462. core.addFile({
  463. source: 'vi',
  464. name: 'multitest.dat',
  465. type: 'application/octet-stream',
  466. data: new File([new Uint8Array(fileSize)], {
  467. type: 'application/octet-stream',
  468. }),
  469. })
  470. await core.upload()
  471. expect(createMultipartUpload).toHaveBeenCalled()
  472. expect(signPart).toHaveBeenCalledTimes(10)
  473. expect(completeMultipartUpload).toHaveBeenCalled()
  474. })
  475. it('preserves file metadata if upload is aborted', async () => {
  476. const signPartWithAbort = vi.fn((file, partData) => {
  477. expect(file.meta.createMultipartUpload).toBe(true)
  478. if (partData.partNumber === 5) {
  479. core.setFileMeta(file.id, {
  480. ...file.meta,
  481. abortingPart: partData.partNumber,
  482. })
  483. core.removeFile(file.id)
  484. return {}
  485. }
  486. core.setFileMeta(file.id, {
  487. ...file.meta,
  488. signPart: true,
  489. [`part${partData.partNumber}`]: partData.partNumber,
  490. })
  491. return {
  492. url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partData.partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`,
  493. }
  494. })
  495. core = new Core()
  496. .use(AwsS3Multipart, {
  497. createMultipartUpload,
  498. signPart: signPartWithAbort,
  499. listParts,
  500. completeMultipartUpload,
  501. abortMultipartUpload,
  502. })
  503. nock('https://bucket.s3.us-east-2.amazonaws.com')
  504. .defaultReplyHeaders({
  505. 'access-control-allow-headers': '*',
  506. 'access-control-allow-method': 'PUT',
  507. 'access-control-allow-origin': '*',
  508. 'access-control-expose-headers': 'ETag',
  509. })
  510. .put((uri) => uri.includes('test/upload/multitest.dat'))
  511. .reply(200, '', { ETag: 'test' })
  512. .persist()
  513. const fileSize = 50 * MB
  514. core.addFile({
  515. source: 'vi',
  516. name: 'multitest.dat',
  517. type: 'application/octet-stream',
  518. data: new File([new Uint8Array(fileSize)], {
  519. type: 'application/octet-stream',
  520. }),
  521. })
  522. await core.upload()
  523. expect(createMultipartUpload).toHaveBeenCalled()
  524. expect(signPartWithAbort).toHaveBeenCalled()
  525. expect(abortMultipartUpload).toHaveBeenCalled()
  526. })
  527. it('preserves file metadata if upload is paused and resumed', async () => {
  528. const completeMultipartUploadAfterPause = vi.fn((file) => {
  529. expect(file.meta.createMultipartUpload).toBe(true)
  530. expect(file.meta.signPart).toBe(true)
  531. for (let i = 1; i <= 10; i++) {
  532. expect(file.meta[`part${i}`]).toBe(i)
  533. }
  534. expect(file.meta.listParts).toBe(true)
  535. return {}
  536. })
  537. const signPartWithPause = vi.fn((file, partData) => {
  538. expect(file.meta.createMultipartUpload).toBe(true)
  539. if (partData.partNumber === 3) {
  540. core.setFileMeta(file.id, {
  541. ...file.meta,
  542. abortingPart: partData.partNumber,
  543. })
  544. core.pauseResume(file.id)
  545. setTimeout(() => core.pauseResume(file.id), 500)
  546. }
  547. core.setFileMeta(file.id, {
  548. ...file.meta,
  549. signPart: true,
  550. [`part${partData.partNumber}`]: partData.partNumber,
  551. })
  552. return {
  553. url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partData.partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`,
  554. }
  555. })
  556. core = new Core()
  557. .use(AwsS3Multipart, {
  558. createMultipartUpload,
  559. signPart: signPartWithPause,
  560. listParts,
  561. completeMultipartUpload: completeMultipartUploadAfterPause,
  562. abortMultipartUpload,
  563. })
  564. nock('https://bucket.s3.us-east-2.amazonaws.com')
  565. .defaultReplyHeaders({
  566. 'access-control-allow-headers': '*',
  567. 'access-control-allow-method': 'PUT',
  568. 'access-control-allow-origin': '*',
  569. 'access-control-expose-headers': 'ETag',
  570. })
  571. .put((uri) => uri.includes('test/upload/multitest.dat'))
  572. .reply(200, '', { ETag: 'test' })
  573. .persist()
  574. const fileSize = 50 * MB
  575. core.addFile({
  576. source: 'vi',
  577. name: 'multitest.dat',
  578. type: 'application/octet-stream',
  579. data: new File([new Uint8Array(fileSize)], {
  580. type: 'application/octet-stream',
  581. }),
  582. })
  583. await core.upload()
  584. expect(createMultipartUpload).toHaveBeenCalled()
  585. expect(signPartWithPause).toHaveBeenCalled()
  586. expect(listParts).toHaveBeenCalled()
  587. expect(completeMultipartUploadAfterPause).toHaveBeenCalled()
  588. })
  589. })
  590. })