feat: wrap message by fluent ui toast

This commit is contained in:
zhaoyupeng 2024-09-27 14:52:10 +08:00
parent 77dff6649d
commit b2c13508f4
27 changed files with 677 additions and 102 deletions

View File

@ -7,6 +7,7 @@ export interface Resp<T> {
data: T;
}
// 类型保护函数
function isResp<T>(obj: any): obj is Resp<T> {
return (
@ -18,23 +19,32 @@ function isResp<T>(obj: any): obj is Resp<T> {
);
}
const invoke = async <T>(path: string, req: any): Promise<Resp<T>> => {
export async function Dial<T>(path: string, req: any = null): Promise<Resp<T>> {
const bs = JSON.stringify(req)
console.log(`[DEBUG] invoke req: path = ${path}, req =`, req)
const res = await Invoke(path, bs)
console.log(`[DEBUG] invoke res: path = ${path}, res =`, res)
let result: Resp<T>;
let ok = false;
try {
const res = await Invoke(path, bs)
const parsed = JSON.parse(res);
if (isResp<T>(parsed)) {
return parsed;
result = parsed;
ok = true
} else {
console.error('[ERROR] invoke: resp not valid =', res)
throw new Error('Parsed response does not match Resp<T> structure');
result = {status: 500, msg: "发生错误(0)", err: res} as Resp<T>;
}
} catch (error) {
console.error(`[ERROR] invoke: resp parse err, err = ${error}, res =`, res);
throw new Error('Invalid response format');
result = {status: 500, msg: "发生错误(-1)", err: "backend method(Invoke) not found in window"} as Resp<T>;
}
}
export const Dial = invoke;
if (ok) {
console.log(`[DEBUG] invoke res: path = ${path}, res =`, result)
} else {
console.error(`[ERROR] invoke res: path = ${path}, res =`, result)
}
return result
}

View File

@ -0,0 +1,9 @@
export interface Connection {
id: number;
created_at: number;
updated_at: number;
deleted_at: number;
name: string;
endpoint: string;
active: boolean;
}

View File

@ -1,22 +1,25 @@
import React from 'react'
import {createRoot} from 'react-dom/client'
import './style.css'
import { FluentProvider, webLightTheme } from '@fluentui/react-components';
import {createBrowserRouter,RouterProvider} from "react-router-dom";
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 {ToastProvider} from "./message";
const container = document.getElementById('root')
const root = createRoot(container!)
const router = createBrowserRouter([
{path:'/', element: <Home />},
{path:'/connection', element: <Connection />},
{path: '/', element: <Home/>},
{path: '/connection', element: <Connection/>},
])
root.render(
<FluentProvider theme={webLightTheme}>
<RouterProvider router={router} />
<ToastProvider>
<RouterProvider router={router}/>
</ToastProvider>
</FluentProvider>,
);

37
frontend/src/message.tsx Normal file
View File

@ -0,0 +1,37 @@
import {createContext, FC, ReactNode, useContext} from "react";
import {Toast, Toaster, ToastTitle, useId, useToastController} from "@fluentui/react-components";
interface ToastContextType {
dispatchMessage: (content: string, type: "success" | "error" | "warning" | "info") => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export const ToastProvider: FC<{ children: ReactNode }> = ({children}) => {
const toasterId = useId("toaster");
const {dispatchToast} = useToastController(toasterId);
const dispatchMessage = (content: string, type: "success" | "error" | "warning" | "info" = "info") => {
dispatchToast(
<Toast>
<ToastTitle>{content}</ToastTitle>
</Toast>,
{position: "top-end", intent: type}
);
};
return <ToastContext.Provider value={{dispatchMessage}}>
{children}
<Toaster toasterId={toasterId}/>
</ToastContext.Provider>
}
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error("useToast must be used within a ToastProvider");
}
return context;
};

View File

@ -3,23 +3,28 @@ import {
useId,
Button,
FieldProps,
useToastController,
Toast,
ToastTitle,
ToastIntent,
Toaster
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<FieldProps>) => {
const toasterId = useId("toaster");
const {dispatchToast} = useToastController(toasterId);
const { dispatchMessage } = useToast();
const navigate = useNavigate();
const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial");
const buttonIcon =
testLoading === "loading" ? (
<Spinner size="tiny"/>
) : testLoading === "success" ? (
<CheckmarkFilled/>
) : testLoading === "error" ? (
<DismissRegular/>
) : null;
const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
name: '',
endpoint: '',
@ -28,16 +33,24 @@ const Connection = (props: Partial<FieldProps>) => {
})
function test() {
const val = JSON.stringify(value);
console.log('[DEBUG] connection.test: value =', val)
setTestLoading("loading")
Dial<string>("/api/connection/test", value).then(res => {
let status: "success" | "error" = "error"
if (res.status === 200) {
dispatchToast(
<Toast>
<ToastTitle>!</ToastTitle>
</Toast>,
{position: "top-end", intent: "success"}
)
status = "success"
}
setTestLoading(status);
dispatchMessage(res.msg, status)
})
}
function create() {
Dial<unknown>("/api/connection/create", value).then(res => {
dispatchMessage(res.msg, res.status === 200?"success":"error");
if (res.status === 200) {
navigate("/");
}
})
}
@ -94,17 +107,17 @@ const Connection = (props: Partial<FieldProps>) => {
</Field>
</div>
<div className='connection-form-field connection-form-field-actions'>
<Button appearance='transparent' onClick={() => test()}></Button>
<Button appearance='transparent' icon={buttonIcon} onClick={() => test()}></Button>
<div style={{marginLeft: 'auto'}}>
<Button style={{marginRight: '20px'}} className='connection-form-field-actions-cancel'
onClick={() => {
navigate("/")
}}></Button>
<Button className='connection-form-field-actions-confirm' appearance='primary'></Button>
<Button className='connection-form-field-actions-confirm' appearance='primary'
onClick={() => create()}></Button>
</div>
</div>
</div>
<Toaster toasterId={toasterId}/>
</div>
}

View File

@ -1,4 +1,4 @@
import {useState} from 'react';
import {useEffect, useState} from 'react';
import './home.css';
import {
Button,
@ -6,25 +6,45 @@ import {
import {
CloudAddFilled, DismissRegular
} from "@fluentui/react-icons";
import { useNavigate } from "react-router-dom";
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<string>('');
const navigate = useNavigate();
const [connectionList, setConnectionList] = useState<Connection[]>([]);
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)
}
})
}, []);
return (
<div className="container">
<div className="header">
<Button appearance="primary" icon={<CloudAddFilled />} onClick={() => {navigate("/connection")}}>
<Button appearance="primary" icon={<CloudAddFilled/>} onClick={() => {
navigate("/connection")
}}>
</Button>
</div>
<div className="body">
<div className="body-connections">
<div className="body-connections-search">
<input className="body-connections-search-input" type={"text"} placeholder="搜索连接" value={connectionFilterKeywords} onChange={(e) => setConnectionFilterKeywords(e.target.value)} />
<div className="body-connections-search-dismiss" onClick={() => {setConnectionFilterKeywords('')}}>
<DismissRegular />
<input className="body-connections-search-input" type={"text"} placeholder="搜索连接"
value={connectionFilterKeywords}
onChange={(e) => setConnectionFilterKeywords(e.target.value)}/>
<div className="body-connections-search-dismiss" onClick={() => {
setConnectionFilterKeywords('')
}}>
<DismissRegular/>
</div>
</div>
<div className="body-connections-list"></div>

27
go.mod
View File

@ -1,4 +1,4 @@
module nf-disk
module github.com/loveuer/nf-disk
go 1.21
@ -9,8 +9,15 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.27.38
github.com/aws/aws-sdk-go-v2/credentials v1.17.36
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.2
github.com/aws/smithy-go v1.21.0
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/psanford/httpreadat v0.1.0
github.com/wailsapp/wails/v2 v2.9.2
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
)
require (
@ -27,13 +34,19 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.23.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.31.2 // indirect
github.com/aws/smithy-go v1.21.0 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
@ -42,20 +55,24 @@ require (
github.com/leaanthony/u v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
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
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.16 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.9.2 => C:\Users\loveuer\go\pkg\mod

48
go.sum
View File

@ -43,12 +43,26 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
@ -64,6 +78,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/loveuer/go-sqlite3 v1.0.2 h1:kcENqm6mt0wPH/N9Sw+6UC74qtU8o+aMEO04I62pjDE=
github.com/loveuer/go-sqlite3 v1.0.2/go.mod h1:8+45etSlBYCtYP/ThX/e1wLgG+x6G6oXck2FhjC57tA=
github.com/loveuer/nf v0.2.11 h1:W775exDO8eNAHT45WDhXekMYCuWahOW9t1aVmGh3u1o=
github.com/loveuer/nf v0.2.11/go.mod h1:M6reF17/kJBis30H4DxR5hrtgo/oJL4AV4cBe4HzJLw=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
@ -75,21 +91,32 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-sqlite3 v0.18.4 h1:Je8o3y33MDwPYY/Cacas8yCsuoUzpNY/AgoSlN2ekyE=
github.com/ncruces/go-sqlite3 v0.18.4/go.mod h1:4HLag13gq1k10s4dfGBhMfRVsssJRT9/5hYqVM9RUYo=
github.com/ncruces/go-sqlite3/gormlite v0.18.4 h1:NdZkzS7SkcGlUafCmF6/fpqS/JkhxXP/DRPDYmSVdL4=
github.com/ncruces/go-sqlite3/gormlite v0.18.4/go.mod h1:laAntS4laxUO47GmxhIhSeJPrRSPF9TdsOQhaqlIifI=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@ -103,13 +130,15 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -120,15 +149,22 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

32
internal/api/api.go Normal file
View File

@ -0,0 +1,32 @@
package api
import (
"context"
"github.com/loveuer/nf-disk/internal/handler"
"github.com/loveuer/nf-disk/ndh"
"github.com/loveuer/nf/nft/log"
"reflect"
)
var (
apis = make(map[string]ndh.Handler)
)
func register(path string, h ndh.Handler) {
name := reflect.ValueOf(h).String()
log.Info("app register: path = %s, name = %s", path, name)
apis[path] = h
}
func Resolve(path string) (ndh.Handler, bool) {
h, ok := apis[path]
return h, ok
}
func Init(ctx context.Context) error {
register("/api/connection/test", handler.ConnectionTest)
register("/api/connection/create", handler.ConnectionCreate)
register("/api/connection/list", handler.ConnectionList)
return nil
}

View File

@ -1,18 +0,0 @@
package controller
import (
"github.com/loveuer/nf/nft/log"
"nf-disk/internal/handler"
"nf-disk/internal/ndh"
"reflect"
)
func (a *App) register(path string, handler ndh.Handler) {
name := reflect.ValueOf(handler).String()
log.Info("app register: path = %s, name = %s", path, name)
a.handlers[path] = handler
}
func initApi(a *App) {
a.register("/api/connection/test", handler.ConnectionTest)
}

View File

@ -1,13 +1,13 @@
package controller
import (
"bytes"
"context"
"encoding/json"
"github.com/loveuer/nf-disk/internal/api"
"github.com/loveuer/nf-disk/internal/db"
"github.com/loveuer/nf-disk/internal/model"
"github.com/loveuer/nf-disk/internal/tool"
"github.com/loveuer/nf-disk/ndh"
"github.com/loveuer/nf/nft/log"
"nf-disk/internal/ndh"
"nf-disk/internal/tool"
"strings"
)
type App struct {
@ -22,24 +22,11 @@ func NewApp() *App {
}
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
log.Info("app startup!!!")
initApi(a)
}
func (a *App) Invoke(path string, req string) (res string) {
log.Info("app invoke: path = %s, req = %s", path, req)
handler, ok := a.handlers[path]
if !ok {
return `{"err": "handler not found", "status": 404}`
}
var buf bytes.Buffer
ctx := ndh.NewCtx(tool.Timeout(), json.NewDecoder(strings.NewReader(req)), &buf)
if err := handler(ctx); err != nil {
return err.Error()
}
return buf.String()
a.ctx = ctx
tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
tool.Must(model.Init(db.Default.Session()))
tool.Must(api.Init(ctx))
}

View File

@ -0,0 +1,51 @@
package controller
import (
"bytes"
"encoding/json"
"fmt"
"github.com/loveuer/nf-disk/internal/api"
"github.com/loveuer/nf-disk/internal/opt"
"github.com/loveuer/nf-disk/internal/tool"
"github.com/loveuer/nf-disk/ndh"
"github.com/loveuer/nf/nft/log"
"strings"
)
func handleError(err error) string {
bs, _ := json.Marshal(map[string]any{
"err": err.Error(),
"msg": opt.Msg500,
"status": 500,
})
return string(bs)
}
func handleNotFound(path string) string {
bs, _ := json.Marshal(map[string]any{
"err": fmt.Sprintf("path not found, path: %s", path),
"msg": opt.Msg500,
"status": 404,
})
return string(bs)
}
func (a *App) Invoke(path string, req string) (res string) {
log.Info("app invoke: path = %s, req = %s", path, req)
handler, ok := api.Resolve(path)
if !ok {
log.Warn("app invoke: path not found, path = %s", path)
return handleNotFound(path)
}
var buf bytes.Buffer
ctx := ndh.NewCtx(tool.TimeoutCtx(a.ctx), strings.NewReader(req), &buf)
if err := handler(ctx); err != nil {
return handleError(err)
}
return buf.String()
}

61
internal/db/client.go Normal file
View File

@ -0,0 +1,61 @@
package db
import (
"context"
"github.com/loveuer/nf-disk/internal/opt"
"github.com/loveuer/nf-disk/internal/tool"
"io"
"gorm.io/gorm"
)
var (
Default *Client
)
type Client struct {
ctx context.Context
cli *gorm.DB
ttype string
cfgSqlite *cfgSqlite
}
func (c *Client) Type() string {
return c.ttype
}
func (c *Client) Session(ctxs ...context.Context) *gorm.DB {
var ctx context.Context
if len(ctxs) > 0 && ctxs[0] != nil {
ctx = ctxs[0]
} else {
ctx = tool.Timeout(30)
}
session := c.cli.Session(&gorm.Session{Context: ctx})
if opt.Debug {
session = session.Debug()
}
return session
}
func (c *Client) Close() {
d, _ := c.cli.DB()
d.Close()
}
// Dump
// Only for sqlite with mem mode to dump data to bytes(io.Reader)
func (c *Client) Dump() (reader io.ReadSeekCloser, ok bool) {
if c.ttype != "sqlite" {
return nil, false
}
if c.cfgSqlite.fsType != "mem" {
return nil, false
}
return c.cfgSqlite.memDump.Dump(), true
}

44
internal/db/db_test.go Normal file
View File

@ -0,0 +1,44 @@
package db
import (
"context"
"io"
"os"
"testing"
)
func TestOpen(t *testing.T) {
myClient, err := New(context.TODO(), "sqlite::", OptSqliteByMem())
if err != nil {
t.Fatalf("TestOpen: New err = %v", err)
}
type Start struct {
Id int `json:"id" gorm:"column:id;primaryKey"`
Name string `json:"name" gorm:"column:name"`
Dis float64 `json:"dis" gorm:"column:dis"`
}
if err = myClient.Session().AutoMigrate(&Start{}); err != nil {
t.Fatalf("TestOpen: AutoMigrate err = %v", err)
}
if err = myClient.Session().Create(&Start{Name: "sun", Dis: 6631.76}).Error; err != nil {
t.Fatalf("TestOpen: Create err = %v", err)
}
if err = myClient.Session().Create(&Start{Name: "mar", Dis: 786.35}).Error; err != nil {
t.Fatalf("TestOpen: Create err = %v", err)
}
if reader, ok := myClient.Dump(); ok {
bs, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("TestOpen: ReadAll err = %v", err)
}
os.WriteFile("dump.db", bs, 0644)
}
myClient.Close()
}

54
internal/db/init.go Normal file
View File

@ -0,0 +1,54 @@
package db
import (
"context"
"fmt"
"strings"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func New(ctx context.Context, uri string, opts ...Option) (*Client, error) {
strs := strings.Split(uri, "::")
if len(strs) != 2 {
return nil, fmt.Errorf("db.Init: opt db uri invalid: %s", uri)
}
c := &Client{ttype: strs[0], cfgSqlite: &cfgSqlite{fsType: "file"}}
for _, f := range opts {
f(c)
}
var (
err error
dsn = strs[1]
)
switch strs[0] {
case "sqlite":
err = openSqlite(c, dsn)
case "mysql":
c.cli, err = gorm.Open(mysql.Open(dsn))
case "postgres":
c.cli, err = gorm.Open(postgres.Open(dsn))
default:
return nil, fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", strs[0])
}
if err != nil {
return nil, fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", strs[0], dsn, err)
}
return c, nil
}
func Init(ctx context.Context, uri string, opts ...Option) (err error) {
if Default, err = New(ctx, uri, opts...); err != nil {
return err
}
return nil
}

27
internal/db/option.go Normal file
View File

@ -0,0 +1,27 @@
package db
import (
_ "github.com/loveuer/go-sqlite3/embed"
"io"
)
type Option func(c *Client)
func OptSqliteByUrl(address string) Option {
return func(c *Client) {
c.cfgSqlite.fsType = "url"
}
}
type SqliteMemDumper interface {
Dump() io.ReadSeekCloser
}
// 如果传 nil 则表示新生成一个 mem 的 sqlite
// 如果传了一个合法的 reader 则会从这个 reader 初始化 database
func OptSqliteByMem(reader io.ReadCloser) Option {
return func(c *Client) {
c.cfgSqlite.memReader = reader
c.cfgSqlite.fsType = "mem"
}
}

63
internal/db/sqlite.go Normal file
View File

@ -0,0 +1,63 @@
package db
import (
"fmt"
"io"
"time"
_ "github.com/loveuer/go-sqlite3/embed"
"github.com/loveuer/go-sqlite3/vfs/memdb"
"github.com/loveuer/go-sqlite3/vfs/readervfs"
"github.com/ncruces/go-sqlite3/gormlite"
"github.com/psanford/httpreadat"
"gorm.io/gorm"
)
type cfgSqlite struct {
fsType string // file, mem(bytes), url
memDump *memdb.MemDB
memReader io.ReadCloser
}
func openSqlite(c *Client, dsn string) error {
var (
db gorm.Dialector
err error
)
switch c.cfgSqlite.fsType {
case "file":
db = gormlite.Open("file:" + dsn)
case "url":
name := fmt.Sprintf("%d.db", time.Now().UnixNano())
readervfs.Create(name, httpreadat.New(dsn))
uri := fmt.Sprintf("file:%s?vfs=reader", name)
db = gormlite.Open(uri)
case "mem":
var (
bs []byte
name = fmt.Sprintf("%d.db", time.Now().UnixNano())
)
if c.cfgSqlite.memReader == nil {
bs = make([]byte, 0)
} else {
if bs, err = io.ReadAll(c.cfgSqlite.memReader); err != nil {
return err
}
}
memDump := memdb.Create(name, bs)
c.cfgSqlite.memDump = memDump
uri := fmt.Sprintf("file:/%s?vfs=memdb", name)
db = gormlite.Open(uri)
default:
return fmt.Errorf("unsupported sqlite fs type: %s", c.cfgSqlite.fsType)
}
if c.cli, err = gorm.Open(db); err != nil {
return err
}
return nil
}

View File

@ -1,8 +1,11 @@
package handler
import (
"nf-disk/internal/ndh"
"nf-disk/internal/s3"
"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"
)
func ConnectionTest(c *ndh.Ctx) error {
@ -30,5 +33,69 @@ func ConnectionTest(c *ndh.Ctx) error {
return c.Send500(err.Error(), "连接失败")
}
return c.Send200("test success")
return c.Send200("连接测试成功")
}
func ConnectionCreate(c *ndh.Ctx) error {
type Req struct {
Name string `json:"name"`
Endpoint string `json:"endpoint"`
Access string `json:"access"`
Key string `json:"key"`
Force bool `json:"force"`
}
var (
err error
req = new(Req)
client *s3.Client
)
if err = c.ReqParse(req); err != nil {
return err
}
if req.Endpoint == "" || req.Access == "" || req.Key == "" {
return c.Send400(nil, "endpoint, secret_access, secret_key 是必填项")
}
if client, err = s3.New(c.Context(), req.Endpoint, req.Access, req.Key); err != nil {
return c.Send500(err.Error(), "连接失败")
}
if req.Name == "" {
req.Name = req.Endpoint
}
connection := &model.Connection{
Name: req.Name,
Endpoint: req.Endpoint,
Access: req.Access,
Key: req.Key,
}
if err = connection.Create(db.Default.Session()); err != nil {
return c.Send500(err.Error(), "创建连接失败(1)")
}
if err = manager.Register(connection, client); err != nil {
return c.Send500(err.Error(), "创建连接失败(2)")
}
return c.Send200(connection, "创建连接成功")
}
func ConnectionList(c *ndh.Ctx) error {
var (
err error
list = make([]*model.Connection, 0)
)
if err = db.Default.Session().Model(&model.Connection{}).
Find(&list).
Error; err != nil {
return err
}
return c.Send200(map[string]any{"list": list})
}

View File

@ -0,0 +1,18 @@
package manager
import (
"context"
"github.com/loveuer/nf-disk/internal/model"
"github.com/loveuer/nf-disk/internal/s3"
"github.com/loveuer/nf/nft/log"
)
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)
return nil
}

9
internal/model/init.go Normal file
View File

@ -0,0 +1,9 @@
package model
import "gorm.io/gorm"
func Init(tx *gorm.DB) error {
return tx.AutoMigrate(
&Connection{},
)
}

18
internal/model/s3.go Normal file
View File

@ -0,0 +1,18 @@
package model
import "gorm.io/gorm"
type Connection struct {
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
Name string `json:"name" gorm:"unique;column:name"`
Endpoint string `json:"endpoint" gorm:"column:endpoint"`
Access string `json:"access" gorm:"column:access"`
Key string `json:"key" gorm:"column:key"`
}
func (c *Connection) Create(tx *gorm.DB) error {
return tx.Create(c).Error
}

View File

@ -1,5 +1,11 @@
package opt
const (
Msg200 = "操作成功"
Msg400 = "输入不正确"
Msg500 = "发生错误"
)
var (
Debug bool
)

View File

@ -7,9 +7,9 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
smithyendpoints "github.com/aws/smithy-go/endpoints"
"github.com/loveuer/nf-disk/internal/tool"
"github.com/loveuer/nf/nft/log"
"net/url"
"nf-disk/internal/tool"
)
type resolverV2 struct{}

11
internal/tool/must.go Normal file
View File

@ -0,0 +1,11 @@
package tool
import "github.com/loveuer/nf/nft/log"
func Must(errs ...error) {
for _, err := range errs {
if err != nil {
log.Panic(err.Error())
}
}
}

View File

@ -3,8 +3,8 @@ package main
import (
"embed"
"flag"
"github.com/loveuer/nf-disk/internal/controller"
"github.com/loveuer/nf/nft/nfctl/opt"
"nf-disk/internal/controller"
"github.com/loveuer/nf/nft/log"
"github.com/wailsapp/wails/v2"

View File

@ -8,11 +8,11 @@ import (
type Ctx struct {
ctx context.Context
req *json.Decoder
req io.Reader
res io.Writer
}
func NewCtx(ctx context.Context, req *json.Decoder, res io.Writer) *Ctx {
func NewCtx(ctx context.Context, req io.Reader, res io.Writer) *Ctx {
return &Ctx{
ctx: ctx,
req: req,
@ -29,7 +29,7 @@ func (c *Ctx) Write(bs []byte) (int, error) {
}
func (c *Ctx) ReqParse(req any) error {
return c.req.Decode(req)
return json.NewDecoder(c.req).Decode(req)
}
func (c *Ctx) Send200(data any, msg ...string) error {