diff --git a/src/App.vue b/src/App.vue index ba93ceeaa7158c17baf9a7b1e8f609ce2a33b322..a9b94a9a34d38fffe98a9e950256d42049652a10 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 ee07760cfbe973c91f0919e57dd86210f61c818c..c3bb9a44c8437cf3c8c845f3cae3b00b5e478bd3 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 9de29087f9c9d5cabc4e6f4d56aaba864808f23a..08f5280a86aac2400e20909bae8723377ac92916 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 a0b5f354ab8ee7e03f16e833b20ae49ace12ea6f..ca92ccf7aa938bbb83a06bd93767a5ab4c2cd442 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 c2ed8e1fb2698b3e4ecfd3fca1745655d24c5221..ab6889076b69551a521fb5476d810ce20df9fc01 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 6665e36cc10efc9df2405f462a8c48e1513d101e..2d1c8234d12d6b8b77e6cdf6d8f291b1e776c502 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>