From 777253063bab94503ba28edb12fbf24ae61af6a1 Mon Sep 17 00:00:00 2001 From: zhaoyupeng Date: Sat, 12 Oct 2024 17:35:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E4=BA=86=20=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E6=A1=B6;=20=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6(?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD)=20todo:=20=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=20rename,=20=E4=B8=8A=E4=BC=A0=20public=20=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=80=89=E6=8B=A9=20bug:=20=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=20conns=20list;=20=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E5=80=99=E5=89=8D=E7=BC=80=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json.md5 | 2 +- .../{file => bucket}/list_bucket.tsx | 4 +- frontend/src/component/bucket/new.tsx | 111 ++++++++++++++++ frontend/src/component/connection/list.tsx | 47 ++++++- frontend/src/component/connection/new.tsx | 22 ++-- frontend/src/component/file/content.tsx | 7 +- frontend/src/component/file/list_file.tsx | 60 ++++++++- frontend/src/component/file/path.tsx | 10 +- frontend/src/component/file/upload_files.tsx | 123 ++++++++++++++++++ frontend/src/component/home/header.tsx | 61 +++++++-- frontend/src/hook/strings.ts | 6 +- frontend/src/store/bucket.tsx | 17 +++ frontend/wailsjs/go/controller/App.d.ts | 3 - frontend/wailsjs/go/controller/App.js | 4 - go.mod | 2 +- internal/api/api.go | 3 + internal/controller/app.go | 26 ++-- internal/handler/bucket.go | 33 +++++ internal/handler/connection.go | 10 +- internal/handler/dialog.go | 56 ++++++++ internal/handler/file.go | 88 +++++++++++++ internal/handler/item.go | 21 --- internal/model/res.go | 24 ++++ internal/s3/create.go | 36 +++++ internal/s3/put.go | 62 +++++++++ internal/tool/slice.go | 32 +++++ main.go | 7 +- xtest/path.js | 10 ++ 28 files changed, 791 insertions(+), 96 deletions(-) rename frontend/src/component/{file => bucket}/list_bucket.tsx (95%) create mode 100644 frontend/src/component/bucket/new.tsx create mode 100644 frontend/src/component/file/upload_files.tsx create mode 100644 internal/handler/dialog.go create mode 100644 internal/handler/file.go delete mode 100644 internal/handler/item.go create mode 100644 internal/model/res.go create mode 100644 internal/s3/create.go create mode 100644 internal/s3/put.go create mode 100644 internal/tool/slice.go create mode 100644 xtest/path.js 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 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(); + 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 <> + + + 在 {conn_active?.name} 新建桶 + +
+ + { + set_name(e.target.value) + }}> + +
+ + { + if (public_read_write) { + return + } + + set_public_read(e.target.checked); + }} + label={"公共读"}> + + + { + set_public_read_write(e.target.checked) + if (e.target.checked) { + set_public_read(e.target.checked) + } + }} + label={"公共读/写"}> + +
+
+
+ + + + + + +
+
+ +} 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(''); 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)} /> +
+ + 连接 + 设置 + 删除 + +
{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) {
{ @@ -96,11 +94,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
{ setValue({...value, endpoint: e.target.value}); }}/> @@ -109,10 +107,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
- { + { setValue({...value, access: e.target.value}); }}/> @@ -120,10 +119,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
- { + { setValue({...value, key: e.target.value}); }}/> 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
{ 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]) }}> : }> + icon={files_list[idx].type ? : + }> {filename(files_list[idx].key)} @@ -94,7 +101,7 @@ export function ListFileComponent() { }} :
- +
没有文件 @@ -102,4 +109,47 @@ export function ListFileComponent() {
} +} + +type FileIconProps = { + name: string +} + +function FileIcon(props: FileIconProps) { + const strings = props.name.split(".") + const suffix = strings[strings.length - 1] + switch (suffix) { + case "png": + return + case "jpg": + return + case "jpeg": + return + case "gif": + return + case "db": + return + case "sqlite": + return + case "sqlite3": + return + case "pdf": + return + case "css": + return + case "js": + return + case "yaml": + return + case "yml": + return + case "html": + return + case "json": + return + case "go": + return + default: + return + } } \ 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([]); + + 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 <> + + + 上传文件到 {`${bucket_active?.name} / ${prefix}`} + + + + + + }/> + + + + + + + + + + + +} 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
-
+
setOpenCreate(data.open)}> + open={open_create_conn} + onOpenChange={(event, data) => set_open_create_conn(data.open)}> - - +
+ + {conn_active && +
+ set_open_create_bucket(data.open)}> + + + + + +
+ } + + { + bucket_active && +
+ set_open_upload(data.open)}> + + + + + +
+ }
} \ 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()((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; export function Invoke(arg1:string,arg2:string):Promise; 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