From b11365cfa3569e76ee1ebf350e2f7c7afa26208a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Mon, 5 Sep 2022 16:35:41 +0200 Subject: [PATCH] Add copy object modal to copy a file to another bucket #15 --- src/components/BucketView.vue | 68 +++--- src/components/Modals/CopyObjectModal.vue | 216 ++++++++++++++++++++ src/components/Modals/UploadObjectModal.vue | 13 +- src/views/object-storage/BucketsView.vue | 17 +- 4 files changed, 283 insertions(+), 31 deletions(-) create mode 100644 src/components/Modals/CopyObjectModal.vue diff --git a/src/components/BucketView.vue b/src/components/BucketView.vue index 6d261da..2a9c4f0 100644 --- a/src/components/BucketView.vue +++ b/src/components/BucketView.vue @@ -1,7 +1,11 @@ <script setup lang="ts"> import { onMounted, reactive, watch, computed } from "vue"; import type { ComputedRef } from "vue"; -import type { S3ObjectMetaInformation, BucketPermission } from "@/client"; +import type { + S3ObjectMetaInformation, + BucketPermission, + BucketOut, +} from "@/client"; import { ObjectService } from "@/client"; import BootstrapIcon from "@/components/BootstrapIcon.vue"; import fileSize from "filesize"; @@ -9,6 +13,7 @@ import dayjs from "dayjs"; import { Toast, Tooltip } from "bootstrap"; import PermissionListModal from "@/components/Modals/PermissionListModal.vue"; import UploadObjectModal from "@/components/Modals/UploadObjectModal.vue"; +import CopyObjectModal from "@/components/Modals/CopyObjectModal.vue"; import PermissionModal from "@/components/Modals/PermissionModal.vue"; import CreateFolderModal from "@/components/Modals/CreateFolderModal.vue"; import { @@ -23,6 +28,19 @@ import { useAuthStore } from "@/stores/auth"; const authStore = useAuthStore(); +const middleware = [ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (next) => async (args) => { + args.request.headers["host"] = import.meta.env.VITE_S3_URL.split("://")[1]; + return await next(args); + }, + { + relation: "before", + toMiddleware: awsAuthMiddlewareOptions?.name ?? "impossible", + }, +]; + let client = new S3Client({ region: "us-east-1", endpoint: import.meta.env.VITE_S3_URL, @@ -31,20 +49,11 @@ let client = new S3Client({ accessKeyId: authStore.s3key?.access_key ?? "", secretAccessKey: authStore.s3key?.secret_key ?? "", }, - tls: false, + tls: import.meta.env.VITE_S3_URL.startsWith("https"), }); -client.middlewareStack.addRelativeTo( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (next) => async (args) => { - args.request.headers["host"] = "localhost:9998"; - return await next(args); - }, - { - relation: "before", - toMiddleware: awsAuthMiddlewareOptions?.name ?? "impossible", - } -); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +client.middlewareStack.addRelativeTo(middleware[0], middleware[1]); // If S3 Key changes authStore.$onAction(({ name, args }) => { @@ -60,20 +69,11 @@ authStore.$onAction(({ name, args }) => { accessKeyId: args[0].access_key, secretAccessKey: args[0].secret_key, }, - tls: false, + tls: import.meta.env.VITE_S3_URL.startsWith("https"), }); - client.middlewareStack.addRelativeTo( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (next) => async (args) => { - args.request.headers["host"] = "localhost:9998"; - return await next(args); - }, - { - relation: "before", - toMiddleware: awsAuthMiddlewareOptions?.name ?? "impossible", - } - ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + client.middlewareStack.addRelativeTo(middleware[0], middleware[1]); } } }); @@ -85,6 +85,7 @@ const props = defineProps<{ bucketName: string; subFolders: string[] | string; permission: BucketPermission | undefined; + writableBuckets: BucketOut[]; }>(); const randomIDSuffix = Math.random().toString(16).substr(2, 8); let successToast: Toast | null = null; @@ -119,6 +120,7 @@ const objectState = reactive({ createdPermission: undefined, deletedItem: "", editObjectKey: "", + copyObjectKey: "", } as { objects: S3ObjectMetaInformation[]; loading: boolean; @@ -127,6 +129,7 @@ const objectState = reactive({ createdPermission: undefined | BucketPermission; deletedItem: string; editObjectKey: string; + copyObjectKey: string; }); // Watcher @@ -759,6 +762,9 @@ watch( class="dropdown-item" type="button" :disabled="!writeS3Permission" + data-bs-toggle="modal" + data-bs-target="#copy-object-modal" + @click="objectState.copyObjectKey = obj.key" > Copy </button> @@ -817,6 +823,14 @@ watch( :edit-object-file-name="getObjectFileName(objectState.editObjectKey)" @object-created="objectUploaded" /> + <copy-object-modal + :source-bucket-name="props.bucketName" + :source-key="objectState.copyObjectKey" + :s3-client="client" + modalID="copy-object-modal" + modal-label="some-label" + :available-buckets="props.writableBuckets" + /> </div> </div> </template> diff --git a/src/components/Modals/CopyObjectModal.vue b/src/components/Modals/CopyObjectModal.vue new file mode 100644 index 0000000..690a1be --- /dev/null +++ b/src/components/Modals/CopyObjectModal.vue @@ -0,0 +1,216 @@ +<script setup lang="ts"> +import type { S3Client } from "@aws-sdk/client-s3"; +import { CopyObjectCommand } from "@aws-sdk/client-s3"; +import BootstrapModal from "@/components/Modals/BootstrapModal.vue"; +import type { BucketOut } from "@/client"; +import { Modal, Toast } from "bootstrap"; +import { onMounted, reactive, watch, computed } from "vue"; +import type { ComputedRef } from "vue"; + +const props = defineProps<{ + modalID: string; + modalLabel: string; + sourceBucketName: string; + sourceKey: string; + s3Client: S3Client; + availableBuckets: BucketOut[]; +}>(); + +const formState = reactive({ + destKey: "", + destBucket: "", + uploading: false, +} as { + destKey: string; + destBucket: string; + uploading: boolean; +}); + +const randomIDSuffix = Math.random().toString(16).substr(2, 8); +let copyModal: Modal | null = null; +let successToast: Toast | null = null; +let errorToast: Toast | null = null; + +const sourceFilteredBuckets: ComputedRef<BucketOut[]> = computed(() => { + return props.availableBuckets.filter( + (bucket) => bucket.name !== props.sourceBucketName + ); +}); + +function getFileName(key: string): string { + const spliitedKey = key.split("/"); + return spliitedKey[spliitedKey.length - 1]; +} + +function copyObject() { + const command = new CopyObjectCommand({ + Bucket: formState.destBucket, + CopySource: encodeURI(`/${props.sourceBucketName}/${props.sourceKey}`), + Key: formState.destKey, + }); + formState.uploading = true; + props.s3Client + .send(command) + .then(() => { + copyModal?.hide(); + successToast?.show(); + formState.destBucket = ""; + }) + .catch((e) => { + console.error(e); + errorToast?.show(); + }) + .finally(() => { + formState.uploading = false; + }); +} + +function modalClosed() { + formState.destBucket = ""; +} + +watch( + () => props.sourceKey, + (newKey) => { + formState.destKey = newKey; + } +); + +onMounted(() => { + copyModal = new Modal("#" + props.modalID); + successToast = new Toast("#successToast-" + randomIDSuffix); + errorToast = new Toast("#errorToast-" + randomIDSuffix); +}); +</script> + +<template> + <div class="toast-container position-fixed top-0 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 copied 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-0 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="modalLabel" + v-on="{ 'hidden.bs.modal': modalClosed }" + > + <template v-slot:header> + <h4>Copy file {{ getFileName(props.sourceKey) }}</h4> + </template> + <template v-slot:body> + <div class="container-fluid"> + <div class="row"> + <form + class="col-7" + :id="'copyObjectForm' + randomIDSuffix" + @submit.prevent="copyObject" + > + <div class="mb-3"> + <label + :for="'destinationBucket' + randomIDSuffix" + class="form-label" + > + Destination Bucket * + </label> + <select + class="form-select text-lowercase" + :id="'destinationBucket' + randomIDSuffix" + required + v-model="formState.destBucket" + > + <option disabled selected>Select one...</option> + <option + v-for="bucket in sourceFilteredBuckets" + :key="bucket.name" + :value="bucket.name" + > + {{ bucket.name }} + </option> + </select> + </div> + <div class="mb-3"> + <label :for="'objectKey' + randomIDSuffix" class="form-label" + >Destination Filename *</label + > + <input + type="text" + class="form-control" + :id="'objectKey' + randomIDSuffix" + required + v-model="formState.destKey" + /> + </div> + </form> + <div class="col-5"> + You can copy objects. You have to create destination container prior + to copy.<br /> + You can specify folder by using '/' at destination object field. For + example, if you want to copy object under the folder named + 'folder1', you need to specify destination object like + 'folder1/[your object name]'. + </div> + </div> + </div> + </template> + <template v-slot:footer> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <button + :disabled="formState.uploading" + type="submit" + :form="'copyObjectForm' + randomIDSuffix" + class="btn btn-primary" + > + <span + v-if="formState.uploading" + class="spinner-border spinner-border-sm" + role="status" + aria-hidden="true" + ></span> + Copy + </button> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/src/components/Modals/UploadObjectModal.vue b/src/components/Modals/UploadObjectModal.vue index ca819ef..90eacc2 100644 --- a/src/components/Modals/UploadObjectModal.vue +++ b/src/components/Modals/UploadObjectModal.vue @@ -194,7 +194,18 @@ onMounted(() => { @submit.prevent="uploadObject" > <div class="mb-3"> - <label :for="'objectFile' + randomIDSuffix" class="form-label"> + <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 diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue index 2f02c94..c01d35e 100644 --- a/src/views/object-storage/BucketsView.vue +++ b/src/views/object-storage/BucketsView.vue @@ -1,8 +1,8 @@ <script setup lang="ts"> -import { onMounted, reactive, computed } from "vue"; import type { ComputedRef } from "vue"; +import { computed, onMounted, reactive } from "vue"; import type { BucketOut, BucketPermission } from "@/client"; -import { BucketService, BucketPermissionsService } from "@/client"; +import { BucketPermissionsService, BucketService } from "@/client"; import { useRoute, useRouter } from "vue-router"; import BootstrapIcon from "@/components/BootstrapIcon.vue"; import CreateBucketModal from "@/components/Modals/CreateBucketModal.vue"; @@ -44,6 +44,14 @@ function fetchBuckets() { } } +const writableBuckets: ComputedRef<BucketOut[]> = computed(() => { + return bucketsState.buckets.filter( + (bucket) => + bucketsState.permissions[bucket.name] === undefined || + bucketsState.permissions[bucket.name].permission !== "READ" + ); +}); + const currentPermission: ComputedRef<BucketPermission | undefined> = computed( () => { return bucketsState.permissions[route.params.bucketName as string]; @@ -148,7 +156,10 @@ onMounted(() => { </div> </div> <div class="col-9"> - <router-view :permission="currentPermission"></router-view> + <router-view + :permission="currentPermission" + :writable-buckets="writableBuckets" + ></router-view> </div> </div> </template> -- GitLab