From 06fd123e7042e7d35aa09bb9121ac4e6b7e9a1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Fri, 3 Nov 2023 11:57:26 +0100 Subject: [PATCH] Fetch meta information about S3 Object dynamically #79 --- src/App.vue | 2 +- src/components/NavbarTop.vue | 2 +- .../modals/ObjectDetailModal.vue | 97 ++++++++++++++++--- src/main.ts | 7 ++ src/stores/s3objects.ts | 30 +++++- src/views/object-storage/BucketView.vue | 13 +-- 6 files changed, 127 insertions(+), 24 deletions(-) diff --git a/src/App.vue b/src/App.vue index ba93cee..a9b94a9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -27,7 +27,7 @@ onBeforeMount(() => { err.response.data.detail?.includes("JWT") ) { store.logout(); - cookies.remove("bearer", undefined, window.location.hostname); + cookies.remove("bearer"); router.push({ name: "login", query: { diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue index ee07760..c3bb9a4 100644 --- a/src/components/NavbarTop.vue +++ b/src/components/NavbarTop.vue @@ -17,7 +17,7 @@ const route = useRoute(); function logout() { store.logout(); - cookies.remove("bearer", undefined, window.location.hostname); + cookies.remove("bearer"); } const activeRoute = ref(""); diff --git a/src/components/object-storage/modals/ObjectDetailModal.vue b/src/components/object-storage/modals/ObjectDetailModal.vue index 9de2908..08f5280 100644 --- a/src/components/object-storage/modals/ObjectDetailModal.vue +++ b/src/components/object-storage/modals/ObjectDetailModal.vue @@ -1,14 +1,54 @@ <script setup lang="ts"> import BootstrapModal from "@/components/modals/BootstrapModal.vue"; -import type { _Object as S3Object } from "@aws-sdk/client-s3"; import dayjs from "dayjs"; import { filesize } from "filesize"; +import { computed, onMounted, watch, reactive } from "vue"; +import { useS3ObjectStore } from "@/stores/s3objects"; + +const objectRepository = useS3ObjectStore(); const props = defineProps<{ modalID: string; - s3Object: S3Object; + objectKey: string | undefined; bucket: string; }>(); + +const detailState = reactive<{ + loading: boolean; + error: boolean; +}>({ + loading: true, + error: false, +}); + +const metaIdentifier = computed<string>(() => { + return props.bucket + "/" + props.objectKey; +}); + +watch(props, (newProps) => { + detailState.loading = true; + if (newProps.objectKey) { + fetchMetaInfo(newProps.bucket, newProps.objectKey); + } +}); + +function fetchMetaInfo(bucket: string, key: string) { + detailState.loading = true; + detailState.error = false; + objectRepository + .fetchS3ObjectMeta(bucket, key, () => { + detailState.loading = false; + }) + .catch(() => { + detailState.error = true; + }); +} + +onMounted(() => { + if (props.objectKey) { + fetchMetaInfo(props.bucket, props.objectKey); + } +}); </script> <template> @@ -30,32 +70,63 @@ const props = defineProps<{ </tr> <tr> <th scope="row">Name</th> - <td>{{ props.s3Object.Key }}</td> + <td>{{ props.objectKey }}</td> </tr> <tr> <th scope="row">Size</th> - <td> + <td v-if="detailState.error">N/A</td> + <td v-else-if="detailState.loading" class="placeholder-glow"> + <span class="placeholder col-2"></span> + </td> + <td v-else> {{ - filesize(props.s3Object.Size ?? 0, { - base: 2, - standard: "jedec", - }) + filesize( + objectRepository.objectMetaMapping[metaIdentifier] + .ContentLength ?? 0, + { + base: 2, + standard: "jedec", + }, + ) }} </td> </tr> <tr> <th scope="row">Timestamp</th> - <td> + <td v-if="detailState.error">N/A</td> + <td v-else-if="detailState.loading" class="placeholder-glow"> + <span class="placeholder col-6"></span> + </td> + <td v-else> {{ - dayjs(props.s3Object.LastModified).format( - "YYYY-MM-DD HH:mm:ss", - ) + dayjs( + objectRepository.objectMetaMapping[metaIdentifier] + .LastModified, + ).format("YYYY-MM-DD HH:mm:ss") }} </td> </tr> <tr> <th scope="row">Content Type</th> - <td>binary/octet-stream</td> + <td v-if="detailState.error">N/A</td> + <td v-else-if="detailState.loading" class="placeholder-glow"> + <span class="placeholder col-5"></span> + </td> + <td v-else> + {{ + objectRepository.objectMetaMapping[metaIdentifier].ContentType + }} + </td> + </tr> + <tr> + <th scope="row">ETag</th> + <td v-if="detailState.error">N/A</td> + <td v-else-if="detailState.loading" class="placeholder-glow"> + <span class="placeholder col-10"></span> + </td> + <td v-else> + {{ objectRepository.objectMetaMapping[metaIdentifier].ETag }} + </td> </tr> </tbody> </table> diff --git a/src/main.ts b/src/main.ts index a0b5f35..ca92ccf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,6 +28,13 @@ import "@fortawesome/fontawesome-free/css/brands.css"; import "./assets/main.css"; +import { globalCookiesConfig } from "vue3-cookies"; + +globalCookiesConfig({ + expireTimes: "8d", + domain: window.location.hostname, +}); + const app = createApp(App); app.use(createPinia()); diff --git a/src/stores/s3objects.ts b/src/stores/s3objects.ts index c2ed8e1..ab68890 100644 --- a/src/stores/s3objects.ts +++ b/src/stores/s3objects.ts @@ -1,8 +1,9 @@ import { defineStore } from "pinia"; -import type { _Object as S3Object } from "@aws-sdk/client-s3"; +import type { _Object as S3Object, HeadObjectOutput } from "@aws-sdk/client-s3"; import { CopyObjectCommand, GetObjectCommand, + HeadObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3"; @@ -24,6 +25,7 @@ export const useS3ObjectStore = defineStore({ state: () => ({ objectMapping: {}, + objectMetaMapping: {}, client: new S3Client({ region: "us-east-1", endpoint: environment.S3_URL, @@ -35,14 +37,19 @@ export const useS3ObjectStore = defineStore({ }), }) as { objectMapping: Record<string, S3Object[]>; + objectMetaMapping: Record<string, HeadObjectOutput>; client: S3Client; }, getters: { getPresignedUrl(): (bucketName: string, key: string) => Promise<string> { return (bucketName, key) => { + const keySplit = key.split("/"); const command = new GetObjectCommand({ Bucket: bucketName, Key: key, + ResponseContentDisposition: `attachment; filename=${ + keySplit[keySplit.length - 1] + }`, }); return getSignedUrl(this.client, command, { expiresIn: 30, @@ -99,6 +106,27 @@ export const useS3ObjectStore = defineStore({ } return objs; }, + fetchS3ObjectMeta( + bucketName: string, + key: string, + onFinally?: () => void, + ): Promise<HeadObjectOutput> { + const identifier = bucketName + "/" + key; + if (this.objectMetaMapping[identifier]) { + onFinally?.(); + } + const command = new HeadObjectCommand({ + Bucket: bucketName, + Key: key, + }); + return this.client + .send(command) + .then((resp) => { + this.objectMetaMapping[identifier] = resp; + return resp; + }) + .finally(onFinally); + }, deleteObject(bucketName: string, key: string): Promise<void> { const command = new DeleteObjectCommand({ Bucket: bucketName, diff --git a/src/views/object-storage/BucketView.vue b/src/views/object-storage/BucketView.vue index 6665e36..2d1c823 100644 --- a/src/views/object-storage/BucketView.vue +++ b/src/views/object-storage/BucketView.vue @@ -58,7 +58,7 @@ const objectState = reactive<{ bucketPermissionError: boolean; editObjectKey: string; copyObject: S3Object; - viewDetailObject: S3Object; + viewDetailKey: string | undefined; }>({ loading: true, filterString: "", @@ -70,11 +70,7 @@ const objectState = reactive<{ Size: 0, LastModified: new Date(), }, - viewDetailObject: { - Key: "", - Size: 0, - LastModified: new Date(), - }, + viewDetailKey: undefined, }); // Computed Properties @@ -232,6 +228,7 @@ watch( () => props.bucketName, (newBucketName, oldBucketName) => { if (oldBucketName !== newBucketName) { + objectState.viewDetailKey = undefined; // If bucket is changed, update the objects objectState.bucketPermissionError = false; objectState.bucketNotFoundError = false; @@ -735,7 +732,7 @@ function getObjectFileName(key: string): string { type="button" data-bs-toggle="modal" data-bs-target="#detail-object-modal" - @click="objectState.viewDetailObject = obj" + @click="objectState.viewDetailKey = obj.Key" > Details </button> @@ -814,7 +811,7 @@ function getObjectFileName(key: string): string { /> <object-detail-modal :bucket="bucketName" - :s3-object="objectState.viewDetailObject" + :object-key="objectState.viewDetailKey" modalID="detail-object-modal" /> </div> -- GitLab