diff --git a/frontend/package.json.md5 b/frontend/package.json.md5
index 58dd7b9..283188a 100644
--- a/frontend/package.json.md5
+++ b/frontend/package.json.md5
@@ -1 +1 @@
-b20ef5a27687e07e09878451f9a2e1aa
\ No newline at end of file
+674272f31b23a798d77a17321c6a8785
\ No newline at end of file
diff --git a/frontend/src/component/file/list_bucket.tsx b/frontend/src/component/bucket/list_bucket.tsx
similarity index 95%
rename from frontend/src/component/file/list_bucket.tsx
rename to frontend/src/component/bucket/list_bucket.tsx
index a995da6..d2616a6 100644
--- a/frontend/src/component/file/list_bucket.tsx
+++ b/frontend/src/component/bucket/list_bucket.tsx
@@ -1,5 +1,5 @@
 import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
-import {ArchiveRegular, DocumentBulletListRegular} from "@fluentui/react-icons";
+import {ArchiveRegular} from "@fluentui/react-icons";
 import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
 import React from "react";
 import {useStoreBucket} from "../../store/bucket";
@@ -50,7 +50,7 @@ export function ListBucketComponent() {
 
     return <MenuList className={styles.container}>
         <VirtualizerScrollView
-            numItems={bucket_list.length}
+            numItems={bucket_list?bucket_list.length:0}
             itemSize={32}
             container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
         >
diff --git a/frontend/src/component/bucket/new.tsx b/frontend/src/component/bucket/new.tsx
new file mode 100644
index 0000000..6262b5d
--- /dev/null
+++ b/frontend/src/component/bucket/new.tsx
@@ -0,0 +1,111 @@
+import {
+    DialogTrigger,
+    DialogSurface,
+    DialogTitle,
+    DialogBody,
+    DialogActions,
+    DialogContent,
+    Button,  Field, Input,  makeStyles, tokens,  Checkbox,
+} from "@fluentui/react-components";
+import {useState} from "react";
+import {useToast} from "../../message";
+import {useStoreConnection} from "../../store/connection";
+import {useStoreBucket} from "../../store/bucket";
+
+const useStyle = makeStyles({
+    container: {
+        backgroundColor: tokens.colorNeutralBackground1,
+        display: "flex",
+        flexDirection: "row",
+        height: "100%",
+        width: "100%",
+        gridColumnStart: 0,
+    },
+    content: {},
+    input: {
+        margin: '1rem',
+    },
+    checks: {
+        margin: '1rem',
+        display: "flex",
+    },
+});
+
+export interface BucketCreateProps {
+    openFn: (open: boolean) => void;
+}
+
+export function BucketCreate(props: BucketCreateProps) {
+    const styles = useStyle();
+    const {dispatchMessage} = useToast();
+
+    const [name, set_name] = useState<string>();
+    const [public_read, set_public_read] = useState(false);
+    const [public_read_write, set_public_read_write] = useState(false);
+
+    const {conn_active} = useStoreConnection();
+    const {bucket_create, bucket_get} = useStoreBucket()
+
+    async function create() {
+        if (!name) {
+            dispatchMessage('桶名不能为空', "warning")
+            return
+        }
+
+        bucket_create(conn_active!, name, public_read, public_read_write);
+        bucket_get(conn_active!, true)
+        props.openFn(false)
+    }
+
+    return <>
+        <DialogSurface>
+            <DialogBody>
+                <DialogTitle>在 {conn_active?.name} 新建桶</DialogTitle>
+                <DialogContent>
+                    <div className={styles.content}>
+                        <Field className={styles.input} label="桶名">
+                            <Input
+                                value={name}
+                                onChange={(e) => {
+                                    set_name(e.target.value)
+                                }}></Input>
+                        </Field>
+                        <div className={styles.checks}>
+                            <Field>
+                                <Checkbox
+                                    checked={public_read}
+                                    onChange={(e) => {
+                                        if (public_read_write) {
+                                            return
+                                        }
+
+                                        set_public_read(e.target.checked);
+                                    }}
+                                    label={"公共读"}></Checkbox>
+                            </Field>
+                            <Field>
+                                <Checkbox
+                                    checked={public_read_write}
+                                    onChange={(e) => {
+                                        set_public_read_write(e.target.checked)
+                                        if (e.target.checked) {
+                                            set_public_read(e.target.checked)
+                                        }
+                                    }}
+                                    label={"公共读/写"}></Checkbox>
+                            </Field>
+                        </div>
+                    </div>
+                </DialogContent>
+                <DialogActions className={styles.container}>
+                    <DialogTrigger disableButtonEnhancement>
+                        <Button appearance="secondary">取消</Button>
+                    </DialogTrigger>
+                    <Button onClick={async () => {
+                        await create()
+                    }} appearance="primary">新建</Button>
+                </DialogActions>
+            </DialogBody>
+        </DialogSurface>
+    </>
+}
diff --git a/frontend/src/component/connection/list.tsx b/frontend/src/component/connection/list.tsx
index 3a3aaa9..ac147da 100644
--- a/frontend/src/component/connection/list.tsx
+++ b/frontend/src/component/connection/list.tsx
@@ -2,16 +2,16 @@ import {
     Button,
     Input,
     makeStyles,
-    Menu,
+
     MenuItem,
-    MenuList, MenuPopover, MenuProps,
-    mergeClasses, PositioningImperativeRef,
+    MenuList,
+    mergeClasses,
     tokens,
     Tooltip
 } from "@fluentui/react-components"
 import {DatabaseLinkRegular, DismissRegular} from "@fluentui/react-icons";
-import React, {useState} from "react";
-import {Bucket, Connection} from "../../interfaces/connection";
+import React, { useEffect,  useState} from "react";
+import { Connection} from "../../interfaces/connection";
 import {useToast} from "../../message";
 import {Dial} from "../../api";
 import {useStoreConnection} from "../../store/connection";
@@ -40,6 +40,15 @@ const useStyles = makeStyles({
         marginLeft: "0.5rem",
         marginRight: "0.5rem",
     },
+    ctx_menu: {
+        position: "absolute",
+        zIndex: "1000",
+        width: "15rem",
+        backgroundColor: tokens.colorNeutralBackground1,
+        boxShadow: `${tokens.shadow16}`,
+        paddingTop: "4px",
+        paddingBottom: "4px",
+    },
     items: {
         height: "100%",
         width: "100%",
@@ -78,6 +87,20 @@ export function ConnectionList() {
     const {conn_list, conn_update} = useStoreConnection();
     const [conn_filter, set_conn_filter] = useState<string>('');
     const {bucket_get, bucket_set} = useStoreBucket()
+    const [ctx_menu, set_ctx_menu] = useState<{
+        x: number,
+        y: number,
+        display: 'none' | 'block'
+    }>({x: 0, y: 0, display: 'none'});
+
+    useEffect(() => {
+        document.addEventListener("click", (e) => {
+            set_ctx_menu({x: 0, y: 0, display: 'none'});
+        })
+        return () => {
+            document.removeEventListener("click", (e) => {})
+        }
+    }, [])
 
     async function handleSelect(item: Connection) {
         conn_list.map((one: Connection) => {
@@ -114,6 +137,11 @@ export function ConnectionList() {
         e.preventDefault()
         console.log('[DEBUG] right click connection =', item, 'event =', e)
         console.log(`[DEBUG] click position: [${e.pageX}, ${e.pageY}]`)
+        set_ctx_menu({
+            x: e.pageX,
+            y: e.pageY,
+            display: 'block',
+        })
     }
 
     return (
@@ -132,6 +160,15 @@ export function ConnectionList() {
                         onChange={(e) => set_conn_filter(e.target.value)}
                     />
                 </div>
+                <div className={styles.ctx_menu}
+                     style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
+                >
+                    <MenuList>
+                        <MenuItem>连接</MenuItem>
+                        <MenuItem>设置</MenuItem>
+                        <MenuItem>删除</MenuItem>
+                    </MenuList>
+                </div>
                 <div className={styles.items}>
                     <MenuList>
                         {conn_list.filter(item => item.name.includes(conn_filter)).map(item => {
diff --git a/frontend/src/component/connection/new.tsx b/frontend/src/component/connection/new.tsx
index 0efddfa..5e2e65f 100644
--- a/frontend/src/component/connection/new.tsx
+++ b/frontend/src/component/connection/new.tsx
@@ -5,7 +5,7 @@ import {
     DialogBody,
     DialogActions,
     DialogContent,
-    Button, Spinner, Field, Input, FieldProps, makeStyles, tokens,
+    Button, Spinner, Field, Input,  makeStyles, tokens,
 } from "@fluentui/react-components";
 import {useState} from "react";
 import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
@@ -84,8 +84,6 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
                             <div className='connection-form-field'>
                                 <Field
                                     label="name"
-                                    validationState="success"
-                                    validationMessage="This is a success message."
                                 >
                                     <Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
                                            onChange={(e) => {
@@ -96,11 +94,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
                             <div className='connection-form-field'>
                                 <Field
                                     label="endpoint"
-                                    validationState="success"
-                                    validationMessage="This is a success message."
+                                    required
                                 >
                                     <Input placeholder='地址 (example: https://ip_or_server-name:port)'
                                            value={value.endpoint}
+                                           required
                                            onChange={(e) => {
                                                setValue({...value, endpoint: e.target.value});
                                            }}/>
@@ -109,10 +107,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
                             <div className='connection-form-field'>
                                 <Field
                                     label="secret access"
-                                    validationState="success"
-                                    validationMessage="This is a success message."
+                                    required
                                 >
-                                    <Input placeholder='' value={value.access} onChange={(e) => {
+                                    <Input placeholder=''
+                                           required
+                                           value={value.access} onChange={(e) => {
                                         setValue({...value, access: e.target.value});
                                     }}/>
                                 </Field>
@@ -120,10 +119,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
                             <div className='connection-form-field'>
                                 <Field
                                     label="secret key"
-                                    validationState="success"
-                                    validationMessage="This is a success message."
+                                    required
                                 >
-                                    <Input placeholder='' value={value.key} onChange={(e) => {
+                                    <Input placeholder=''
+                                           required
+                                           value={value.key} onChange={(e) => {
                                         setValue({...value, key: e.target.value});
                                     }}/>
                                 </Field>
diff --git a/frontend/src/component/file/content.tsx b/frontend/src/component/file/content.tsx
index a1333c9..d4982df 100644
--- a/frontend/src/component/file/content.tsx
+++ b/frontend/src/component/file/content.tsx
@@ -1,8 +1,7 @@
 import {Path} from "./path";
-import {ListBucketComponent} from "./list_bucket";
+import {ListBucketComponent} from "../bucket/list_bucket";
 import {makeStyles} from "@fluentui/react-components";
 import {useStoreBucket} from "../../store/bucket";
-import {useStoreFile} from "../../store/file";
 import {ListFileComponent} from "./list_file";
 
 const useStyles = makeStyles({
@@ -18,9 +17,7 @@ const useStyles = makeStyles({
 export function Content() {
 
     const styles = useStyles()
-    const {bucket_active, bucket_list} = useStoreBucket()
-    const {file_list} = useStoreFile()
-
+    const {bucket_active } = useStoreBucket()
     return <div className={styles.content}>
         <Path/>
         {
diff --git a/frontend/src/component/file/list_file.tsx b/frontend/src/component/file/list_file.tsx
index 7811862..02b86fd 100644
--- a/frontend/src/component/file/list_file.tsx
+++ b/frontend/src/component/file/list_file.tsx
@@ -1,9 +1,15 @@
 import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
-import {ArchiveRegular, DocumentBulletListRegular, DocumentDismissRegular, FolderRegular} from "@fluentui/react-icons";
+import {
+
+    DocumentBulletListRegular, DocumentChevronDoubleRegular, DocumentCssRegular, DocumentDatabaseRegular,
+    DocumentDismissRegular,
+    DocumentImageRegular, DocumentJavascriptRegular, DocumentPdfRegular, DocumentYmlRegular,
+    FolderRegular
+} from "@fluentui/react-icons";
 import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
-import React, {useEffect} from "react";
+import React from "react";
 import {useStoreBucket} from "../../store/bucket";
-import {Bucket, S3File} from "../../interfaces/connection";
+import { S3File} from "../../interfaces/connection";
 import {useStoreFile} from "../../store/file";
 import {useStoreConnection} from "../../store/connection";
 import {TrimSuffix} from "../../hook/strings";
@@ -85,7 +91,8 @@ export function ListFileComponent() {
                             handleRightClick(e, files_list[idx])
                         }}>
                         <MenuItem className={styles.item}
-                                  icon={files_list[idx].type ? <FolderRegular/> : <DocumentBulletListRegular/>}>
+                                  icon={files_list[idx].type ? <FolderRegular/> :
+                                      <FileIcon name={files_list[idx].name}/>}>
                             <Text truncate wrap={false} className={styles.text}>
                                 {filename(files_list[idx].key)}
                             </Text>
@@ -94,7 +101,7 @@ export function ListFileComponent() {
                 }}
             </VirtualizerScrollView> : <div className={styles.no_data}>
                 <div>
-                    <DocumentDismissRegular />
+                    <DocumentDismissRegular/>
                 </div>
                 <Text size={900}>
                     没有文件
@@ -102,4 +109,47 @@ export function ListFileComponent() {
             </div>
         }
     </MenuList>
+}
+
+type FileIconProps = {
+    name: string
+}
+
+function FileIcon(props: FileIconProps) {
+    const strings = props.name.split(".")
+    const suffix = strings[strings.length - 1]
+    switch (suffix) {
+        case "png":
+            return <DocumentImageRegular/>
+        case "jpg":
+            return <DocumentImageRegular/>
+        case "jpeg":
+            return <DocumentImageRegular/>
+        case "gif":
+            return <DocumentImageRegular/>
+        case "db":
+            return <DocumentDatabaseRegular/>
+        case "sqlite":
+            return <DocumentDatabaseRegular/>
+        case "sqlite3":
+            return <DocumentDatabaseRegular/>
+        case "pdf":
+            return <DocumentPdfRegular/>
+        case "css":
+            return <DocumentCssRegular />
+        case "js":
+            return <DocumentJavascriptRegular/>
+        case "yaml":
+            return <DocumentYmlRegular/>
+        case "yml":
+            return <DocumentYmlRegular/>
+        case "html":
+            return <DocumentChevronDoubleRegular/>
+        case "json":
+            return <DocumentChevronDoubleRegular/>
+        case "go":
+            return <DocumentChevronDoubleRegular/>
+        default:
+            return <DocumentBulletListRegular/>
+    }
 }
\ No newline at end of file
diff --git a/frontend/src/component/file/path.tsx b/frontend/src/component/file/path.tsx
index 4c03c53..5f5d755 100644
--- a/frontend/src/component/file/path.tsx
+++ b/frontend/src/component/file/path.tsx
@@ -54,11 +54,19 @@ const useStyles = makeStyles({
 export function Path() {
     const styles = useStyles()
     const {conn_active} = useStoreConnection()
-    const {bucket_active} = useStoreBucket()
+    const {bucket_active, bucket_get, bucket_set} = useStoreBucket()
     const {prefix, files_get} = useStoreFile()
 
     async function handleClickUp() {
+        const dirs = prefix.split('/').filter((item => item))
+        if (dirs.length > 0) {
+            dirs.pop()
+            files_get(conn_active!, bucket_active!, dirs.join("/"))
+            return
+        }
 
+        bucket_get(conn_active!, false)
+        bucket_set(null)
     }
 
 
diff --git a/frontend/src/component/file/upload_files.tsx b/frontend/src/component/file/upload_files.tsx
new file mode 100644
index 0000000..a8ebbf6
--- /dev/null
+++ b/frontend/src/component/file/upload_files.tsx
@@ -0,0 +1,123 @@
+import {
+    DialogTrigger,
+    DialogSurface,
+    DialogTitle,
+    DialogBody,
+    DialogActions,
+    DialogContent,
+    Button, Field, Input, makeStyles, tokens, Tooltip,
+} from "@fluentui/react-components";
+import {useState} from "react";
+import {useToast} from "../../message";
+import {Dial} from "../../api";
+import {useStoreConnection} from "../../store/connection";
+import {useStoreBucket} from "../../store/bucket";
+import {useStoreFile} from "../../store/file";
+import {MoreHorizontalRegular} from "@fluentui/react-icons";
+
+const useStyle = makeStyles({
+    container: {
+        backgroundColor: tokens.colorNeutralBackground1,
+        display: "flex",
+        flexDirection: "row",
+        height: "100%",
+        width: "100%",
+        gridColumnStart: 0,
+    },
+    input: {
+        cursor: "pointer",
+        display: 'flex',
+    },
+    select: {
+        minWidth: 'unset',
+    },
+});
+
+export interface UploadFilesProps {
+    openFn: (open: boolean) => void;
+}
+
+export function UploadFiles(props: UploadFilesProps) {
+    const styles = useStyle();
+    const {dispatchMessage} = useToast();
+
+    const { conn_active} = useStoreConnection();
+    const {bucket_active} = useStoreBucket();
+    const {prefix, files_get} = useStoreFile()
+
+    const [selected, set_selected] = useState<string[]>([]);
+
+    async function handleSelect() {
+        const res = await Dial<{ result: string[] }>('/runtime/dialog/open', {
+            title: '选择文件',
+            type: 'multi'
+        })
+
+        if (res.status !== 200) {
+            return
+        }
+
+        set_selected(res.data.result)
+    }
+
+    async function create() {
+        let ok = true
+        for (const item of selected) {
+            const res = await Dial('/api/file/upload', {
+                conn_id: conn_active?.id,
+                bucket: bucket_active?.name,
+                location: item,
+                detect_content_type: true,
+            })
+
+            if (res.status !== 200) {
+                dispatchMessage(`上传文件: ${item} 失败`, "error")
+                ok = false
+                return
+            }
+        }
+
+        if(ok) {
+            files_get(conn_active!, bucket_active!, prefix)
+            dispatchMessage('上传成功!', 'success')
+            props.openFn(false)
+            return
+        }
+    }
+
+    return <>
+        <DialogSurface>
+            <DialogBody>
+                <DialogTitle>上传文件到 {`${bucket_active?.name} / ${prefix}`}</DialogTitle>
+                <DialogContent>
+                    <Field label="选择文件" required>
+                        <Input
+                            className={styles.input}
+                            value={selected?.join('; ')}
+                            contentAfter={
+                                <Tooltip content={'点击选择文件'} relationship={'description'}>
+                                    <Button
+                                        className={styles.select}
+                                        onClick={async () => {
+                                            await handleSelect()
+                                        }}
+                                        size={'small'}
+                                        appearance={'transparent'}>
+                                        <MoreHorizontalRegular/>
+                                    </Button>
+                                </Tooltip>
+                            }/>
+                    </Field>
+                </DialogContent>
+                <DialogActions className={styles.container}>
+                    <DialogTrigger disableButtonEnhancement>
+                        <Button appearance="secondary">取消</Button>
+                    </DialogTrigger>
+                    <Button onClick={async () => {
+                        await create()
+                    }} appearance="primary">上传</Button>
+                </DialogActions>
+            </DialogBody>
+        </DialogSurface>
+    </>
+}
diff --git a/frontend/src/component/home/header.tsx b/frontend/src/component/home/header.tsx
index fb41af7..91fcbdf 100644
--- a/frontend/src/component/home/header.tsx
+++ b/frontend/src/component/home/header.tsx
@@ -1,37 +1,76 @@
-import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components";
+import {Button, Dialog,  DialogTrigger, makeStyles} from "@fluentui/react-components";
 import {ConnectionCreate} from "../connection/new";
-import {CloudAddFilled} from "@fluentui/react-icons";
+import {AppsAddInRegular,  DocumentArrowUpRegular, PlugConnectedAddRegular} from "@fluentui/react-icons";
 import {useState} from "react";
+import {useStoreConnection} from "../../store/connection";
+import {BucketCreate} from "../bucket/new";
+import {useStoreBucket} from "../../store/bucket";
+import {UploadFiles} from "../file/upload_files";
 
 const useStyles = makeStyles({
     header: {
-       height: "5rem",
+        height: "5rem",
         width: "100%",
         display: 'flex',
         alignItems: "center",
         borderBottom: "1px solid lightgray",
     },
-    button_new_connection: {
-     margin: '0.5rem',
+    button_new: {
+        margin: '0.5rem',
     },
 })
 
 export function Header() {
     const styles = useStyles();
-    const [openCreate, setOpenCreate] = useState(false);
+    const {conn_active} = useStoreConnection()
+    const {bucket_active} = useStoreBucket()
+    const [open_create_conn, set_open_create_conn] = useState(false);
+    const [open_create_bucket, set_open_create_bucket] = useState(false);
+    const [open_upload, set_open_upload] = useState(false);
 
     return <div className={styles.header}>
-        <div className={styles.button_new_connection}>
+        <div className={styles.button_new}>
             <Dialog
-                open={openCreate}
-                onOpenChange={(event, data) => setOpenCreate(data.open)}>
+                open={open_create_conn}
+                onOpenChange={(event, data) => set_open_create_conn(data.open)}>
                 <DialogTrigger disableButtonEnhancement>
-                    <Button appearance="primary" icon={<CloudAddFilled/>}>
+                    <Button appearance="primary" icon={<PlugConnectedAddRegular/>}>
                         新建连接
                     </Button>
                 </DialogTrigger>
-                <ConnectionCreate openFn={setOpenCreate}/>
+                <ConnectionCreate openFn={set_open_create_conn}/>
             </Dialog>
         </div>
+
+        {conn_active &&
+            <div className={styles.button_new}>
+                <Dialog
+                    open={open_create_bucket}
+                    onOpenChange={(event, data) => set_open_create_bucket(data.open)}>
+                    <DialogTrigger disableButtonEnhancement>
+                        <Button appearance="primary" icon={<AppsAddInRegular/>}>
+                            新建桶
+                        </Button>
+                    </DialogTrigger>
+                    <BucketCreate openFn={set_open_create_bucket}/>
+                </Dialog>
+            </div>
+        }
+
+        {
+            bucket_active &&
+            <div className={styles.button_new}>
+                <Dialog
+                    open={open_upload}
+                    onOpenChange={(event, data) => set_open_upload(data.open)}>
+                    <DialogTrigger disableButtonEnhancement>
+                        <Button appearance="primary" icon={<DocumentArrowUpRegular />}>
+                            上传
+                        </Button>
+                    </DialogTrigger>
+                    <UploadFiles openFn={set_open_upload}/>
+                </Dialog>
+            </div>
+        }
     </div>
 }
\ No newline at end of file
diff --git a/frontend/src/hook/strings.ts b/frontend/src/hook/strings.ts
index c90cdca..9227427 100644
--- a/frontend/src/hook/strings.ts
+++ b/frontend/src/hook/strings.ts
@@ -3,4 +3,8 @@ export function TrimSuffix(str: string, suffix: string) {
         return str.substring(0, str.length - suffix.length);
     }
     return str;
-}
\ No newline at end of file
+}
+
+export function GetBaseFileName(fullPath: string) {
+    return fullPath.replace(/.*[\/\\]/, '');
+}
diff --git a/frontend/src/store/bucket.tsx b/frontend/src/store/bucket.tsx
index dadd765..fd4aee3 100644
--- a/frontend/src/store/bucket.tsx
+++ b/frontend/src/store/bucket.tsx
@@ -7,6 +7,7 @@ interface StoreBucket {
     bucket_set: (Bucket: Bucket | null) => void;
     bucket_list: Bucket[];
     bucket_get: (conn: Connection, refresh: boolean) => void;
+    bucket_create: (conn: Connection, name: string, public_read: boolean, public_read_write: boolean) => void;
 }
 
 let bucket_map: { [id: number]: Bucket[] };
@@ -34,5 +35,21 @@ export const useStoreBucket = create<StoreBucket>()((set) => ({
 
             return {bucket_list: bucket_map[conn.id]};
         })
+    },
+    bucket_create: async (conn: Connection, name: string, public_read: boolean) => {
+        const res = await Dial<{ bucket: string }>('/api/bucket/create', {
+            conn_id: conn.id,
+            name: name,
+            public_read: public_read,
+            public_read_write: public_read,
+        })
+
+        if (res.status !== 200) {
+            return
+        }
+
+        set((state) => {
+            return {bucket_list: [...state.bucket_list, {name: res.data.bucket, created_at: 0}]}
+        })
     }
 }))
diff --git a/frontend/wailsjs/go/controller/App.d.ts b/frontend/wailsjs/go/controller/App.d.ts
index f04e377..dfdb059 100755
--- a/frontend/wailsjs/go/controller/App.d.ts
+++ b/frontend/wailsjs/go/controller/App.d.ts
@@ -1,7 +1,4 @@
 // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 // This file is automatically generated. DO NOT EDIT
-import {context} from '../models';
-
-export function Init(arg1:context.Context):Promise<void>;
 
 export function Invoke(arg1:string,arg2:string):Promise<string>;
diff --git a/frontend/wailsjs/go/controller/App.js b/frontend/wailsjs/go/controller/App.js
index 4358348..3bb5d47 100755
--- a/frontend/wailsjs/go/controller/App.js
+++ b/frontend/wailsjs/go/controller/App.js
@@ -2,10 +2,6 @@
 // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 // This file is automatically generated. DO NOT EDIT
 
-export function Init(arg1) {
-  return window['go']['controller']['App']['Init'](arg1);
-}
-
 export function Invoke(arg1, arg2) {
   return window['go']['controller']['App']['Invoke'](arg1, arg2);
 }
diff --git a/go.mod b/go.mod
index d1d6327..f70db8e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/loveuer/nf-disk
 
-go 1.21
+go 1.22
 
 toolchain go1.23.0
 
diff --git a/internal/api/api.go b/internal/api/api.go
index cf42813..dcdce5e 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -24,6 +24,7 @@ func Resolve(path string) (ndh.Handler, bool) {
 }
 
 func Init(ctx context.Context) error {
+	register("/runtime/dialog/open", handler.DialogOpen(ctx))
 	register("/api/connection/test", handler.ConnectionTest)
 	register("/api/connection/create", handler.ConnectionCreate)
 	register("/api/connection/list", handler.ConnectionList)
@@ -31,6 +32,8 @@ func Init(ctx context.Context) error {
 	register("/api/connection/disconnect", handler.ConnectionDisconnect)
 	register("/api/connection/buckets", handler.ConnectionBuckets)
 	register("/api/bucket/files", handler.BucketFiles)
+	register("/api/bucket/create", handler.BucketCreate)
+	register("/api/file/upload", handler.FileUpload)
 
 	return nil
 }
diff --git a/internal/controller/app.go b/internal/controller/app.go
index 1b64325..4449e11 100644
--- a/internal/controller/app.go
+++ b/internal/controller/app.go
@@ -9,6 +9,11 @@ import (
 	"github.com/loveuer/nf-disk/internal/tool"
 	"github.com/loveuer/nf-disk/ndh"
 	"github.com/loveuer/nf/nft/log"
+	"github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+var (
+	app *App
 )
 
 type App struct {
@@ -16,23 +21,24 @@ type App struct {
 	handlers map[string]ndh.Handler
 }
 
-func NewApp() *App {
-	return &App{
+func NewApp(gctx context.Context) *App {
+	app = &App{
 		handlers: make(map[string]ndh.Handler),
 	}
+
+	go func() {
+		<-gctx.Done()
+		runtime.Quit(app.ctx)
+	}()
+
+	return app
 }
 
-func (a *App) Init(ctx context.Context) {
-	log.Info("app init!!!")
-
+func (a *App) Startup(ctx context.Context) {
+	log.Info("app startup!!!")
 	a.ctx = ctx
-
 	tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
 	tool.Must(model.Init(db.Default.Session()))
 	tool.Must(manager.Init(ctx))
 	tool.Must(api.Init(ctx))
 }
-
-func (a *App) Startup(ctx context.Context) {
-	log.Info("app startup!!!")
-}
diff --git a/internal/handler/bucket.go b/internal/handler/bucket.go
index f6d9f75..9f147e5 100644
--- a/internal/handler/bucket.go
+++ b/internal/handler/bucket.go
@@ -38,3 +38,36 @@ func BucketFiles(c *ndh.Ctx) error {
 
 	return c.Send200(map[string]any{"list": list})
 }
+
+func BucketCreate(c *ndh.Ctx) error {
+	type Req struct {
+		ConnId          uint64 `json:"conn_id"`
+		Name            string `json:"name"`
+		PublicRead      bool   `json:"public_read"`
+		PublicReadWrite bool   `json:"public_read_write"`
+	}
+
+	var (
+		err    error
+		req    = new(Req)
+		client *s3.Client
+	)
+
+	if err = c.ReqParse(req); err != nil {
+		return c.Send400(err.Error())
+	}
+
+	if req.Name == "" {
+		return c.Send400(req, "桶名不能为空")
+	}
+
+	if _, client, err = manager.Manager.Use(req.ConnId); err != nil {
+		return c.Send500(err.Error())
+	}
+
+	if err = client.CreateBucket(c.Context(), req.Name, req.PublicRead, req.PublicReadWrite); err != nil {
+		return c.Send500(err.Error())
+	}
+
+	return c.Send200(map[string]any{"bucket": req.Name})
+}
diff --git a/internal/handler/connection.go b/internal/handler/connection.go
index c2f38d6..e829219 100644
--- a/internal/handler/connection.go
+++ b/internal/handler/connection.go
@@ -2,7 +2,6 @@ package handler
 
 import (
 	"errors"
-	"fmt"
 	"github.com/loveuer/nf-disk/internal/db"
 	"github.com/loveuer/nf-disk/internal/manager"
 	"github.com/loveuer/nf-disk/internal/model"
@@ -211,18 +210,11 @@ func ConnectionBuckets(c *ndh.Ctx) error {
 		return c.Send500(err.Error())
 	}
 
+	// todo: for frontend test
 	buckets = append(buckets, &s3.ListBucketRes{
 		Name:      "这是一个非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长的名字",
 		CreatedAt: time.Now().UnixMilli(),
 	})
 
-	// todo: for frontend test
-	for i := 1; i <= 500; i++ {
-		buckets = append(buckets, &s3.ListBucketRes{
-			CreatedAt: time.Now().UnixMilli(),
-			Name:      fmt.Sprintf("test-bucket-%03d", i),
-		})
-	}
-
 	return c.Send200(map[string]any{"list": buckets})
 }
diff --git a/internal/handler/dialog.go b/internal/handler/dialog.go
new file mode 100644
index 0000000..12ad693
--- /dev/null
+++ b/internal/handler/dialog.go
@@ -0,0 +1,56 @@
+package handler
+
+import (
+	"context"
+	"github.com/loveuer/nf-disk/ndh"
+	"github.com/samber/lo"
+	"github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+func DialogOpen(ctx context.Context) ndh.Handler {
+	return func(c *ndh.Ctx) error {
+		type Req struct {
+			Title   string   `json:"title"`
+			Type    string   `json:"type"` // "dir", "multi", ""
+			Filters []string `json:"filters"`
+		}
+
+		var (
+			err error
+			req = new(Req)
+			opt = runtime.OpenDialogOptions{
+				Title: "请选择文件",
+			}
+			result any
+		)
+
+		if err = c.ReqParse(req); err != nil {
+			return c.Send400(err.Error())
+		}
+
+		if req.Title != "" {
+			opt.Title = req.Title
+		}
+
+		if len(req.Filters) > 0 {
+			opt.Filters = lo.Map(req.Filters, func(item string, index int) runtime.FileFilter {
+				return runtime.FileFilter{Pattern: item}
+			})
+		}
+
+		switch req.Type {
+		case "dir":
+			result, err = runtime.OpenDirectoryDialog(ctx, opt)
+		case "multi":
+			result, err = runtime.OpenMultipleFilesDialog(ctx, opt)
+		default:
+			result, err = runtime.OpenFileDialog(ctx, opt)
+		}
+
+		if err != nil {
+			return c.Send500(err.Error())
+		}
+
+		return c.Send200(map[string]interface{}{"result": result})
+	}
+}
diff --git a/internal/handler/file.go b/internal/handler/file.go
new file mode 100644
index 0000000..95c5d85
--- /dev/null
+++ b/internal/handler/file.go
@@ -0,0 +1,88 @@
+package handler
+
+import (
+	"errors"
+	"fmt"
+	"github.com/loveuer/nf-disk/internal/manager"
+	"github.com/loveuer/nf-disk/internal/s3"
+	"github.com/loveuer/nf-disk/ndh"
+	"github.com/loveuer/nf/nft/log"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+)
+
+func FileUpload(c *ndh.Ctx) error {
+	type Req struct {
+		ConnId            uint64 `json:"conn_id"`
+		Bucket            string `json:"bucket"`
+		Location          string `json:"location"`
+		Name              string `json:"name"`
+		PublicRead        bool   `json:"public_read"`
+		PublicReadWrite   bool   `json:"public_read_write"`
+		DetectContentType bool   `json:"detect_content_type"`
+	}
+
+	var (
+		err    error
+		req    = new(Req)
+		client *s3.Client
+		reader *os.File
+		info   os.FileInfo
+	)
+
+	if err = c.ReqParse(req); err != nil {
+		return c.Send400(c, err.Error())
+	}
+
+	if req.Location == "" {
+		return c.Send400(req, "缺少文件位置")
+	}
+
+	if req.Name == "" {
+		req.Name = filepath.Base(req.Location)
+	}
+
+	if _, client, err = manager.Manager.Use(req.ConnId); err != nil {
+		return c.Send500(err.Error())
+	}
+
+	if reader, err = os.Open(req.Location); err != nil {
+		return c.Send400(err.Error(), fmt.Sprintf("文件: %s 打开错误", req.Location))
+	}
+
+	if info, err = reader.Stat(); err != nil {
+		log.Error("FileUpload: stat file info err = %s", err.Error())
+		return c.Send500(err.Error())
+	}
+
+	obj := &s3.PutFilesObj{
+		Key:             req.Name,
+		Reader:          reader,
+		ContentLength:   info.Size(),
+		ContentType:     "",
+		ExpireAt:        0,
+		PublicRead:      req.PublicRead,
+		PublicReadWrite: req.PublicReadWrite,
+	}
+
+	if req.DetectContentType {
+		bs := make([]byte, 128)
+		if _, err = reader.ReadAt(bs, 0); err != nil {
+			if !errors.Is(err, io.EOF) {
+				log.Error("FileUpload: read file to detect content_type err = %s", err.Error())
+				return c.Send500(err.Error())
+			}
+		}
+
+		obj.ContentType = http.DetectContentType(bs)
+	}
+
+	if err = client.PutFile(c.Context(), req.Bucket, obj); err != nil {
+		log.Error("FileUpload: client.PutFile err = %s", err.Error())
+		return c.Send500(err.Error())
+	}
+
+	return c.Send200(req)
+}
diff --git a/internal/handler/item.go b/internal/handler/item.go
deleted file mode 100644
index 574dd3e..0000000
--- a/internal/handler/item.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package handler
-
-import "github.com/loveuer/nf-disk/ndh"
-
-func ListItem(c *ndh.Ctx) error {
-	type Req struct {
-		Id     uint64 `json:"id"`
-		Bucket string `json:"bucket"`
-	}
-
-	var (
-		err error
-		req = new(Req)
-	)
-
-	if err = c.ReqParse(req); err != nil {
-		return c.Send400(err.Error())
-	}
-
-	panic("implement me!!!")
-}
diff --git a/internal/model/res.go b/internal/model/res.go
new file mode 100644
index 0000000..367d28d
--- /dev/null
+++ b/internal/model/res.go
@@ -0,0 +1,24 @@
+package model
+
+import "encoding/json"
+
+type Res struct {
+	Status uint32 `json:"status"`
+	Err    string `json:"err"`
+	Msg    string `json:"msg"`
+	Data   any    `json:"data"`
+}
+
+func NewRes(status uint32, err string, msg string, data any) *Res {
+	return &Res{
+		Status: status,
+		Err:    err,
+		Msg:    msg,
+		Data:   data,
+	}
+}
+
+func (r *Res) String() string {
+	bs, _ := json.Marshal(r)
+	return string(bs)
+}
diff --git a/internal/s3/create.go b/internal/s3/create.go
new file mode 100644
index 0000000..74ae71d
--- /dev/null
+++ b/internal/s3/create.go
@@ -0,0 +1,36 @@
+package s3
+
+import (
+	"context"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/s3"
+	"github.com/aws/aws-sdk-go-v2/service/s3/types"
+)
+
+func (c *Client) CreateBucket(ctx context.Context, bucket string, publicRead bool, publicReadWrite bool) error {
+	var (
+		err   error
+		input = &s3.CreateBucketInput{
+			Bucket: aws.String(bucket),
+			ACL:    types.BucketCannedACLAuthenticatedRead,
+		}
+
+		output = &s3.CreateBucketOutput{}
+	)
+
+	if publicRead {
+		input.ACL = types.BucketCannedACLPublicRead
+	}
+
+	if publicReadWrite {
+		input.ACL = types.BucketCannedACLPublicReadWrite
+	}
+
+	if output, err = c.client.CreateBucket(ctx, input); err != nil {
+		return err
+	}
+
+	_ = output
+
+	return nil
+}
diff --git a/internal/s3/put.go b/internal/s3/put.go
new file mode 100644
index 0000000..587cc57
--- /dev/null
+++ b/internal/s3/put.go
@@ -0,0 +1,62 @@
+package s3
+
+import (
+	"context"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/s3"
+	"github.com/aws/aws-sdk-go-v2/service/s3/types"
+	"io"
+	"time"
+)
+
+type PutFilesObj struct {
+	Key             string
+	Reader          io.ReadSeeker
+	ContentLength   int64
+	ContentType     string
+	ExpireAt        int64
+	PublicRead      bool
+	PublicReadWrite bool
+}
+
+func (c *Client) PutFile(ctx context.Context, bucket string, obj *PutFilesObj) error {
+	var (
+		err   error
+		input = &s3.PutObjectInput{
+			Bucket: aws.String(bucket),
+			Key:    aws.String(obj.Key),
+			Body:   obj.Reader,
+			ACL:    types.ObjectCannedACLPrivate,
+		}
+		output *s3.PutObjectOutput
+	)
+
+	if obj.ExpireAt > 0 {
+		t := time.UnixMilli(obj.ExpireAt)
+		input.Expires = &t
+	}
+
+	if obj.ContentLength > 0 {
+		input.ContentLength = aws.Int64(obj.ContentLength)
+	}
+
+	if obj.ContentType == "" {
+		input.ContentType = aws.String(obj.ContentType)
+	}
+
+	if obj.PublicRead {
+		input.ACL = types.ObjectCannedACLPublicRead
+	}
+
+	if obj.PublicReadWrite {
+		input.ACL = types.ObjectCannedACLPublicReadWrite
+	}
+
+	if output, err = c.client.PutObject(ctx, input); err != nil {
+		return err
+	}
+
+	_ = output
+
+	return nil
+}
diff --git a/internal/tool/slice.go b/internal/tool/slice.go
new file mode 100644
index 0000000..bcf8e09
--- /dev/null
+++ b/internal/tool/slice.go
@@ -0,0 +1,32 @@
+package tool
+
+import "iter"
+
+func Bulk[T any](slice []T, size int) iter.Seq2[int, []T] {
+	if size <= 0 {
+		panic("bulk size must be positive")
+	}
+
+	s := make([]T, 0, size)
+	idx := 0
+	return func(yield func(int, []T) bool) {
+		for i := range slice {
+			s = append(s, (slice)[i])
+			if len(s) >= size {
+
+				// send to handle
+				ok := yield(idx, s)
+				if !ok {
+					return
+				}
+
+				idx++
+				s = make([]T, 0, size)
+			}
+		}
+
+		if len(s) > 0 {
+			yield(idx, s)
+		}
+	}
+}
diff --git a/main.go b/main.go
index 843d69c..b9037b4 100644
--- a/main.go
+++ b/main.go
@@ -18,9 +18,6 @@ import (
 //go:embed all:frontend/dist
 var assets embed.FS
 
-func init() {
-}
-
 func main() {
 	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
 	defer cancel()
@@ -32,9 +29,7 @@ func main() {
 		log.SetLogLevel(log.LogLevelDebug)
 	}
 
-	app := controller.NewApp()
-
-	app.Init(ctx)
+	app := controller.NewApp(ctx)
 
 	if err := wails.Run(&options.App{
 		Title:  "nf-disk",
diff --git a/xtest/path.js b/xtest/path.js
new file mode 100644
index 0000000..fe7392f
--- /dev/null
+++ b/xtest/path.js
@@ -0,0 +1,10 @@
+function getBaseFileName(fullPath) {
+    return fullPath.replace(/.*[\/\\]/, '');
+}
+
+// 测试
+const filePath = 'C:\\Users\\username\\Documents\\example.txt';
+console.log(getBaseFileName(filePath));  // 输出: example.txt
+
+const filePath2 = '/home/user/documents/example.txt';
+console.log(getBaseFileName(filePath2));  // 输出: example.txt
\ No newline at end of file