refactor(base64-to-file): clean validation to convert base64 to file
This commit is contained in:
		
							parent
							
								
									5f03619ab4
								
							
						
					
					
						commit
						750a76b00f
					
				| @ -1,16 +1,35 @@ | |||||||
| import { extension as getExtensionFromMime } from 'mime-types'; | import { extension as getExtensionFromMime } from 'mime-types'; | ||||||
| import type { Ref } from 'vue'; | import type { Ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | function getFileExtensionFromBase64({ | ||||||
|  |   base64String, | ||||||
|  |   defaultExtension = 'txt', | ||||||
|  | }: { | ||||||
|  |   base64String: string; | ||||||
|  |   defaultExtension?: string; | ||||||
|  | }) { | ||||||
|  |   const hasMimeType = base64String.match(/data:(.*?);base64/i); | ||||||
|  | 
 | ||||||
|  |   if (hasMimeType) { | ||||||
|  |     return getExtensionFromMime(hasMimeType[1]) || defaultExtension; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return defaultExtension; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) { | export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) { | ||||||
|   return { |   return { | ||||||
|     download() { |     download() { | ||||||
|       const base64 = source.value; |       const base64String = source.value; | ||||||
|       const mimeType = base64.match(/data:(.*?);base64/i)?.[1] ?? 'text/plain'; | 
 | ||||||
|       console.log({ mimeType }); |       if (base64String === '') { | ||||||
|       const cleanFileName = filename ?? `file.${getExtensionFromMime(mimeType)}`; |         throw new Error('Base64 string is empty'); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`; | ||||||
| 
 | 
 | ||||||
|       const a = document.createElement('a'); |       const a = document.createElement('a'); | ||||||
|       a.href = source.value; |       a.href = base64String; | ||||||
|       a.download = cleanFileName; |       a.download = cleanFileName; | ||||||
|       a.click(); |       a.click(); | ||||||
|     }, |     }, | ||||||
|  | |||||||
							
								
								
									
										29
									
								
								src/composable/validation.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/composable/validation.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | /* eslint-disable @typescript-eslint/no-empty-function */ | ||||||
|  | import { describe, expect, it } from 'vitest'; | ||||||
|  | import { isFalsyOrHasThrown } from './validation'; | ||||||
|  | 
 | ||||||
|  | describe('useValidation', () => { | ||||||
|  |   describe('isFalsyOrHasThrown', () => { | ||||||
|  |     it('should return true if the callback return nil, false or throw', () => { | ||||||
|  |       expect(isFalsyOrHasThrown(() => false)).toBe(true); | ||||||
|  |       expect(isFalsyOrHasThrown(() => null)).toBe(true); | ||||||
|  |       expect(isFalsyOrHasThrown(() => undefined)).toBe(true); | ||||||
|  |       expect(isFalsyOrHasThrown(() => {})).toBe(true); | ||||||
|  |       expect( | ||||||
|  |         isFalsyOrHasThrown(() => { | ||||||
|  |           throw new Error(); | ||||||
|  |         }), | ||||||
|  |       ).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should return true for any truthy values and empty string and 0 values', () => { | ||||||
|  |       expect(isFalsyOrHasThrown(() => true)).toBe(false); | ||||||
|  |       expect(isFalsyOrHasThrown(() => 'string')).toBe(false); | ||||||
|  |       expect(isFalsyOrHasThrown(() => 1)).toBe(false); | ||||||
|  |       expect(isFalsyOrHasThrown(() => 0)).toBe(false); | ||||||
|  |       expect(isFalsyOrHasThrown(() => '')).toBe(false); | ||||||
|  |       expect(isFalsyOrHasThrown(() => [])).toBe(false); | ||||||
|  |       expect(isFalsyOrHasThrown(() => ({}))).toBe(false); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -1,13 +1,20 @@ | |||||||
|  | import _ from 'lodash'; | ||||||
| import { reactive, watch, type Ref } from 'vue'; | import { reactive, watch, type Ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| type UseValidationRule<T> = { | type ValidatorReturnType = unknown; | ||||||
|   validator: (value: T) => boolean; |  | ||||||
|   message: string; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| function isFalsyOrHasThrown(cb: () => boolean) { | interface UseValidationRule<T> { | ||||||
|  |   validator: (value: T) => ValidatorReturnType; | ||||||
|  |   message: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | ||||||
|   try { |   try { | ||||||
|     return !cb(); |     const returnValue = cb(); | ||||||
|  | 
 | ||||||
|  |     if (_.isNil(returnValue)) return true; | ||||||
|  | 
 | ||||||
|  |     return returnValue === false; | ||||||
|   } catch (_) { |   } catch (_) { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| @ -17,22 +24,30 @@ export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: Use | |||||||
|   const state = reactive<{ |   const state = reactive<{ | ||||||
|     message: string; |     message: string; | ||||||
|     status: undefined | 'error'; |     status: undefined | 'error'; | ||||||
|  |     isValid: boolean; | ||||||
|   }>({ |   }>({ | ||||||
|     message: '', |     message: '', | ||||||
|     status: undefined, |     status: undefined, | ||||||
|  |     isValid: false, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   watch([source], () => { |   watch( | ||||||
|     state.message = ''; |     [source], | ||||||
|     state.status = undefined; |     () => { | ||||||
|  |       state.message = ''; | ||||||
|  |       state.status = undefined; | ||||||
| 
 | 
 | ||||||
|     for (const rule of rules) { |       for (const rule of rules) { | ||||||
|       if (isFalsyOrHasThrown(() => rule.validator(source.value))) { |         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||||
|         state.message = rule.message; |           state.message = rule.message; | ||||||
|         state.status = 'error'; |           state.status = 'error'; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } | 
 | ||||||
|   }); |       state.isValid = state.status !== 'error'; | ||||||
|  |     }, | ||||||
|  |     { immediate: true }, | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|   return state; |   return state; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,8 +1,16 @@ | |||||||
| <template> | <template> | ||||||
|   <n-card title="Base64 to file"> |   <n-card title="Base64 to file"> | ||||||
|     <n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" /> |     <n-form-item | ||||||
|  |       :feedback="base64InputValidation.message" | ||||||
|  |       :validation-status="base64InputValidation.status" | ||||||
|  |       :show-label="false" | ||||||
|  |     > | ||||||
|  |       <n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" /> | ||||||
|  |     </n-form-item> | ||||||
|     <n-space justify="center"> |     <n-space justify="center"> | ||||||
|       <n-button secondary @click="download()"> Download file </n-button> |       <n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()"> | ||||||
|  |         Download file | ||||||
|  |       </n-button> | ||||||
|     </n-space> |     </n-space> | ||||||
|   </n-card> |   </n-card> | ||||||
| 
 | 
 | ||||||
| @ -26,6 +34,7 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useCopy } from '@/composable/copy'; | import { useCopy } from '@/composable/copy'; | ||||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
| import { Upload } from '@vicons/tabler'; | import { Upload } from '@vicons/tabler'; | ||||||
| import { useBase64 } from '@vueuse/core'; | import { useBase64 } from '@vueuse/core'; | ||||||
| import type { UploadFileInfo } from 'naive-ui'; | import type { UploadFileInfo } from 'naive-ui'; | ||||||
| @ -33,6 +42,25 @@ import { ref, type Ref } from 'vue'; | |||||||
| 
 | 
 | ||||||
| const base64Input = ref(''); | const base64Input = ref(''); | ||||||
| const { download } = useDownloadFileFromBase64({ source: base64Input }); | const { download } = useDownloadFileFromBase64({ source: base64Input }); | ||||||
|  | const base64InputValidation = useValidation({ | ||||||
|  |   source: base64Input, | ||||||
|  |   rules: [ | ||||||
|  |     { | ||||||
|  |       message: 'Invalid base 64 string', | ||||||
|  |       validator: (value) => window.atob(value.replace(/^data:.*?;base64,/, '')), | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function downloadFile() { | ||||||
|  |   if (!base64InputValidation.isValid) return; | ||||||
|  | 
 | ||||||
|  |   try { | ||||||
|  |     download(); | ||||||
|  |   } catch (_) { | ||||||
|  |     // | ||||||
|  |   } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| const fileList = ref(); | const fileList = ref(); | ||||||
| const fileInput = ref() as Ref<File>; | const fileInput = ref() as Ref<File>; | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user