batch-file-uploader.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import pLimit from 'p-limit'
  2. import getMeta from '@/utils/meta'
  3. import { getErrorMessageForStatusCode } from './http-status-messages'
  4. export type BatchUploadItem = {
  5. file: Blob
  6. name: string
  7. relativePath?: string
  8. }
  9. export type BatchUploadOptions = {
  10. projectId: string
  11. folderId: string
  12. /**
  13. * Maximum number of uploads to run in parallel.
  14. * Must be greater than 0; non-positive values fall back to the default of 3.
  15. */
  16. concurrency?: number
  17. }
  18. export type UploadResult =
  19. | {
  20. status: 'success'
  21. name: string
  22. relativePath?: string
  23. data: unknown
  24. }
  25. | {
  26. status: 'error'
  27. name: string
  28. relativePath?: string
  29. error: string
  30. }
  31. const DEFAULT_CONCURRENCY = 3
  32. export async function uploadBatch(
  33. items: BatchUploadItem[],
  34. options: BatchUploadOptions
  35. ): Promise<UploadResult[]> {
  36. if (items.length === 0) {
  37. return []
  38. }
  39. const concurrency =
  40. options.concurrency && options.concurrency > 0
  41. ? options.concurrency
  42. : DEFAULT_CONCURRENCY
  43. const limit = pLimit(concurrency)
  44. return Promise.all(items.map(item => limit(() => uploadOne(item, options))))
  45. }
  46. async function uploadOne(
  47. item: BatchUploadItem,
  48. options: BatchUploadOptions
  49. ): Promise<UploadResult> {
  50. const formData = new FormData()
  51. formData.append('qqfile', item.file, item.name)
  52. formData.append('name', item.name)
  53. if (item.relativePath) {
  54. formData.append('relativePath', item.relativePath)
  55. }
  56. const url = `/project/${options.projectId}/upload?folder_id=${options.folderId}`
  57. try {
  58. const response = await fetch(url, {
  59. method: 'POST',
  60. body: formData,
  61. headers: {
  62. 'X-CSRF-TOKEN': getMeta('ol-csrfToken'),
  63. },
  64. })
  65. if (!response.ok) {
  66. const error = await extractErrorMessage(response)
  67. return {
  68. status: 'error',
  69. name: item.name,
  70. relativePath: item.relativePath,
  71. error,
  72. }
  73. }
  74. const data = await response.json()
  75. return {
  76. status: 'success',
  77. name: item.name,
  78. relativePath: item.relativePath,
  79. data,
  80. }
  81. } catch (err) {
  82. return {
  83. status: 'error',
  84. name: item.name,
  85. relativePath: item.relativePath,
  86. error: err instanceof Error ? err.message : String(err),
  87. }
  88. }
  89. }
  90. async function extractErrorMessage(response: Response): Promise<string> {
  91. try {
  92. const body = await response.json()
  93. if (typeof body?.error === 'string') {
  94. return body.error
  95. }
  96. } catch {
  97. // JSON body not available — fall back to status-code message
  98. }
  99. return getErrorMessageForStatusCode(response.status)
  100. }