mirror of
https://github.com/khairul169/garage-webui.git
synced 2025-04-28 14:59:31 +07:00
Compare commits
3 Commits
4861e1bbb1
...
5a90dd8377
Author | SHA1 | Date | |
---|---|---|---|
5a90dd8377 | |||
145bf3f1a9 | |||
7532c6330c |
@ -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
|
||||
```
|
||||
|
@ -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) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "garage-webui",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:client": "vite",
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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 })
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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 />
|
||||
|
@ -96,6 +96,7 @@ const KeysPage = () => {
|
||||
icon={Eye}
|
||||
size="sm"
|
||||
onClick={() => fetchSecretKey(key.id)}
|
||||
className="shrink-0 min-w-[80px]"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
|
Loading…
x
Reference in New Issue
Block a user