feat(timesheet): add functionality to upload expense attachment to garage test instance
requires further development. Key used to store file needs to be saved to expense to be later used for retrieval
This commit is contained in:
parent
fec7300092
commit
119a145549
105
package-lock.json
generated
105
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"@quasar/extras": "^1.17.0",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"crc": "^4.3.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
|
|
@ -3091,7 +3092,7 @@
|
|||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -3152,6 +3153,30 @@
|
|||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
|
|
@ -3318,10 +3343,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -3336,10 +3361,9 @@
|
|||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-builder": {
|
||||
|
|
@ -4018,6 +4042,22 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/crc/-/crc-4.3.2.tgz",
|
||||
"integrity": "sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"buffer": ">=6.0.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
|
|
@ -4139,6 +4179,30 @@
|
|||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/proxy-from-env": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
|
||||
|
|
@ -5870,7 +5934,7 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -8173,31 +8237,6 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"@quasar/extras": "^1.17.0",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"crc": "^4.3.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
|
|
|
|||
|
|
@ -6,19 +6,22 @@
|
|||
import SchedulePresetsDialog from 'src/modules/employee-list/components/schedule-presets-dialog.vue';
|
||||
import AddModifyDialogSchedulePreview from './add-modify-dialog-schedule-preview.vue';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useEmployeeListApi } from '../composables/use-employee-api';
|
||||
import type { PresetManagerMode } from 'src/modules/employee-list/models/schedule-presets.models';
|
||||
|
||||
// ================= state ======================
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
||||
const preset_options = ref<{ label: string, value: number }[]>([]);
|
||||
const current_preset = ref<{ label: string | undefined, value: number }>({ label: undefined, value: -1 });
|
||||
const manager_watcher = ref(schedule_preset_store.is_manager_open);
|
||||
|
||||
// ====================== methods ========================
|
||||
|
||||
const getPresetOptions = (): { label: string, value: number }[] => {
|
||||
const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
|
||||
|
|
@ -41,8 +44,6 @@
|
|||
onMounted(() => {
|
||||
loadSelectedPresetOption();
|
||||
});
|
||||
|
||||
watch(manager_watcher, loadSelectedPresetOption)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -50,7 +51,7 @@
|
|||
:key="schedule_preset_store.is_manager_open === false ? '0' : '1'"
|
||||
class="column full-width flex-center items-start"
|
||||
>
|
||||
<SchedulePresetsDialog />
|
||||
<SchedulePresetsDialog @before-hide="loadSelectedPresetOption"/>
|
||||
|
||||
<div class="col row justify-center full-width no-wrap">
|
||||
<q-select
|
||||
|
|
|
|||
|
|
@ -10,25 +10,22 @@
|
|||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||
import { Expense, type ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
|
||||
// ================= state ======================
|
||||
|
||||
interface ExpenseOption {
|
||||
label: string;
|
||||
value: ExpenseType;
|
||||
icon: string;
|
||||
}
|
||||
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
||||
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
|
||||
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
||||
const file = defineModel<File>('file');
|
||||
|
||||
const { t } = useI18n();
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
const auth_store = useAuthStore();
|
||||
const expenses_api = useExpensesApi();
|
||||
const files = defineModel<File[] | null>('files');
|
||||
const is_navigator_open = ref(false);
|
||||
const rules = useExpenseRules(t);
|
||||
|
||||
|
|
@ -62,8 +59,9 @@
|
|||
}
|
||||
|
||||
const requestExpenseCreationOrUpdate = async () => {
|
||||
await expenses_api.upsertExpense(expenses_store.current_expense, employeeEmail);
|
||||
|
||||
if (file.value)
|
||||
await expenses_api.upsertExpense(expenses_store.current_expense, file.value, employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL');
|
||||
|
||||
expenses_store.is_showing_create_form = true;
|
||||
expenses_store.mode = 'create';
|
||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
|
|
@ -255,11 +253,9 @@
|
|||
<!-- import attach file section -->
|
||||
<div class="col q-px-xs">
|
||||
<q-file
|
||||
v-model="files"
|
||||
v-model="file"
|
||||
standout
|
||||
dense
|
||||
use-chips
|
||||
multiple
|
||||
stack-label
|
||||
label-slot
|
||||
>
|
||||
|
|
@ -298,7 +294,7 @@
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
||||
border: transparent;
|
||||
}
|
||||
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
||||
border: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,19 +14,21 @@
|
|||
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||
import type { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||
|
||||
const { t } = useI18n();
|
||||
// ================== state =====================
|
||||
|
||||
const expense = defineModel<Expense>({ required: true });
|
||||
|
||||
const expenses_api = useExpensesApi();
|
||||
const expenses_store = useExpensesStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const is_showing_update_form = ref(false);
|
||||
|
||||
const { mode = 'normal' } = defineProps<{
|
||||
mode?: 'approval' | 'normal';
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const expenses_api = useExpensesApi();
|
||||
const expenses_store = useExpensesStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const is_showing_update_form = ref(false);
|
||||
|
||||
// ===================== methods =========================
|
||||
|
||||
const requestExpenseDeletion = async () => {
|
||||
await expenses_api.deleteExpenseById(expense.value.id);
|
||||
|
|
|
|||
|
|
@ -9,25 +9,26 @@
|
|||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||
import { type ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
|
||||
interface ExpenseOption {
|
||||
label: string;
|
||||
value: ExpenseType;
|
||||
icon: string;
|
||||
}
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
|
||||
const file = defineModel<File | undefined>();
|
||||
|
||||
const { employeeEmail } = defineProps<{
|
||||
employeeEmail?: string;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
const auth_store = useAuthStore();
|
||||
const expenses_api = useExpensesApi();
|
||||
const files = defineModel<File[] | null>('files');
|
||||
const is_navigator_open = ref(false);
|
||||
const is_showing_comment_dialog_mobile = ref(false);
|
||||
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
const rules = useExpenseRules(t);
|
||||
|
||||
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||
|
|
@ -49,7 +50,8 @@
|
|||
};
|
||||
|
||||
const requestExpenseCreationOrUpdate = async () => {
|
||||
await expenses_api.upsertExpense(expenses_store.current_expense);
|
||||
if (file.value)
|
||||
await expenses_api.upsertExpense(expenses_store.current_expense, file.value, employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL');
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -218,7 +220,10 @@
|
|||
class="col-auto"
|
||||
/>
|
||||
|
||||
<q-dialog v-model="is_showing_comment_dialog_mobile" class="z-top">
|
||||
<q-dialog
|
||||
v-model="is_showing_comment_dialog_mobile"
|
||||
class="z-top"
|
||||
>
|
||||
<q-card class="full-width bg-primary rounded-10">
|
||||
<q-card-section class="q-pa-none">
|
||||
<span
|
||||
|
|
@ -248,7 +253,7 @@
|
|||
|
||||
<!-- import attach file section -->
|
||||
<q-file
|
||||
v-model="files"
|
||||
v-model="file"
|
||||
standout="bg-blue-grey-9"
|
||||
dense
|
||||
use-chips
|
||||
|
|
@ -273,7 +278,10 @@
|
|||
</q-file>
|
||||
</div>
|
||||
|
||||
<div class="col row full-width items-center" :class="expenses_store.mode === 'create' ? 'q-px-md q-py-xs' : ''">
|
||||
<div
|
||||
class="col row full-width items-center"
|
||||
:class="expenses_store.mode === 'create' ? 'q-px-md q-py-xs' : ''"
|
||||
>
|
||||
<q-btn
|
||||
push
|
||||
color="accent"
|
||||
|
|
|
|||
|
|
@ -8,13 +8,19 @@ export const useExpensesApi = () => {
|
|||
const expenses_store = useExpensesStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const upsertExpense = async (expense: Expense, employee_email?: string): Promise<void> => {
|
||||
const upsertExpense = async (expense: Expense, file: File, employee_email: string): Promise<string> => {
|
||||
const presignedURL = expenses_store.uploadAttachment(file);
|
||||
if (!presignedURL) return 'PRESIGN_FAILED';
|
||||
|
||||
const success = await expenses_store.upsertExpense(expense, employee_email);
|
||||
|
||||
if (success) {
|
||||
expenses_store.current_expense = new Expense(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||
return 'SUCCESS';
|
||||
}
|
||||
|
||||
return 'INVALID_EXPENSE';
|
||||
};
|
||||
|
||||
const deleteExpenseById = async (expense_id: number, employee_email?: string): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -24,4 +24,10 @@ export class Expense {
|
|||
this.comment = '';
|
||||
this.is_approved = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export interface ExpenseOption {
|
||||
label: string;
|
||||
value: ExpenseType;
|
||||
icon: string;
|
||||
}
|
||||
|
|
@ -1,19 +1,31 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const ExpenseService = {
|
||||
createExpense: async (expense: Expense): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
||||
createExpense: async (expense: Expense): Promise<{ success: boolean, data: Expense, error?: unknown }> => {
|
||||
const response = await api.post('expense/create', expense);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateExpense: async (expense: Expense, email?: string): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
||||
updateExpense: async (expense: Expense, email?: string): Promise<{ success: boolean, data: Expense, error?: unknown }> => {
|
||||
const response = await api.patch(`expense/update${email ? '?employee_email=' + email : ''}`, expense);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExpenseById: async (expense_id: number): Promise<{success: boolean, data: number, error?: unknown}> => {
|
||||
deleteExpenseById: async (expense_id: number): Promise<{ success: boolean, data: number, error?: unknown }> => {
|
||||
const response = await api.delete(`expense/delete/${expense_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise<BackendResponse<string>> => {
|
||||
const [file_name, file_type] = file.name.split('.');
|
||||
const response = await api.post(`attachments/s3/upload?file-name=${file_name}&file-type=${file_type}&checksumCRC32=${checksum_crc32}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
uploadAttachmentWithPresignedUrl: async (file: File, url: string) => {
|
||||
const response = await api.put(url, file, { headers: { 'Content-Type': file.type, }, withCredentials: false });
|
||||
console.log('response to upload: ', response);
|
||||
}
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@ import { defineStore } from "pinia";
|
|||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||
import { computeCRC32Base64 } from "src/utils/crc-encoder";
|
||||
|
||||
export const useExpensesStore = defineStore('expenses', () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
|
@ -50,6 +51,22 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
return data.success;
|
||||
}
|
||||
|
||||
const uploadAttachment = async (file: File) => {
|
||||
try {
|
||||
const checksum = await computeCRC32Base64(file);
|
||||
const presignedUrlResponse = await ExpenseService.getPresignedUploadURL(file, checksum);
|
||||
|
||||
if (presignedUrlResponse.success && presignedUrlResponse.data) {
|
||||
const { url, key } = JSON.parse(presignedUrlResponse.data);
|
||||
console.log('key: ', key);
|
||||
|
||||
await ExpenseService.uploadAttachmentWithPresignedUrl(file, url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_open,
|
||||
is_loading,
|
||||
|
|
@ -62,5 +79,6 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
upsertExpense,
|
||||
deleteExpenseById,
|
||||
close,
|
||||
uploadAttachment,
|
||||
};
|
||||
});
|
||||
17
src/utils/crc-encoder.ts
Normal file
17
src/utils/crc-encoder.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { crc32 } from 'crc';
|
||||
|
||||
export async function computeCRC32Base64(file: File): Promise<string> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// Pass arrayBuffer directly to crc32
|
||||
let crc = crc32(arrayBuffer);
|
||||
crc >>>= 0;
|
||||
|
||||
// Convert to 4-byte big-endian
|
||||
const buffer = new ArrayBuffer(4);
|
||||
const view = new DataView(buffer);
|
||||
view.setUint32(0, crc, false);
|
||||
|
||||
// Base64 encode
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user