diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5597f81..39274e0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,9 +10,11 @@ "dependencies": { "@fluentui/react-components": "^9.54.16", "@fluentui/react-icons": "^2.0.258", + "jotai": "^2.10.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.26.2" + "react-router-dom": "^6.26.2", + "zustand": "^5.0.0-rc.2" }, "devDependencies": { "@types/react": "^18.0.17", @@ -3002,6 +3004,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/jotai": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/jotai/-/jotai-2.10.0.tgz", + "integrity": "sha512-8W4u0aRlOIwGlLQ0sqfl/c6+eExl5D8lZgAUolirZLktyaj4WnxO/8a0HEPmtriQAB6X5LMhXzZVmw02X0P0qQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3502,6 +3525,35 @@ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "node_modules/zustand": { + "version": "5.0.0-rc.2", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.0-rc.2.tgz", + "integrity": "sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } }, "dependencies": { @@ -5549,6 +5601,12 @@ "hasown": "^2.0.2" } }, + "jotai": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/jotai/-/jotai-2.10.0.tgz", + "integrity": "sha512-8W4u0aRlOIwGlLQ0sqfl/c6+eExl5D8lZgAUolirZLktyaj4WnxO/8a0HEPmtriQAB6X5LMhXzZVmw02X0P0qQ==", + "requires": {} + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5861,6 +5919,12 @@ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "zustand": { + "version": "5.0.0-rc.2", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.0-rc.2.tgz", + "integrity": "sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==", + "requires": {} } } } diff --git a/frontend/package.json b/frontend/package.json index 3c5c3b2..89e4872 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,11 @@ "dependencies": { "@fluentui/react-components": "^9.54.16", "@fluentui/react-icons": "^2.0.258", + "jotai": "^2.10.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.26.2" + "react-router-dom": "^6.26.2", + "zustand": "^5.0.0-rc.2" }, "devDependencies": { "@types/react": "^18.0.17", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 13ddb8b..ad24e7d 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -b35fc08c84ef0c2b0c3e1bf37916ac94 \ No newline at end of file +f23304e575da740e9b738508a43df31e \ No newline at end of file diff --git a/frontend/src/api.tsx b/frontend/src/api.tsx index a0d9f97..933004f 100644 --- a/frontend/src/api.tsx +++ b/frontend/src/api.tsx @@ -19,7 +19,7 @@ function isResp(obj: any): obj is Resp { ); } -export async function Dial(path: string, req: any = null): Promise> { +export async function Dial(path: string, req: any = null): Promise> { const bs = JSON.stringify(req) console.log(`[DEBUG] invoke req: path = ${path}, req =`, req) diff --git a/frontend/src/page/connection/connection.css b/frontend/src/component/connection/connection.css similarity index 100% rename from frontend/src/page/connection/connection.css rename to frontend/src/component/connection/connection.css diff --git a/frontend/src/component/connection/new.tsx b/frontend/src/component/connection/new.tsx new file mode 100644 index 0000000..8a464be --- /dev/null +++ b/frontend/src/component/connection/new.tsx @@ -0,0 +1,136 @@ +import { + DialogTrigger, + DialogSurface, + DialogTitle, + DialogBody, + DialogActions, + DialogContent, + Button, Spinner, Field, Input, FieldProps, makeStyles, tokens, +} from "@fluentui/react-components"; +import {useState} from "react"; +import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons"; +import {useToast} from "../../message"; +import {Dial} from "../../api"; + +const useActionStyle = makeStyles({ + container: { + backgroundColor: tokens.colorNeutralBackground1, + display: "flex", + flexDirection: "row", + height: "100%", + width: "100%", + gridColumnStart: 0, + }, + test: {} +}); + +interface ConnectionCreateProps { + update: () => Promise +} + +export function ConnectionCreate(props: ConnectionCreateProps){ + const actionStyle = useActionStyle(); + const {dispatchMessage} = useToast(); + const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial"); + const buttonIcon = + testLoading === "loading" ? ( + + ) : testLoading === "success" ? ( + + ) : testLoading === "error" ? ( + + ) : null; + const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({ + name: '', + endpoint: '', + access: '', + key: '' + }) + + async function test() { + setTestLoading("loading") + let res = await Dial("/api/connection/test", value) + const status = res.status === 200 ? "success" : "error" + setTestLoading(status); + dispatchMessage(res.msg, status) + } + + async function create() { + let res = await Dial("/api/connection/create", value) + dispatchMessage(res.msg, res.status === 200 ? "success" : "error"); + if (res.status === 200) { + await props.update() + } + } + + return <> + + + 新建S3连接 + +
+
+
+ + { + setValue({...value, name: e.target.value}); + }}/> + +
+
+ + { + setValue({...value, endpoint: e.target.value}); + }}/> + +
+
+ + { + setValue({...value, access: e.target.value}); + }}/> + +
+
+ + { + setValue({...value, key: e.target.value}); + }}/> + +
+
+
+
+ + + + + + + +
+
+ +} \ No newline at end of file diff --git a/frontend/src/page/home/home.css b/frontend/src/component/home/home.css similarity index 78% rename from frontend/src/page/home/home.css rename to frontend/src/component/home/home.css index 90cbe1a..d8e14be 100644 --- a/frontend/src/page/home/home.css +++ b/frontend/src/component/home/home.css @@ -17,11 +17,13 @@ div.body { display: flex; flex: 1; width: 100%; + height: 100%; } div.body div.body-connections { width: 200px; border-right: 1px solid lightgray; + height: 100%; } div.body-connections-search { @@ -37,7 +39,8 @@ input.body-connections-search-input { outline: none; text-indent: 5px; } -div.body-connections-search-dismiss{ + +div.body-connections-search-dismiss { border: none; background: none; outline: none; @@ -49,13 +52,9 @@ div.body-connections-search-dismiss{ align-items: center; } -div.body-connections-list-item { - height: 36px; - display: flex; - align-items: center; - font-size: 14px; - margin: 1px 0; +div.body-connections-list { + height: 100%; } -div.body-connections-list-item:first-child { - margin-top: 8px; +div.body-connections-list-item.active { + color: var(--colorNeutralForeground2BrandSelected); } diff --git a/frontend/src/component/home/home.tsx b/frontend/src/component/home/home.tsx new file mode 100644 index 0000000..27c5fec --- /dev/null +++ b/frontend/src/component/home/home.tsx @@ -0,0 +1,158 @@ +import {useEffect, useState} from 'react'; +import './home.css'; +import { + Button, Dialog, DialogTrigger, makeStyles,mergeClasses, MenuItem, MenuList, tokens, Tooltip, +} from "@fluentui/react-components"; +import { + CloudAddFilled, DismissRegular +} from "@fluentui/react-icons"; +import {Dial} from "../../api"; +import {useToast} from "../../message"; +import {Connection} from "../../interfaces/connection"; +import {ConnectionCreate} from "../connection/new"; + +const useMenuListContainerStyles = makeStyles({ + container: { + backgroundColor: tokens.colorNeutralBackground1, + flex: 1, + width: "100%", + paddingTop: "4px", + paddingBottom: "4px", + }, + item: { + display: 'flex', + height: '100%', + alignItems: 'center', + flexDirection: 'row', + fontSize: '15px', + '& span': { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + '&.active': { + color: tokens.colorNeutralForeground2BrandHover, + } + }, + item_icon: { + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginLeft: 'auto', + border: 'none', + background: 'transparent', + "&:hover" : { + color: tokens.colorNeutralForeground2BrandHover, + } + } +}); + + +function Home() { + const styles = useMenuListContainerStyles(); + const {dispatchMessage} = useToast(); + const [openCreate, setOpenCreate] = useState(false); + const [connectionFilterKeywords, setConnectionFilterKeywords] = useState(''); + const [connections, setConnections] = useState([]); + + useEffect(() => { + updateConnectionList().then() + }, []); + + async function updateConnectionList() { + const res = await Dial<{ list: Connection[] }>("/api/connection/list"); + dispatchMessage(res.status === 200 ? '获取连接列表成功' : res.msg, res.status === 200 ? 'success' : 'error'); + setConnections(res.status === 200 ? res.data.list : connections); + setOpenCreate(false) + return; + } + + async function handleConnect(item: Connection) { + console.log('[DEBUG] db click item =', item) + for (const c of connections) { + if (item.id === c.id && c.active) { + console.log('[DEBUG] conn is already connected:', c) + return + } + } + + let res = await Dial("/api/connection/connect", {id: item.id}) + if (res.status === 200) { + dispatchMessage("连接成功", "success") + setConnections(connections.map(one => { + if (one.id === item.id) { + one.active = true + } + + return one + })) + } + } + + async function handleDisconnect(item: Connection) { + let res = await Dial('/api/connection/disconnect', {id: item.id}) + if (res.status === 200) { + setConnections(connections.map(c => { + if (item.id === c.id) { + c.active = false + } + return c + })) + } + } + + return ( +
+
+ setOpenCreate(data.open)}> + + + + + +
+
+
+
+ setConnectionFilterKeywords(e.target.value)}/> +
{ + setConnectionFilterKeywords('') + }}> + +
+
+
+
+ + {connections.map(item => { + return { + await handleConnect(item) + }} + className={item.active ? mergeClasses(styles.item, 'active') : styles.item} + key={item.id}> + {item.name} + +
+
+
+
+
+
+
+ ) +} + +export default Home diff --git a/frontend/src/interfaces/connection.ts b/frontend/src/interfaces/connection.ts index 26c6a82..95dfd02 100644 --- a/frontend/src/interfaces/connection.ts +++ b/frontend/src/interfaces/connection.ts @@ -6,4 +6,4 @@ export interface Connection { name: string; endpoint: string; active: boolean; -} \ No newline at end of file +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ff77e3c..bcdef1c 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,9 +3,9 @@ import {createRoot} from 'react-dom/client' import './style.css' import {FluentProvider, webLightTheme} from '@fluentui/react-components'; import {createBrowserRouter, RouterProvider} from "react-router-dom"; -import Home from "./page/home/home"; -import Connection from "./page/connection/connection"; +import Home from "./component/home/home"; import {ToastProvider} from "./message"; +import {JotaiProvider} from "./store/store"; const container = document.getElementById('root') @@ -13,7 +13,6 @@ const root = createRoot(container!) const router = createBrowserRouter([ {path: '/', element: }, - {path: '/connection', element: }, ]) root.render( diff --git a/frontend/src/page/connection/connection.tsx b/frontend/src/page/connection/connection.tsx deleted file mode 100644 index 36f82e0..0000000 --- a/frontend/src/page/connection/connection.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import './connection.css' -import { - useId, - Button, - FieldProps, - Spinner -} from "@fluentui/react-components"; -import {Field, Input} from "@fluentui/react-components"; -import {useNavigate} from "react-router-dom"; -import {useState} from "react"; -import {Dial} from "../../api"; -import {useToast} from "../../message"; -import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons"; - - -const Connection = (props: Partial) => { - const { dispatchMessage } = useToast(); - const navigate = useNavigate(); - const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial"); - const buttonIcon = - testLoading === "loading" ? ( - - ) : testLoading === "success" ? ( - - ) : testLoading === "error" ? ( - - ) : null; - const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({ - name: '', - endpoint: '', - access: '', - key: '' - }) - - function test() { - setTestLoading("loading") - Dial("/api/connection/test", value).then(res => { - let status: "success" | "error" = "error" - if (res.status === 200) { - status = "success" - } - - setTestLoading(status); - - dispatchMessage(res.msg, status) - }) - } - - function create() { - Dial("/api/connection/create", value).then(res => { - dispatchMessage(res.msg, res.status === 200?"success":"error"); - if (res.status === 200) { - navigate("/"); - } - }) - } - - return
-
-
- - { - setValue({...value, name: e.target.value}); - }}/> - -
-
- - { - setValue({...value, endpoint: e.target.value}); - }}/> - -
-
- - { - setValue({...value, access: e.target.value}); - }}/> - -
-
- - { - setValue({...value, key: e.target.value}); - }}/> - -
-
- -
- - -
-
-
-
-} - -export default Connection; \ No newline at end of file diff --git a/frontend/src/page/home/home.tsx b/frontend/src/page/home/home.tsx deleted file mode 100644 index 40ee18c..0000000 --- a/frontend/src/page/home/home.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import {useEffect, useState} from 'react'; -import './home.css'; -import { - Button, -} from "@fluentui/react-components"; -import { - CloudAddFilled, DismissRegular -} from "@fluentui/react-icons"; -import {useNavigate} from "react-router-dom"; -import {Dial} from "../../api"; -import {useToast} from "../../message"; -import {Connection} from "../../interfaces/connection"; - -function Home() { - const {dispatchMessage} = useToast(); - const [connectionFilterKeywords, setConnectionFilterKeywords] = useState(''); - const navigate = useNavigate(); - const [connectionList, setConnectionList] = useState([]); - - useEffect(() => { - Dial<{ list: Connection[] }>("/api/connection/list").then(res => { - dispatchMessage(res.msg, res.status === 200 ? "success" : "error"); - if (res.status === 200) { - setConnectionList(res.data.list) - } - }) - }, []); - - async function handleConnect(item: Connection) { - console.log('[DEBUG] double clicked item =', item) - let res = await Dial("/api/connection/connect", {id: item.id}) - if (res.status === 200) { - connectionList.forEach((conn) => { - if (conn.id === item.id) { - conn.active = true - } - }) - } - } - - return ( -
-
- -
-
-
-
- setConnectionFilterKeywords(e.target.value)}/> -
{ - setConnectionFilterKeywords('') - }}> - -
-
-
- {connectionList.map(item => { - return
- -
- })} -
-
-
-
-
-
- ) -} - -export default Home diff --git a/frontend/src/store/store.tsx b/frontend/src/store/store.tsx new file mode 100644 index 0000000..3f2c07c --- /dev/null +++ b/frontend/src/store/store.tsx @@ -0,0 +1,8 @@ +import { Provider, createStore } from 'jotai' +import {FC, ReactNode} from "react"; + + +export const JotaiProvider: FC<{ children: ReactNode }> = ({children}) => { + const store = createStore(); + return {children} +} diff --git a/go.mod b/go.mod index 060f8b7..d1d6327 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/loveuer/go-sqlite3 v1.0.2 github.com/loveuer/nf v0.2.11 github.com/ncruces/go-sqlite3/gormlite v0.18.4 - github.com/pkg/errors v0.9.1 github.com/psanford/httpreadat v0.1.0 + github.com/samber/lo v1.38.1 github.com/wailsapp/wails/v2 v2.9.2 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.9 @@ -59,8 +59,8 @@ require ( github.com/ncruces/go-sqlite3 v0.18.4 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/samber/lo v1.38.1 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect github.com/tkrajina/go-reflector v0.5.6 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/internal/api/api.go b/internal/api/api.go index 44fec17..ac4b396 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -28,6 +28,7 @@ func Init(ctx context.Context) error { register("/api/connection/create", handler.ConnectionCreate) register("/api/connection/list", handler.ConnectionList) register("/api/connection/connect", handler.ConnectionConnect) + register("/api/connection/disconnect", handler.ConnectionDisconnect) register("/api/connection/buckets", handler.ConnectionBuckets) return nil diff --git a/internal/controller/app.go b/internal/controller/app.go index ca77cad..1b64325 100644 --- a/internal/controller/app.go +++ b/internal/controller/app.go @@ -4,6 +4,7 @@ import ( "context" "github.com/loveuer/nf-disk/internal/api" "github.com/loveuer/nf-disk/internal/db" + "github.com/loveuer/nf-disk/internal/manager" "github.com/loveuer/nf-disk/internal/model" "github.com/loveuer/nf-disk/internal/tool" "github.com/loveuer/nf-disk/ndh" @@ -23,9 +24,12 @@ func NewApp() *App { func (a *App) Init(ctx context.Context) { log.Info("app init!!!") + 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)) } diff --git a/internal/handler/connection.go b/internal/handler/connection.go index 90b0001..d85403a 100644 --- a/internal/handler/connection.go +++ b/internal/handler/connection.go @@ -1,13 +1,13 @@ package handler import ( + "errors" "github.com/loveuer/nf-disk/internal/db" "github.com/loveuer/nf-disk/internal/manager" "github.com/loveuer/nf-disk/internal/model" "github.com/loveuer/nf-disk/internal/s3" "github.com/loveuer/nf-disk/ndh" - "github.com/pkg/errors" - "gorm.io/gorm" + "github.com/samber/lo" ) func ConnectionTest(c *ndh.Ctx) error { @@ -80,7 +80,7 @@ func ConnectionCreate(c *ndh.Ctx) error { return c.Send500(err.Error(), "创建连接失败(1)") } - if err = manager.Register(connection, client); err != nil { + if err = manager.Manager.Register(connection, client); err != nil { return c.Send500(err.Error(), "创建连接失败(2)") } @@ -108,6 +108,18 @@ func ConnectionList(c *ndh.Ctx) error { return err } + listMap := lo.SliceToMap(list, func(item *model.Connection) (uint64, *model.Connection) { + return item.Id, item + }) + + manager.Manager.Map(func(c *model.Connection, s *s3.Client) error { + if item, ok := listMap[c.Id]; ok { + item.Active = true + } + + return nil + }) + return c.Send200(map[string]any{"list": list}) } @@ -119,27 +131,49 @@ func ConnectionConnect(c *ndh.Ctx) error { var ( err error req = new(Req) - conn = new(model.Connection) client *s3.Client ) if err = c.ReqParse(req); err != nil { - return c.Send400(nil, "参数错误") + return c.Send400(req) } - if err = db.Default.Session().Take(conn, req.Id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return c.Send400(c, "不存在的连接") - } - - return c.Send500(nil) + conn := &model.Connection{Id: req.Id} + if err = conn.Get(db.Default.Session(), c); err != nil { + return err } if client, err = s3.New(c.Context(), conn.Endpoint, conn.Access, conn.Key); err != nil { - return c.Send500(err.Error()) + return c.Send500(err.Error(), "连接失败") } - if err = manager.Register(conn, client); err != nil { + if err = manager.Manager.Register(conn, client); err != nil { + return c.Send500(err.Error(), "连接失败") + } + + return c.Send200(conn, "连接成功") +} + +func ConnectionDisconnect(c *ndh.Ctx) error { + type Req struct { + Id uint64 `json:"id"` + } + + var ( + err error + req = new(Req) + ) + + if err = c.ReqParse(req); err != nil { + return c.Send400(req) + } + + conn := &model.Connection{Id: req.Id} + if err = conn.Get(db.Default.Session(), c); err != nil { + return err + } + + if err = manager.Manager.UnRegister(conn.Id); err != nil { return c.Send500(err.Error()) } @@ -153,13 +187,27 @@ func ConnectionBuckets(c *ndh.Ctx) error { } var ( - err error - req = new(Req) + err error + req = new(Req) + client *s3.Client + buckets []*s3.ListBucketRes ) if err = c.ReqParse(req); err != nil { return c.Send400(nil, "参数错误") } - panic("implement me: ConnectionBuckets") + if _, client, err = manager.Manager.Use(req.Id); err != nil { + if errors.Is(err, manager.ErrNotFound) { + return c.Send400(nil, "所选连接未激活") + } + + return c.Send500(err.Error()) + } + + if buckets, err = client.ListBucket(c.Context()); err != nil { + return c.Send500(err.Error()) + } + + return c.Send200(map[string]any{"list": buckets}) } diff --git a/internal/handler/item.go b/internal/handler/item.go new file mode 100644 index 0000000..574dd3e --- /dev/null +++ b/internal/handler/item.go @@ -0,0 +1,21 @@ +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/manager/error.go b/internal/manager/error.go new file mode 100644 index 0000000..3d4895a --- /dev/null +++ b/internal/manager/error.go @@ -0,0 +1,7 @@ +package manager + +import "errors" + +var ( + ErrNotFound = errors.New("not found") +) diff --git a/internal/manager/manager.go b/internal/manager/manager.go index ff17cd1..7bf9e0b 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -5,14 +5,71 @@ import ( "github.com/loveuer/nf-disk/internal/model" "github.com/loveuer/nf-disk/internal/s3" "github.com/loveuer/nf/nft/log" + "sync" +) + +type client struct { + conn *model.Connection + client *s3.Client +} + +type manager struct { + sync.Mutex + clients map[uint64]*client +} + +var ( + Manager *manager ) func Init(ctx context.Context) error { - return nil -} - -func Register(m *model.Connection, c *s3.Client) error { - log.Debug("manager: register connection-client: id = %d, name = %s", m.Id, m.Name) + Manager = &manager{ + clients: make(map[uint64]*client), + } return nil } + +func (m *manager) Register(c *model.Connection, s *s3.Client) error { + log.Debug("manager: register connection-client: id = %d, name = %s", c.Id, c.Name) + + Manager.Lock() + defer Manager.Unlock() + Manager.clients[c.Id] = &client{conn: c, client: s} + + return nil +} + +func (m *manager) UnRegister(id uint64) error { + Manager.Lock() + defer Manager.Unlock() + c, ok := m.clients[id] + if !ok { + return ErrNotFound + } + + log.Debug("manager: register connection-client: id = %d, name = %s", c.conn, c.conn.Name) + + delete(m.clients, id) + + return nil +} + +func (m *manager) Map(fn func(*model.Connection, *s3.Client) error) error { + for _, item := range m.clients { + if err := fn(item.conn, item.client); err != nil { + return err + } + } + + return nil +} + +func (m *manager) Use(id uint64) (*model.Connection, *s3.Client, error) { + c, ok := m.clients[id] + if !ok { + return nil, nil, ErrNotFound + } + + return c.conn, c.client, nil +} diff --git a/internal/model/s3.go b/internal/model/s3.go index e6754a1..4b062dc 100644 --- a/internal/model/s3.go +++ b/internal/model/s3.go @@ -1,6 +1,10 @@ package model -import "gorm.io/gorm" +import ( + "errors" + "github.com/loveuer/nf-disk/ndh" + "gorm.io/gorm" +) type Connection struct { Id uint64 `json:"id" gorm:"primaryKey;column:id"` @@ -11,8 +15,22 @@ type Connection struct { Endpoint string `json:"endpoint" gorm:"column:endpoint"` Access string `json:"access" gorm:"column:access"` Key string `json:"key" gorm:"column:key"` + + Active bool `json:"active" gorm:"-"` } func (c *Connection) Create(tx *gorm.DB) error { return tx.Create(c).Error } + +func (c *Connection) Get(tx *gorm.DB, ctx *ndh.Ctx) error { + if err := tx.Take(c, c.Id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ctx.Send400(err.Error()) + } + + return ctx.Send500(err.Error()) + } + + return nil +} diff --git a/internal/s3/list.go b/internal/s3/list.go new file mode 100644 index 0000000..0056096 --- /dev/null +++ b/internal/s3/list.go @@ -0,0 +1,37 @@ +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" + "github.com/samber/lo" +) + +type ListBucketRes struct { + CreatedAt int64 + Name string +} + +func (c *Client) ListBucket(ctx context.Context) ([]*ListBucketRes, error) { + var ( + err error + input = &s3.ListBucketsInput{ + MaxBuckets: aws.Int32(100), + } + output *s3.ListBucketsOutput + ) + + if output, err = c.client.ListBuckets(ctx, input); err != nil { + return nil, err + } + + res := lo.Map( + output.Buckets, + func(item types.Bucket, index int) *ListBucketRes { + return &ListBucketRes{CreatedAt: item.CreationDate.UnixMilli(), Name: *item.Name} + }, + ) + + return res, nil +} diff --git a/main.go b/main.go index 14db991..843d69c 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,9 @@ 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()