hooks.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import type { ClipboardEvent } from 'react'
  2. import {
  3. useCallback,
  4. useState,
  5. } from 'react'
  6. import { useParams } from 'next/navigation'
  7. import produce from 'immer'
  8. import { v4 as uuid4 } from 'uuid'
  9. import { useTranslation } from 'react-i18next'
  10. import type { FileEntity } from './types'
  11. import { useFileStore } from './store'
  12. import {
  13. fileUpload,
  14. getSupportFileType,
  15. isAllowedFileExtension,
  16. } from './utils'
  17. import {
  18. AUDIO_SIZE_LIMIT,
  19. FILE_SIZE_LIMIT,
  20. IMG_SIZE_LIMIT,
  21. MAX_FILE_UPLOAD_LIMIT,
  22. VIDEO_SIZE_LIMIT,
  23. } from '@/app/components/base/file-uploader/constants'
  24. import { useToastContext } from '@/app/components/base/toast'
  25. import { TransferMethod } from '@/types/app'
  26. import { SupportUploadFileTypes } from '@/app/components/workflow/types'
  27. import type { FileUpload } from '@/app/components/base/features/types'
  28. import { formatFileSize } from '@/utils/format'
  29. import { uploadRemoteFileInfo } from '@/service/common'
  30. import type { FileUploadConfigResponse } from '@/models/common'
  31. export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => {
  32. const imgSizeLimit = Number(fileUploadConfig?.image_file_size_limit) * 1024 * 1024 || IMG_SIZE_LIMIT
  33. const docSizeLimit = Number(fileUploadConfig?.file_size_limit) * 1024 * 1024 || FILE_SIZE_LIMIT
  34. const audioSizeLimit = Number(fileUploadConfig?.audio_file_size_limit) * 1024 * 1024 || AUDIO_SIZE_LIMIT
  35. const videoSizeLimit = Number(fileUploadConfig?.video_file_size_limit) * 1024 * 1024 || VIDEO_SIZE_LIMIT
  36. const maxFileUploadLimit = Number(fileUploadConfig?.workflow_file_upload_limit) || MAX_FILE_UPLOAD_LIMIT
  37. return {
  38. imgSizeLimit,
  39. docSizeLimit,
  40. audioSizeLimit,
  41. videoSizeLimit,
  42. maxFileUploadLimit,
  43. }
  44. }
  45. export const useFile = (fileConfig: FileUpload) => {
  46. const { t } = useTranslation()
  47. const { notify } = useToastContext()
  48. const fileStore = useFileStore()
  49. const params = useParams()
  50. const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileConfig.fileUploadConfig)
  51. const checkSizeLimit = useCallback((fileType: string, fileSize: number) => {
  52. switch (fileType) {
  53. case SupportUploadFileTypes.image: {
  54. if (fileSize > imgSizeLimit) {
  55. notify({
  56. type: 'error',
  57. message: t('common.fileUploader.uploadFromComputerLimit', {
  58. type: SupportUploadFileTypes.image,
  59. size: formatFileSize(imgSizeLimit),
  60. }),
  61. })
  62. return false
  63. }
  64. return true
  65. }
  66. case SupportUploadFileTypes.document: {
  67. if (fileSize > docSizeLimit) {
  68. notify({
  69. type: 'error',
  70. message: t('common.fileUploader.uploadFromComputerLimit', {
  71. type: SupportUploadFileTypes.document,
  72. size: formatFileSize(docSizeLimit),
  73. }),
  74. })
  75. return false
  76. }
  77. return true
  78. }
  79. case SupportUploadFileTypes.audio: {
  80. if (fileSize > audioSizeLimit) {
  81. notify({
  82. type: 'error',
  83. message: t('common.fileUploader.uploadFromComputerLimit', {
  84. type: SupportUploadFileTypes.audio,
  85. size: formatFileSize(audioSizeLimit),
  86. }),
  87. })
  88. return false
  89. }
  90. return true
  91. }
  92. case SupportUploadFileTypes.video: {
  93. if (fileSize > videoSizeLimit) {
  94. notify({
  95. type: 'error',
  96. message: t('common.fileUploader.uploadFromComputerLimit', {
  97. type: SupportUploadFileTypes.video,
  98. size: formatFileSize(videoSizeLimit),
  99. }),
  100. })
  101. return false
  102. }
  103. return true
  104. }
  105. case SupportUploadFileTypes.custom: {
  106. if (fileSize > docSizeLimit) {
  107. notify({
  108. type: 'error',
  109. message: t('common.fileUploader.uploadFromComputerLimit', {
  110. type: SupportUploadFileTypes.document,
  111. size: formatFileSize(docSizeLimit),
  112. }),
  113. })
  114. return false
  115. }
  116. return true
  117. }
  118. default: {
  119. return true
  120. }
  121. }
  122. }, [audioSizeLimit, docSizeLimit, imgSizeLimit, notify, t, videoSizeLimit])
  123. const handleAddFile = useCallback((newFile: FileEntity) => {
  124. const {
  125. files,
  126. setFiles,
  127. } = fileStore.getState()
  128. const newFiles = produce(files, (draft) => {
  129. draft.push(newFile)
  130. })
  131. setFiles(newFiles)
  132. }, [fileStore])
  133. const handleUpdateFile = useCallback((newFile: FileEntity) => {
  134. const {
  135. files,
  136. setFiles,
  137. } = fileStore.getState()
  138. const newFiles = produce(files, (draft) => {
  139. const index = draft.findIndex(file => file.id === newFile.id)
  140. if (index > -1)
  141. draft[index] = newFile
  142. })
  143. setFiles(newFiles)
  144. }, [fileStore])
  145. const handleRemoveFile = useCallback((fileId: string) => {
  146. const {
  147. files,
  148. setFiles,
  149. } = fileStore.getState()
  150. const newFiles = files.filter(file => file.id !== fileId)
  151. setFiles(newFiles)
  152. }, [fileStore])
  153. const handleReUploadFile = useCallback((fileId: string) => {
  154. const {
  155. files,
  156. setFiles,
  157. } = fileStore.getState()
  158. const index = files.findIndex(file => file.id === fileId)
  159. if (index > -1) {
  160. const uploadingFile = files[index]
  161. const newFiles = produce(files, (draft) => {
  162. draft[index].progress = 0
  163. })
  164. setFiles(newFiles)
  165. fileUpload({
  166. file: uploadingFile.originalFile!,
  167. onProgressCallback: (progress) => {
  168. handleUpdateFile({ ...uploadingFile, progress })
  169. },
  170. onSuccessCallback: (res) => {
  171. handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
  172. },
  173. onErrorCallback: () => {
  174. notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
  175. handleUpdateFile({ ...uploadingFile, progress: -1 })
  176. },
  177. }, !!params.token)
  178. }
  179. }, [fileStore, notify, t, handleUpdateFile, params])
  180. const startProgressTimer = useCallback((fileId: string) => {
  181. const timer = setInterval(() => {
  182. const files = fileStore.getState().files
  183. const file = files.find(file => file.id === fileId)
  184. if (file && file.progress < 80 && file.progress >= 0)
  185. handleUpdateFile({ ...file, progress: file.progress + 20 })
  186. else
  187. clearTimeout(timer)
  188. }, 200)
  189. }, [fileStore, handleUpdateFile])
  190. const handleLoadFileFromLink = useCallback((url: string) => {
  191. const allowedFileTypes = fileConfig.allowed_file_types
  192. const uploadingFile = {
  193. id: uuid4(),
  194. name: url,
  195. type: '',
  196. size: 0,
  197. progress: 0,
  198. transferMethod: TransferMethod.local_file,
  199. supportFileType: '',
  200. url,
  201. isRemote: true,
  202. }
  203. handleAddFile(uploadingFile)
  204. startProgressTimer(uploadingFile.id)
  205. uploadRemoteFileInfo(url, !!params.token).then((res) => {
  206. const newFile = {
  207. ...uploadingFile,
  208. type: res.mime_type,
  209. size: res.size,
  210. progress: 100,
  211. supportFileType: getSupportFileType(res.name, res.mime_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
  212. uploadedId: res.id,
  213. url: res.url,
  214. }
  215. if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
  216. notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
  217. handleRemoveFile(uploadingFile.id)
  218. }
  219. if (!checkSizeLimit(newFile.supportFileType, newFile.size))
  220. handleRemoveFile(uploadingFile.id)
  221. else
  222. handleUpdateFile(newFile)
  223. }).catch(() => {
  224. notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') })
  225. handleRemoveFile(uploadingFile.id)
  226. })
  227. }, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types, fileConfig.allowed_file_extensions, startProgressTimer, params.token])
  228. const handleLoadFileFromLinkSuccess = useCallback(() => { }, [])
  229. const handleLoadFileFromLinkError = useCallback(() => { }, [])
  230. const handleClearFiles = useCallback(() => {
  231. const {
  232. setFiles,
  233. } = fileStore.getState()
  234. setFiles([])
  235. }, [fileStore])
  236. const handleLocalFileUpload = useCallback((file: File) => {
  237. if (!isAllowedFileExtension(file.name, file.type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
  238. notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
  239. return
  240. }
  241. const allowedFileTypes = fileConfig.allowed_file_types
  242. const fileType = getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom))
  243. if (!checkSizeLimit(fileType, file.size))
  244. return
  245. const reader = new FileReader()
  246. const isImage = file.type.startsWith('image')
  247. reader.addEventListener(
  248. 'load',
  249. () => {
  250. const uploadingFile = {
  251. id: uuid4(),
  252. name: file.name,
  253. type: file.type,
  254. size: file.size,
  255. progress: 0,
  256. transferMethod: TransferMethod.local_file,
  257. supportFileType: getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
  258. originalFile: file,
  259. base64Url: isImage ? reader.result as string : '',
  260. }
  261. handleAddFile(uploadingFile)
  262. fileUpload({
  263. file: uploadingFile.originalFile,
  264. onProgressCallback: (progress) => {
  265. handleUpdateFile({ ...uploadingFile, progress })
  266. },
  267. onSuccessCallback: (res) => {
  268. handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
  269. },
  270. onErrorCallback: () => {
  271. notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
  272. handleUpdateFile({ ...uploadingFile, progress: -1 })
  273. },
  274. }, !!params.token)
  275. },
  276. false,
  277. )
  278. reader.addEventListener(
  279. 'error',
  280. () => {
  281. notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerReadError') })
  282. },
  283. false,
  284. )
  285. reader.readAsDataURL(file)
  286. }, [checkSizeLimit, notify, t, handleAddFile, handleUpdateFile, params.token, fileConfig?.allowed_file_types, fileConfig?.allowed_file_extensions])
  287. const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
  288. const file = e.clipboardData?.files[0]
  289. if (file) {
  290. e.preventDefault()
  291. handleLocalFileUpload(file)
  292. }
  293. }, [handleLocalFileUpload])
  294. const [isDragActive, setIsDragActive] = useState(false)
  295. const handleDragFileEnter = useCallback((e: React.DragEvent<HTMLElement>) => {
  296. e.preventDefault()
  297. e.stopPropagation()
  298. setIsDragActive(true)
  299. }, [])
  300. const handleDragFileOver = useCallback((e: React.DragEvent<HTMLElement>) => {
  301. e.preventDefault()
  302. e.stopPropagation()
  303. }, [])
  304. const handleDragFileLeave = useCallback((e: React.DragEvent<HTMLElement>) => {
  305. e.preventDefault()
  306. e.stopPropagation()
  307. setIsDragActive(false)
  308. }, [])
  309. const handleDropFile = useCallback((e: React.DragEvent<HTMLElement>) => {
  310. e.preventDefault()
  311. e.stopPropagation()
  312. setIsDragActive(false)
  313. const file = e.dataTransfer.files[0]
  314. if (file)
  315. handleLocalFileUpload(file)
  316. }, [handleLocalFileUpload])
  317. return {
  318. handleAddFile,
  319. handleUpdateFile,
  320. handleRemoveFile,
  321. handleReUploadFile,
  322. handleLoadFileFromLink,
  323. handleLoadFileFromLinkSuccess,
  324. handleLoadFileFromLinkError,
  325. handleClearFiles,
  326. handleLocalFileUpload,
  327. handleClipboardPasteFile,
  328. isDragActive,
  329. handleDragFileEnter,
  330. handleDragFileOver,
  331. handleDragFileLeave,
  332. handleDropFile,
  333. }
  334. }