Skip to content
Snippets Groups Projects

Add direct S3 interaction

Merged Daniel Göbel requested to merge feature/s3-interaction into development
1 file
+ 10
4
Compare changes
  • Side-by-side
  • Inline
+ 314
13
<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";
import dayjs from "dayjs";
import { Tooltip } from "bootstrap";
import PermissionListModal from "@/components/PermissionListModal.vue";
import PermissionModal from "@/components/PermissionModal.vue";
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 ObjectDetailModal from "@/components/Modals/ObjectDetailModal.vue";
import CreateFolderModal from "@/components/Modals/CreateFolderModal.vue";
import {
S3Client,
DeleteObjectCommand,
DeleteObjectsCommand,
GetObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { awsAuthMiddlewareOptions } from "@aws-sdk/middleware-signing";
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,
forcePathStyle: true,
credentials: {
accessKeyId: authStore.s3key?.access_key ?? "",
secretAccessKey: authStore.s3key?.secret_key ?? "",
},
tls: import.meta.env.VITE_S3_URL.startsWith("https"),
});
// 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 }) => {
if (name === "setS3Key") {
if (args[0] === null) {
console.error("There are no S3 Keys");
} else {
client = new S3Client({
region: "us-east-1",
endpoint: import.meta.env.VITE_S3_URL,
forcePathStyle: true,
credentials: {
accessKeyId: args[0].access_key,
secretAccessKey: args[0].secret_key,
},
tls: import.meta.env.VITE_S3_URL.startsWith("https"),
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
client.middlewareStack.addRelativeTo(middleware[0], middleware[1]);
}
}
});
// Constants
// -----------------------------------------------------------------------------
@@ -17,7 +86,10 @@ 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;
// Typescript types
// -----------------------------------------------------------------------------
@@ -47,12 +119,30 @@ const objectState = reactive({
bucketNotFoundError: false,
bucketPermissionError: false,
createdPermission: undefined,
deletedItem: "",
editObjectKey: "",
copyObject: {
key: "",
size: 0,
bucket: "",
last_modified: "2022-01-01",
},
viewDetailObject: {
key: "",
size: 0,
bucket: "",
last_modified: "2022-01-01",
},
} as {
objects: S3ObjectMetaInformation[];
loading: boolean;
bucketNotFoundError: boolean;
bucketPermissionError: boolean;
createdPermission: undefined | BucketPermission;
deletedItem: string;
editObjectKey: string;
copyObject: S3ObjectMetaInformation;
viewDetailObject: S3ObjectMetaInformation;
});
// Watcher
@@ -176,7 +266,7 @@ const visibleObjects: ComputedRef<(S3ObjectWithFolder | S3PseudoFolder)[]> =
} as S3PseudoFolder;
})
);
return arr;
return arr.filter((obj) => !obj.key.endsWith(".s3keep"));
});
const subFolderInUrl: ComputedRef<boolean> = computed(
@@ -186,6 +276,11 @@ const errorLoadingObjects: ComputedRef<boolean> = computed(
() => objectState.bucketPermissionError || objectState.bucketNotFoundError
);
const writeS3Permission: ComputedRef<boolean> = computed(
() =>
props.permission == undefined || props.permission.permission == "READWRITE"
);
// Lifecycle Hooks
// -----------------------------------------------------------------------------
onMounted(() => {
@@ -195,6 +290,7 @@ onMounted(() => {
.forEach(
(tooltipTriggerEl) => new Tooltip(tooltipTriggerEl, { trigger: "hover" })
);
successToast = new Toast("#successToast-" + randomIDSuffix);
});
// Functions
@@ -267,6 +363,110 @@ function isS3Object(
return (obj as S3ObjectWithFolder).folder !== undefined;
}
/**
* callback function when an object has been uploaded
* @param newObject Uploaded object
*/
function objectUploaded(newObject: S3ObjectMetaInformation) {
const index = objectState.objects
.map((obj) => obj.key)
.indexOf(newObject.key);
if (index > -1) {
objectState.objects[index] = newObject;
} else {
objectState.objects.push(newObject);
}
}
/**
* callback function when an object has been copied
* @param copiedObject Uploaded object
*/
function objectCopied(copiedObject: S3ObjectMetaInformation) {
if (copiedObject.bucket === props.bucketName) {
objectState.objects.push(copiedObject);
}
}
/**
* Delete an Object in the current folder
* @param key Key of the Object
*/
function deleteObject(key: string) {
const command = new DeleteObjectCommand({
Bucket: props.bucketName,
Key: key,
});
client
.send(command)
.then(() => {
const splittedKey = key.split("/");
objectState.deletedItem = splittedKey[splittedKey.length - 1];
successToast?.show();
objectState.objects = objectState.objects.filter(
(obj) => obj.key !== key
);
})
.catch((err) => {
console.error(err);
});
}
/**
* Initiate the download of the provided object
* @param key Key of the object
* @param bucket Bucket of the object
*/
async function downloadObject(key: string, bucket: string) {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
const url = await getSignedUrl(client, command, { expiresIn: 30 });
//creating an invisible element
const element = document.createElement("a");
element.setAttribute("href", url);
element.setAttribute("target", "_blank");
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/**
* Delete a folder in the current Bucket
* @param folderPath Path to the folder with a trailing "/", e.g. some/path/to/a/folder/
*/
function deleteFolder(folderPath: string) {
const command = new DeleteObjectsCommand({
Bucket: props.bucketName,
Delete: {
Objects: objectState.objects
.filter((obj) => obj.key.startsWith(folderPath))
.map((obj) => {
return { Key: obj.key };
}),
},
});
client
.send(command)
.then(() => {
const splittedPath = folderPath.split("/");
objectState.deletedItem = splittedPath[splittedPath.length - 2];
successToast?.show();
objectState.objects = objectState.objects.filter(
(obj) => !obj.key.startsWith(folderPath)
);
})
.catch((err) => {
console.error(err);
});
}
function getObjectFileName(key: string): string {
const splittedKey = key.split("/");
return splittedKey[splittedKey.length - 1];
}
watch(
visibleObjects,
(visObjs) => {
@@ -284,6 +484,28 @@ watch(
</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 deleted {{ objectState.deletedItem }}
</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>
<!-- Navbar Breadcrumb -->
<nav aria-label="breadcrumb" class="fs-2">
<ol class="breadcrumb">
@@ -342,25 +564,44 @@ watch(
<button
type="button"
class="btn btn-secondary me-2 tooltip-container"
:disabled="errorLoadingObjects"
data-bs-toggle="tooltip"
:disabled="errorLoadingObjects || !writeS3Permission"
data-bs-toggle="modal"
data-bs-title="Upload Object"
data-bs-target="#upload-object-modal"
>
<bootstrap-icon icon="upload" :width="16" :height="16" fill="white" />
<span class="visually-hidden">Upload Object</span>
</button>
<upload-object-modal
:bucket-name="props.bucketName"
:s3-client="client"
modalID="upload-object-modal"
modal-label="some-label"
:key-prefix="currentSubFolders.join('/')"
:edit-object-file-name="undefined"
@object-created="objectUploaded"
/>
<!-- Add folder button -->
<button
type="button"
class="btn btn-secondary m-2 tooltip-container"
:disabled="errorLoadingObjects"
data-bs-toggle="tooltip"
:disabled="errorLoadingObjects || !writeS3Permission"
data-bs-toggle="modal"
data-bs-title="Create Folder"
data-bs-target="#create-folder-modal"
>
<bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" />
Folder
<span class="visually-hidden">Add Folder</span>
</button>
<create-folder-modal
:bucket-name="props.bucketName"
:s3-client="client"
modalID="create-folder-modal"
modal-label="some-label"
:key-prefix="currentSubFolders.join('/')"
@folder-created="objectUploaded"
/>
<!-- Add bucket permission button -->
<button
:hidden="props.permission != null"
@@ -507,7 +748,11 @@ watch(
class="btn-group btn-group-sm dropdown-center dropdown-menu-start"
>
<!-- Download Button -->
<button type="button" class="btn btn-secondary">
<button
type="button"
class="btn btn-secondary"
@click="downloadObject(obj.key, props.bucketName)"
>
Download
</button>
<button
@@ -521,18 +766,46 @@ watch(
<!-- Dropdown menu -->
<ul class="dropdown-menu dropdown-menu-dark">
<li>
<button class="dropdown-item" type="button">Details</button>
<button
class="dropdown-item"
type="button"
data-bs-toggle="modal"
data-bs-target="#detail-object-modal"
@click="objectState.viewDetailObject = obj"
>
Details
</button>
</li>
<li>
<button class="dropdown-item" type="button">Edit</button>
<button
class="dropdown-item"
type="button"
:disabled="!writeS3Permission"
data-bs-toggle="modal"
data-bs-target="#edit-object-modal"
@click="objectState.editObjectKey = obj.key"
>
Edit
</button>
</li>
<li>
<button class="dropdown-item" type="button">Copy</button>
<button
class="dropdown-item"
type="button"
:disabled="!writeS3Permission"
data-bs-toggle="modal"
data-bs-target="#copy-object-modal"
@click="objectState.copyObject = obj"
>
Copy
</button>
</li>
<li>
<button
class="dropdown-item text-danger align-middle"
type="button"
@click="deleteObject(obj.key)"
:disabled="!writeS3Permission"
>
<bootstrap-icon
icon="trash-fill"
@@ -551,6 +824,12 @@ watch(
<button
type="button"
class="btn btn-danger btn-sm align-middle"
:disabled="!writeS3Permission"
@click="
deleteFolder(
obj.parentFolder.concat(['']).join('/') + obj.name + '/'
)
"
>
<bootstrap-icon
icon="trash-fill"
@@ -566,6 +845,28 @@ watch(
</tr>
</tbody>
</table>
<upload-object-modal
:bucket-name="props.bucketName"
:s3-client="client"
modalID="edit-object-modal"
modal-label="some-label"
:key-prefix="currentSubFolders.join('/')"
:edit-object-file-name="getObjectFileName(objectState.editObjectKey)"
@object-created="objectUploaded"
/>
<copy-object-modal
:source-object="objectState.copyObject"
:s3-client="client"
modalID="copy-object-modal"
modal-label="some-label"
:available-buckets="props.writableBuckets"
@object-copied="objectCopied"
/>
<object-detail-modal
:s3-object="objectState.viewDetailObject"
modalID="detail-object-modal"
modal-label="some-label"
/>
</div>
</div>
</template>
Loading