Compare commits

...

3 Commits

9 changed files with 86 additions and 17 deletions

View File

@ -55,7 +55,7 @@ services:
Get the latest binary from the [release page](https://github.com/khairul169/garage-webui/releases/latest) according to your OS architecture. For example:
```sh
$ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.2/garage-webui-v1.0.2-linux-amd64
$ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.3/garage-webui-v1.0.3-linux-amd64
$ chmod +x garage-webui
$ sudo cp garage-webui /usr/local/bin
```

View File

@ -16,6 +16,7 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
)
@ -183,6 +184,8 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
bucket := r.PathValue("bucket")
key := r.PathValue("key")
recursive := r.URL.Query().Get("recursive") == "true"
isDirectory := strings.HasSuffix(key, "/")
client, err := getS3Client(bucket)
if err != nil {
@ -190,7 +193,52 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
return
}
_, err = client.DeleteObject(context.Background(), &s3.DeleteObjectInput{
// Delete directory and its content
if isDirectory && recursive {
objects, err := client.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String(key),
})
if err != nil {
utils.ResponseError(w, err)
return
}
if len(objects.Contents) == 0 {
utils.ResponseSuccess(w, true)
return
}
keys := make([]types.ObjectIdentifier, 0, len(objects.Contents))
for _, object := range objects.Contents {
keys = append(keys, types.ObjectIdentifier{
Key: object.Key,
})
}
res, err := client.DeleteObjects(context.Background(), &s3.DeleteObjectsInput{
Bucket: aws.String(bucket),
Delete: &types.Delete{Objects: keys},
})
if err != nil {
utils.ResponseError(w, fmt.Errorf("cannot delete object: %w", err))
return
}
if len(res.Errors) > 0 {
utils.ResponseError(w, fmt.Errorf("cannot delete object: %v", res.Errors[0]))
return
}
utils.ResponseSuccess(w, res)
return
}
// Delete single object
res, err := client.DeleteObject(context.Background(), &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
@ -200,7 +248,7 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
return
}
utils.ResponseSuccess(w, nil)
utils.ResponseSuccess(w, res)
}
func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {

View File

@ -1,7 +1,7 @@
{
"name": "garage-webui",
"private": true,
"version": "1.0.2",
"version": "1.0.3",
"type": "module",
"scripts": {
"dev:client": "vite",

View File

@ -21,7 +21,7 @@ const MainLayout = () => {
<Drawer
open={sidebar.isOpen}
onClickOverlay={sidebar.onClose}
className="md:drawer-open h-screen"
className="md:drawer-open h-screen max-h-dvh"
side={<Sidebar />}
contentClassName="flex flex-col overflow-hidden"
>

View File

@ -40,10 +40,13 @@ export const usePutObject = (
export const useDeleteObject = (
bucket: string,
options?: UseMutationOptions<any, Error, string>
options?: UseMutationOptions<any, Error, { key: string; recursive?: boolean }>
) => {
return useMutation({
mutationFn: (key) => api.delete(`/browse/${bucket}/${key}`),
mutationFn: (data) =>
api.delete(`/browse/${bucket}/${data.key}`, {
params: { recursive: data.recursive },
}),
...options,
});
};

View File

@ -13,9 +13,10 @@ import { shareDialog } from "./share-dialog";
type Props = {
prefix?: string;
object: Pick<Object, "objectKey" | "downloadUrl">;
end?: boolean;
};
const ObjectActions = ({ prefix = "", object }: Props) => {
const ObjectActions = ({ prefix = "", object, end }: Props) => {
const { bucketName } = useBucketContext();
const queryClient = useQueryClient();
const isDirectory = object.objectKey.endsWith("/");
@ -33,8 +34,17 @@ const ObjectActions = ({ prefix = "", object }: Props) => {
};
const onDelete = () => {
if (window.confirm("Are you sure you want to delete this object?")) {
deleteObject.mutate(prefix + object.objectKey);
if (
window.confirm(
`Are you sure you want to delete this ${
isDirectory ? "directory and its content" : "object"
}?`
)
) {
deleteObject.mutate({
key: prefix + object.objectKey,
recursive: isDirectory,
});
}
};
@ -45,12 +55,12 @@ const ObjectActions = ({ prefix = "", object }: Props) => {
<Button icon={DownloadIcon} color="ghost" onClick={onDownload} />
)}
<Dropdown end>
<Dropdown end vertical={end ? "top" : "bottom"}>
<Dropdown.Toggle button={false}>
<Button icon={EllipsisVertical} color="ghost" />
</Dropdown.Toggle>
<Dropdown.Menu className="bg-base-300 gap-y-1">
<Dropdown.Menu className="gap-y-1">
<Dropdown.Item
onClick={() =>
shareDialog.open({ key: object.objectKey, prefix })

View File

@ -23,7 +23,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => {
};
return (
<div className="overflow-x-auto pb-32">
<div className="overflow-x-auto overflow-y-hidden">
<Table>
<Table.Head>
<span>Name</span>
@ -63,7 +63,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => {
</tr>
))}
{data?.objects.map((object) => {
{data?.objects.map((object, idx) => {
const extIdx = object.objectKey.lastIndexOf(".");
const filename =
extIdx >= 0
@ -93,7 +93,11 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => {
<td className="whitespace-nowrap">
{dayjs(object.lastModified).fromNow()}
</td>
<ObjectActions prefix={data.prefix} object={object} />
<ObjectActions
prefix={data.prefix}
object={object}
end={idx >= data.objects.length - 2}
/>
</tr>
);
})}

View File

@ -184,7 +184,7 @@ const NodesList = ({ nodes }: NodeListProps) => {
</Alert>
) : null}
<div className="w-full overflow-x-auto min-h-[400px] pb-16">
<div className="w-full overflow-x-auto overflow-y-hidden min-h-[400px]">
<Table size="sm" className="min-w-[800px]">
<Table.Head>
<span>#</span>
@ -266,7 +266,10 @@ const NodesList = ({ nodes }: NodeListProps) => {
: "Inactive"}
</Badge>
<Dropdown end>
<Dropdown
end
vertical={idx >= items.length - 2 ? "top" : "bottom"}
>
<Dropdown.Toggle button={false}>
<Button shape="circle" color="ghost">
<EllipsisVertical />

View File

@ -96,6 +96,7 @@ const KeysPage = () => {
icon={Eye}
size="sm"
onClick={() => fetchSecretKey(key.id)}
className="shrink-0 min-w-[80px]"
>
View
</Button>