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 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 }) { | ||||
|   return { | ||||
|     download() { | ||||
|       const base64 = source.value; | ||||
|       const mimeType = base64.match(/data:(.*?);base64/i)?.[1] ?? 'text/plain'; | ||||
|       console.log({ mimeType }); | ||||
|       const cleanFileName = filename ?? `file.${getExtensionFromMime(mimeType)}`; | ||||
|       const base64String = source.value; | ||||
| 
 | ||||
|       if (base64String === '') { | ||||
|         throw new Error('Base64 string is empty'); | ||||
|       } | ||||
| 
 | ||||
|       const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`; | ||||
| 
 | ||||
|       const a = document.createElement('a'); | ||||
|       a.href = source.value; | ||||
|       a.href = base64String; | ||||
|       a.download = cleanFileName; | ||||
|       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'; | ||||
| 
 | ||||
| type UseValidationRule<T> = { | ||||
|   validator: (value: T) => boolean; | ||||
|   message: string; | ||||
| }; | ||||
| type ValidatorReturnType = unknown; | ||||
| 
 | ||||
| function isFalsyOrHasThrown(cb: () => boolean) { | ||||
| interface UseValidationRule<T> { | ||||
|   validator: (value: T) => ValidatorReturnType; | ||||
|   message: string; | ||||
| } | ||||
| 
 | ||||
| export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | ||||
|   try { | ||||
|     return !cb(); | ||||
|     const returnValue = cb(); | ||||
| 
 | ||||
|     if (_.isNil(returnValue)) return true; | ||||
| 
 | ||||
|     return returnValue === false; | ||||
|   } catch (_) { | ||||
|     return true; | ||||
|   } | ||||
| @ -17,22 +24,30 @@ export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: Use | ||||
|   const state = reactive<{ | ||||
|     message: string; | ||||
|     status: undefined | 'error'; | ||||
|     isValid: boolean; | ||||
|   }>({ | ||||
|     message: '', | ||||
|     status: undefined, | ||||
|     isValid: false, | ||||
|   }); | ||||
| 
 | ||||
|   watch([source], () => { | ||||
|     state.message = ''; | ||||
|     state.status = undefined; | ||||
|   watch( | ||||
|     [source], | ||||
|     () => { | ||||
|       state.message = ''; | ||||
|       state.status = undefined; | ||||
| 
 | ||||
|     for (const rule of rules) { | ||||
|       if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||
|         state.message = rule.message; | ||||
|         state.status = 'error'; | ||||
|       for (const rule of rules) { | ||||
|         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||
|           state.message = rule.message; | ||||
|           state.status = 'error'; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|       state.isValid = state.status !== 'error'; | ||||
|     }, | ||||
|     { immediate: true }, | ||||
|   ); | ||||
| 
 | ||||
|   return state; | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,16 @@ | ||||
| <template> | ||||
|   <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-button secondary @click="download()"> Download file </n-button> | ||||
|       <n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()"> | ||||
|         Download file | ||||
|       </n-button> | ||||
|     </n-space> | ||||
|   </n-card> | ||||
| 
 | ||||
| @ -26,6 +34,7 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { Upload } from '@vicons/tabler'; | ||||
| import { useBase64 } from '@vueuse/core'; | ||||
| import type { UploadFileInfo } from 'naive-ui'; | ||||
| @ -33,6 +42,25 @@ import { ref, type Ref } from 'vue'; | ||||
| 
 | ||||
| const base64Input = ref(''); | ||||
| 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 fileInput = ref() as Ref<File>; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user