refactor(base64-to-file): clean validation to convert base64 to file

This commit is contained in:
Corentin Thomasset 2022-07-29 10:56:04 +02:00
parent 5f03619ab4
commit 750a76b00f
No known key found for this signature in database
GPG key ID: 3103EB5E79496F9C
4 changed files with 113 additions and 22 deletions

View file

@ -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();
}, },

View 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);
});
});
});

View file

@ -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;
} }

View file

@ -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>;