Skip to content
Snippets Groups Projects
UploadObjectModal.vue 7.89 KiB
Newer Older
  • Learn to ignore specific revisions
  • <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";
    
    Daniel Göbel's avatar
    Daniel Göbel committed
    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>
    
    Daniel Göbel's avatar
    Daniel Göbel committed
      <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>
    
    Daniel Göbel's avatar
    Daniel Göbel committed
      <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">
    
    Daniel Göbel's avatar
    Daniel Göbel committed
              {{ 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>