<script setup lang="ts"> import type { S3Client } from "@aws-sdk/client-s3"; import { Upload } from "@aws-sdk/lib-storage"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import { computed, onMounted, reactive, watch } from "vue"; import type { ComputedRef } from "vue"; import type { S3ObjectMetaInformation } from "@/client/s3proxy"; import dayjs from "dayjs"; import { filesize } from "filesize"; import { Modal, Toast } from "bootstrap"; const props = defineProps<{ modalID: string; bucketName: string; keyPrefix: string; s3Client: S3Client; editObjectFileName: string | undefined; }>(); const randomIDSuffix = Math.random().toString(16).substr(2, 8); let uploadModal: Modal | null = null; let successToast: Toast | null = null; let errorToast: Toast | null = null; const currentFolders: ComputedRef<string[]> = computed(() => props.keyPrefix.split("/") ); const emit = defineEmits<{ (e: "object-created", object: S3ObjectMetaInformation): void; }>(); watch( () => props.editObjectFileName, (nextFileName) => { formState.key = nextFileName ?? ""; } ); const formState = reactive({ file: {}, key: "", uploading: false, uploadDone: 0, uploadTotal: 1, } as { file: File; key: string; uploading: boolean; uploadDone: number; uploadTotal: number; }); const uploadProgress: ComputedRef<number> = computed(() => Math.round((100 * formState.uploadDone) / formState.uploadTotal) ); const editObject: ComputedRef<boolean> = computed( () => props.editObjectFileName !== undefined ); async function uploadObject() { const key = props.keyPrefix.length > 0 ? props.keyPrefix + "/" + formState.key : formState.key; try { formState.uploadDone = 0; formState.uploading = true; const parallelUploads3 = new Upload({ client: props.s3Client, params: { Bucket: props.bucketName, Body: formState.file, ContentType: formState.file.type, Key: key, }, queueSize: 4, // optional concurrency configuration partSize: 1024 * 1024 * 5, // optional size of each part, in bytes, at least 5MB leavePartsOnError: false, // optional manually handle dropped parts }); parallelUploads3.on("httpUploadProgress", (progress) => { if (progress.loaded != null && progress.total != null) { formState.uploadDone = progress.loaded; formState.uploadTotal = progress.total; } }); await parallelUploads3.done(); uploadModal?.hide(); successToast?.show(); emit("object-created", { key: key, bucket: props.bucketName, size: formState.file?.size ?? 0, last_modified: dayjs().toISOString(), content_type: formState.file?.type ?? "text/plain", }); formState.key = ""; ( document.getElementById("objectFile" + randomIDSuffix) as HTMLInputElement ).value = ""; } catch (e) { console.error(e); errorToast?.show(); } finally { formState.uploading = false; } } // eslint-disable-next-line function fileChange(event: any) { formState.file = event.target.files[0]; if (!editObject.value) { formState.key = formState.file.name; } } onMounted(() => { uploadModal = new Modal("#" + props.modalID); successToast = new Toast("#successToast-" + randomIDSuffix); errorToast = new Toast("#errorToast-" + randomIDSuffix); }); </script> <template> <div class="toast-container position-fixed top-toast end-0 p-3"> <div role="alert" aria-live="assertive" aria-atomic="true" class="toast text-bg-success align-items-center border-0" data-bs-autohide="true" :id="'successToast-' + randomIDSuffix" > <div class="d-flex"> <div class="toast-body">Successfully uploaded file</div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" ></button> </div> </div> </div> <div class="toast-container position-fixed top-toast end-0 p-3"> <div role="alert" aria-live="assertive" aria-atomic="true" class="toast text-bg-danger align-items-center border-0" data-bs-autohide="true" :id="'errorToast-' + randomIDSuffix" > <div class="d-flex"> <div class="toast-body"> There has been some Error.<br /> Try again later </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" ></button> </div> </div> </div> <bootstrap-modal :modalID="modalID" :static-backdrop="true" modal-label="Upload Object Modal" > <template v-slot:header> <h4>Upload file to</h4> <ol class="breadcrumb"> <li class="breadcrumb-item">{{ props.bucketName }}</li> <li class="breadcrumb-item" v-for="folder in currentFolders" :key="folder" > {{ folder }} </li> </ol> </template> <template v-slot:body> <div class="container-fluid"> <div class="row"> <form class="col-7" :id="'uploadObjectForm' + randomIDSuffix" @submit.prevent="uploadObject" > <div class="mb-3"> <label :for="'objectFile' + randomIDSuffix" class="form-label" v-if="editObject" > New File Content * </label> <label :for="'objectFile' + randomIDSuffix" class="form-label" v-else > File * </label> <input class="form-control" type="file" :id="'objectFile' + randomIDSuffix" required @change="fileChange" /> </div> <div class="mb-3"> <label :for="'objectKey' + randomIDSuffix" class="form-label" >Filename</label > <input type="text" :class="{ 'form-control-plaintext': editObject, 'form-control': !editObject, }" :id="'objectKey' + randomIDSuffix" required :disabled="editObject" v-model="formState.key" /> </div> </form> <div class="col-5"> Note: Delimiters ('/') are allowed in the file name to place the new file into a folder that will be created when the file is uploaded (to any depth of folders). </div> </div> </div> </template> <template v-slot:footer> <div class="w-50 me-auto" v-if="formState.uploading"> <div class="progress"> <div class="progress-bar bg-info" role="progressbar" aria-label="Basic example" :style="{ width: uploadProgress + '%' }" :aria-valuenow="uploadProgress" aria-valuemin="0" aria-valuemax="100" > {{ uploadProgress }}% </div> </div> <span v-if="formState.uploadDone > 0"> {{ filesize(formState.uploadDone) }} / {{ filesize(formState.uploadTotal) }} </span> </div> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button> <button :disabled="formState.uploading" type="submit" :form="'uploadObjectForm' + randomIDSuffix" class="btn btn-primary" > <span v-if="formState.uploading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" ></span> Upload </button> </template> </bootstrap-modal> </template> <style scoped></style>