Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ff83f12b4 | ||
|
|
6f15f82122 | ||
|
|
660c5a7efb | ||
|
|
777253063b | ||
|
|
1c818daf16 |
@@ -1,19 +1,5 @@
|
||||
# README
|
||||
# NF-DISK
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails React-TS template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
一个由 wails 构建的桌面应用,用于管理S3。
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<title>nf-disk</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root" style="height: 100%"></div>
|
||||
<script src="./src/main.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+110
-1
@@ -10,11 +10,16 @@
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.54.16",
|
||||
"@fluentui/react-icons": "^2.0.258",
|
||||
"jotai": "^2.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.26.2"
|
||||
"react-router-dom": "^6.26.2",
|
||||
"use-debounce": "^10.0.3",
|
||||
"zustand": "^5.0.0-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
@@ -2370,6 +2375,13 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.10.tgz",
|
||||
"integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
@@ -3002,6 +3014,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",
|
||||
@@ -3037,6 +3070,12 @@
|
||||
"integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -3427,6 +3466,18 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/use-debounce/-/use-debounce-10.0.3.tgz",
|
||||
"integrity": "sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/use-disposable": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/use-disposable/-/use-disposable-1.0.2.tgz",
|
||||
@@ -3502,6 +3553,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": {
|
||||
@@ -5193,6 +5273,12 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.17.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.10.tgz",
|
||||
"integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
@@ -5549,6 +5635,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",
|
||||
@@ -5571,6 +5663,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/keyborg/-/keyborg-2.6.0.tgz",
|
||||
"integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -5831,6 +5928,12 @@
|
||||
"picocolors": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"use-debounce": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/use-debounce/-/use-debounce-10.0.3.tgz",
|
||||
"integrity": "sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==",
|
||||
"requires": {}
|
||||
},
|
||||
"use-disposable": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/use-disposable/-/use-disposable-1.0.2.tgz",
|
||||
@@ -5861,6 +5964,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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,16 @@
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.54.16",
|
||||
"@fluentui/react-icons": "^2.0.258",
|
||||
"jotai": "^2.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.26.2"
|
||||
"react-router-dom": "^6.26.2",
|
||||
"use-debounce": "^10.0.3",
|
||||
"zustand": "^5.0.0-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
b35fc08c84ef0c2b0c3e1bf37916ac94
|
||||
674272f31b23a798d77a17321c6a8785
|
||||
Generated
+34
@@ -20,6 +20,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-router-dom:
|
||||
specifier: ^6.26.2
|
||||
version: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^18.0.17
|
||||
@@ -698,6 +701,10 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@remix-run/router@1.19.2':
|
||||
resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@swc/helpers@0.5.13':
|
||||
resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==}
|
||||
|
||||
@@ -990,6 +997,19 @@ packages:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react-router-dom@6.26.2:
|
||||
resolution: {integrity: sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
react-router@6.26.2:
|
||||
resolution: {integrity: sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
|
||||
react-transition-group@4.4.5:
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
@@ -2370,6 +2390,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@remix-run/router@1.19.2': {}
|
||||
|
||||
'@swc/helpers@0.5.13':
|
||||
dependencies:
|
||||
tslib: 2.7.0
|
||||
@@ -2602,6 +2624,18 @@ snapshots:
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.19.2
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-router: 6.26.2(react@18.3.1)
|
||||
|
||||
react-router@6.26.2(react@18.3.1):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.19.2
|
||||
react: 18.3.1
|
||||
|
||||
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.6
|
||||
|
||||
@@ -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=any>(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
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
|
||||
import {ArchiveRegular} from "@fluentui/react-icons";
|
||||
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
|
||||
import React from "react";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {Bucket} from "../../interfaces/connection";
|
||||
import {useStoreFile, useStoreFileFilter} from "../../store/file";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
marginTop: '0.5rem',
|
||||
maxWidth: 'calc(100vw - 25rem - 1px)',
|
||||
},
|
||||
row: {
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
marginLeft: '0.5rem',
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
item: {
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
"&:hover": {
|
||||
color: tokens.colorNeutralForeground2BrandPressed,
|
||||
}
|
||||
},
|
||||
text: {
|
||||
overflow: 'hidden',
|
||||
width: 'calc(100vw - 32rem)',
|
||||
display: "block",
|
||||
}
|
||||
})
|
||||
|
||||
export function ListBucketComponent() {
|
||||
|
||||
const styles = useStyles();
|
||||
const {bucket_set, bucket_list} = useStoreBucket()
|
||||
const {filter_set, prefix_set} = useStoreFileFilter()
|
||||
|
||||
async function handleClick(item: Bucket) {
|
||||
await bucket_set(item)
|
||||
await filter_set('')
|
||||
await prefix_set('')
|
||||
}
|
||||
|
||||
function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: Bucket) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
return <MenuList className={styles.container}>
|
||||
<VirtualizerScrollView
|
||||
numItems={bucket_list ? bucket_list.length : 0}
|
||||
itemSize={32}
|
||||
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
|
||||
>
|
||||
{(idx) => {
|
||||
return <div
|
||||
className={styles.row} key={idx}
|
||||
onClick={async () => {
|
||||
await handleClick(bucket_list[idx])
|
||||
}}
|
||||
onContextMenu={async (e) => {
|
||||
handleRightClick(e, bucket_list[idx])
|
||||
}}>
|
||||
<MenuItem className={styles.item}
|
||||
icon={<ArchiveRegular/>}>
|
||||
<Text truncate wrap={false} className={styles.text}>
|
||||
{bucket_list[idx].name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</div>
|
||||
}}
|
||||
</VirtualizerScrollView>
|
||||
</MenuList>
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
makeStyles,
|
||||
|
||||
MenuItem,
|
||||
MenuList,
|
||||
mergeClasses,
|
||||
tokens,
|
||||
Tooltip
|
||||
} from "@fluentui/react-components"
|
||||
import {
|
||||
DatabaseLinkRegular,
|
||||
DeleteRegular,
|
||||
DismissRegular,
|
||||
PlugConnectedRegular, PlugDisconnectedRegular,
|
||||
SettingsRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Connection} from "../../interfaces/connection";
|
||||
import {useToast} from "../../message";
|
||||
import {Dial} from "../../api";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
list: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
height: "100%",
|
||||
},
|
||||
content: {
|
||||
height: "100%",
|
||||
width: "25rem",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
filter: {
|
||||
height: "4rem",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
filter_input: {
|
||||
width: "100%",
|
||||
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%",
|
||||
},
|
||||
items_one: {
|
||||
marginLeft: "0.5rem",
|
||||
marginRight: "0.5rem",
|
||||
"&:hover": {
|
||||
color: tokens.colorNeutralForeground2BrandPressed,
|
||||
},
|
||||
"&.active": {
|
||||
color: tokens.colorNeutralForeground2BrandPressed,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
"& > span": {
|
||||
display: "flex",
|
||||
},
|
||||
},
|
||||
items_disconn: {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
slider: {
|
||||
height: '100%', width: '1px',
|
||||
// todo: resize
|
||||
// cursor: 'ew-resize',
|
||||
'& > div': {
|
||||
height: '100%', width: '1px',
|
||||
backgroundColor: 'lightgray',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function ConnectionList() {
|
||||
const styles = useStyles()
|
||||
const {dispatchMessage} = useToast();
|
||||
const {conn_get, conn_list, conn_set} = 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'});
|
||||
const [menu_conn, set_menu_conn] = useState<Connection | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", (e) => {
|
||||
set_ctx_menu({x: 0, y: 0, display: 'none'});
|
||||
})
|
||||
setTimeout(() => {
|
||||
conn_get().then()
|
||||
}, 1000)
|
||||
return () => {
|
||||
document.removeEventListener("click", (e) => {
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function handleSelect(item: Connection) {
|
||||
conn_list.map((one: Connection) => {
|
||||
if (item.id === one.id && one.active) {
|
||||
conn_set(one)
|
||||
bucket_get(one, false)
|
||||
bucket_set(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleConnect(item: Connection | null) {
|
||||
if (!item) return;
|
||||
let res = await Dial('/api/connection/connect', {id: item.id});
|
||||
if (res.status !== 200) {
|
||||
dispatchMessage(res.msg, "error")
|
||||
return
|
||||
}
|
||||
|
||||
await conn_set({...item, active: true})
|
||||
bucket_get(item, true)
|
||||
await bucket_set(null)
|
||||
}
|
||||
|
||||
async function handleDisconnect(item: Connection | null) {
|
||||
if (!item) return;
|
||||
let res = await Dial('/api/connection/disconnect', {id: item.id})
|
||||
if (res.status !== 200) {
|
||||
dispatchMessage(res.msg, "error")
|
||||
return
|
||||
}
|
||||
await conn_set({...item, active: false})
|
||||
}
|
||||
|
||||
async function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: Connection) {
|
||||
e.preventDefault()
|
||||
set_menu_conn(item)
|
||||
|
||||
const ele = document.querySelector('#list-connection-container')
|
||||
const eleX = ele ? ele.clientWidth : 0;
|
||||
const eleY = ele ? ele.clientHeight : 0;
|
||||
const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX
|
||||
const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY
|
||||
|
||||
set_ctx_menu({
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
display: 'block',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.filter}>
|
||||
<Input
|
||||
value={conn_filter}
|
||||
className={styles.filter_input}
|
||||
contentAfter={
|
||||
<Button appearance={'transparent'} onClick={async () => {
|
||||
set_conn_filter('')
|
||||
}} size="small" icon={<DismissRegular/>}/>
|
||||
}
|
||||
placeholder="搜索连接"
|
||||
onChange={(e) => set_conn_filter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id={'list-connection-container'}
|
||||
className={styles.ctx_menu}
|
||||
style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await handleConnect(menu_conn)
|
||||
}}
|
||||
icon={<PlugConnectedRegular/>}>连接</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await handleDisconnect(menu_conn)
|
||||
}}
|
||||
icon={<PlugDisconnectedRegular/>}>断开</MenuItem>
|
||||
<MenuItem icon={<SettingsRegular/>}>设置</MenuItem>
|
||||
<MenuItem icon={<DeleteRegular/>}>删除</MenuItem>
|
||||
</MenuList>
|
||||
</div>
|
||||
<div className={styles.items}>
|
||||
<MenuList>
|
||||
{conn_list.filter(item => item.name.includes(conn_filter)).map(item => {
|
||||
return <MenuItem
|
||||
className={item.active ? mergeClasses(styles.items_one, "active") : styles.items_one}
|
||||
onClick={async () => {
|
||||
await handleSelect(item)
|
||||
}}
|
||||
onDoubleClick={async () => {
|
||||
await handleConnect(item)
|
||||
}}
|
||||
onContextMenu={async (e) => {
|
||||
await handleRightClick(e, item)
|
||||
}}
|
||||
icon={<DatabaseLinkRegular/>}
|
||||
key={item.id}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
})}
|
||||
</MenuList>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.slider}>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
DialogTrigger,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
DialogBody,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Button, Spinner, Field, Input, 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";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
|
||||
const useActionStyle = makeStyles({
|
||||
container: {
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridColumnStart: 0,
|
||||
},
|
||||
test: {}
|
||||
});
|
||||
|
||||
export interface ConnectionCreateProps {
|
||||
openFn: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function ConnectionCreate(props: ConnectionCreateProps) {
|
||||
const actionStyle = useActionStyle();
|
||||
const {dispatchMessage} = useToast();
|
||||
const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial");
|
||||
const {conn_get} = useStoreConnection();
|
||||
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: '',
|
||||
access: '',
|
||||
key: ''
|
||||
})
|
||||
|
||||
async function test() {
|
||||
setTestLoading("loading")
|
||||
let res = await Dial<string>("/api/connection/test", value)
|
||||
const status = res.status === 200 ? "success" : "error"
|
||||
setTestLoading(status);
|
||||
dispatchMessage(res.msg, status)
|
||||
}
|
||||
|
||||
async function create() {
|
||||
// self
|
||||
// qUvfW8xpOTc23O96
|
||||
// eTcuc8BebHPVpZZwIaNmzfwxRxPYGfTj
|
||||
|
||||
// 48-dev
|
||||
// OSIsqPrl0TkAUj3R
|
||||
// FYF4BBzL2j2ObbVYH0FrvOZqJf1EACRy
|
||||
let res = await Dial("/api/connection/create", value)
|
||||
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
|
||||
if (res.status === 200) {
|
||||
dispatchMessage("新建连接成功", "success");
|
||||
await conn_get()
|
||||
props.openFn(false)
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>新建S3连接</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className='connection-container'>
|
||||
<div className='connection-form'>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="name"
|
||||
>
|
||||
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
|
||||
onChange={(e) => {
|
||||
setValue({...value, name: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="endpoint"
|
||||
required
|
||||
>
|
||||
<Input placeholder='地址 (example: https://ip_or_server-name:port)'
|
||||
value={value.endpoint}
|
||||
required
|
||||
onChange={(e) => {
|
||||
setValue({...value, endpoint: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret access"
|
||||
required
|
||||
>
|
||||
<Input placeholder=''
|
||||
required
|
||||
value={value.access} onChange={(e) => {
|
||||
setValue({...value, access: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret key"
|
||||
required
|
||||
>
|
||||
<Input placeholder=''
|
||||
required
|
||||
value={value.key} onChange={(e) => {
|
||||
setValue({...value, key: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions className={actionStyle.container}>
|
||||
<Button className={actionStyle.test} appearance='transparent' icon={buttonIcon}
|
||||
onClick={async () => await test()}>测试连接</Button>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="secondary">取消</Button>
|
||||
</DialogTrigger>
|
||||
<Button onClick={async () => {
|
||||
await create()
|
||||
}} appearance="primary">新建</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {Path} from "./path";
|
||||
import {ListBucketComponent} from "../bucket/list_bucket";
|
||||
import {makeStyles} from "@fluentui/react-components";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {ListFileComponent} from "./list_file";
|
||||
import {useState} from "react";
|
||||
import {PreviewFile} from "../preview/preview";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
flex: '1',
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
},
|
||||
})
|
||||
|
||||
export function Content() {
|
||||
|
||||
const styles = useStyles()
|
||||
const [preview, set_preview] = useState<{ url: string, content_type: string }>({url: '', content_type: ''})
|
||||
const {bucket_active} = useStoreBucket()
|
||||
|
||||
return <div className={styles.content}>
|
||||
<Path/>
|
||||
{
|
||||
preview.url ? <PreviewFile url={preview.url} content_type={preview.content_type}/> :
|
||||
(
|
||||
bucket_active ?
|
||||
<ListFileComponent set_preview_fn={set_preview}/> : <ListBucketComponent/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import {makeStyles, MenuItem, MenuList, Spinner, Text, tokens} from "@fluentui/react-components";
|
||||
import {
|
||||
ArrowDownloadFilled,
|
||||
DeleteRegular,
|
||||
DocumentBulletListRegular,
|
||||
DocumentChevronDoubleRegular,
|
||||
DocumentCssRegular,
|
||||
DocumentDatabaseRegular,
|
||||
DocumentDismissRegular,
|
||||
DocumentImageRegular,
|
||||
DocumentJavascriptRegular,
|
||||
DocumentPdfRegular,
|
||||
DocumentYmlRegular,
|
||||
FolderRegular,
|
||||
PreviewLinkRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {S3File} from "../../interfaces/connection";
|
||||
import {useStoreFile, useStoreFileFilter} from "../../store/file";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
import {TrimSuffix} from "../../hook/strings";
|
||||
import {Dial} from "../../api";
|
||||
import {useToast} from "../../message";
|
||||
import {CanPreview} from "../../hook/preview";
|
||||
import {useStorePreview} from "../../store/preview";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
marginTop: '0.5rem',
|
||||
maxWidth: 'calc(100vw - 25.2rem)',
|
||||
width: 'calc(100vw - 25.2rem)',
|
||||
maxHeight: 'calc(100vh - 10rem)',
|
||||
height: 'calc(100vh - 10rem)',
|
||||
},
|
||||
loading: {
|
||||
flex: "1",
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
no_data: {
|
||||
flex: "1",
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '8rem',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
list: {
|
||||
flex: "1",
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
row: {
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
marginLeft: '0.5rem',
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
item: {
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
"&:hover": {
|
||||
color: tokens.colorNeutralForeground2BrandPressed,
|
||||
}
|
||||
},
|
||||
text: {
|
||||
overflow: 'hidden',
|
||||
width: 'calc(100vw - 32rem)',
|
||||
display: "block",
|
||||
},
|
||||
ctx_menu: {
|
||||
position: "absolute",
|
||||
zIndex: "1000",
|
||||
width: "15rem",
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
boxShadow: `${tokens.shadow16}`,
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
export interface ListFileComponentProps {
|
||||
set_preview_fn: React.Dispatch<React.SetStateAction<{url:string, content_type: string}>>
|
||||
}
|
||||
|
||||
export function ListFileComponent(props: ListFileComponentProps) {
|
||||
|
||||
const styles = useStyles();
|
||||
const {dispatchMessage} = useToast();
|
||||
const {conn_active} = useStoreConnection();
|
||||
const {bucket_active} = useStoreBucket()
|
||||
const {file_active, files_get, file_set, files_list} = useStoreFile()
|
||||
const {prefix, filter, prefix_set} = useStoreFileFilter()
|
||||
const [preview_content_type, set_preview_content_type] = useState('')
|
||||
const {preview_get} = useStorePreview()
|
||||
const [ctx_menu, set_ctx_menu] = useState<{
|
||||
x: number,
|
||||
y: number,
|
||||
display: 'none' | 'block'
|
||||
}>({x: 0, y: 0, display: 'none'});
|
||||
const [loading, set_loading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", (e) => {
|
||||
set_ctx_menu({x: 0, y: 0, display: 'none'});
|
||||
})
|
||||
return () => {
|
||||
document.removeEventListener("click", (e) => {
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
set_loading(true)
|
||||
files_get(conn_active!, bucket_active!, prefix, filter).then(() => {
|
||||
set_loading(false)
|
||||
})
|
||||
}, [conn_active, bucket_active, prefix, filter]);
|
||||
|
||||
const filename = (key: string) => {
|
||||
let strs = TrimSuffix(key, "/").split("/")
|
||||
return strs[strs.length - 1]
|
||||
}
|
||||
|
||||
async function handleClick(item: S3File) {
|
||||
if (item.type === 1) {
|
||||
await prefix_set(item.key)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: S3File) {
|
||||
e.preventDefault()
|
||||
await file_set(item.key)
|
||||
set_preview_content_type(CanPreview(item.name))
|
||||
const ele = document.querySelector('#list-file-container')
|
||||
let eleX = ele ? ele.clientWidth : 0
|
||||
let eleY = ele ? ele.clientHeight : 0
|
||||
const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX
|
||||
const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY
|
||||
set_ctx_menu({
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
display: 'block',
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDownload(file: string | null) {
|
||||
if (!file) return
|
||||
const res1 = await Dial<{ result: string }>("/runtime/dialog/save", {
|
||||
default_filename: file,
|
||||
})
|
||||
if (res1.status !== 200) {
|
||||
return
|
||||
}
|
||||
if (!res1.data) return
|
||||
const res2 = await Dial('/api/file/download', {
|
||||
conn_id: conn_active?.id,
|
||||
bucket: bucket_active?.name,
|
||||
key: file,
|
||||
location: res1.data.result,
|
||||
})
|
||||
if (res2.status === 200) {
|
||||
dispatchMessage('保存文件成功', 'success')
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePreview() {
|
||||
const res = await Dial<{url:string, method: string}>('/api/file/get', {
|
||||
conn_id: conn_active?.id,
|
||||
bucket: bucket_active?.name,
|
||||
key: file_active ?? "",
|
||||
})
|
||||
if (res.status !== 200) {
|
||||
dispatchMessage('预览失败', 'warning')
|
||||
return
|
||||
}
|
||||
props.set_preview_fn({url: res.data.url, content_type: preview_content_type})
|
||||
}
|
||||
|
||||
return <div className={styles.container}>
|
||||
<div
|
||||
id={'list-file-container'}
|
||||
className={styles.ctx_menu}
|
||||
style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await handleDownload(file_active)
|
||||
}}
|
||||
icon={<ArrowDownloadFilled/>}>下载</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!preview_content_type}
|
||||
onClick={async () => {
|
||||
await handlePreview()
|
||||
}}
|
||||
icon={<PreviewLinkRegular/>}>预览</MenuItem>
|
||||
<MenuItem icon={<DeleteRegular/>}>删除</MenuItem>
|
||||
</MenuList>
|
||||
</div>
|
||||
<div className={styles.loading} style={{display: loading ? 'flex' : 'none'}}>
|
||||
<Spinner appearance="primary" label="加载中..."/>
|
||||
</div>
|
||||
<div className={styles.no_data} style={{display: (!loading && !files_list.length) ? 'flex' : 'none'}}>
|
||||
<div>
|
||||
<DocumentDismissRegular/>
|
||||
</div>
|
||||
<Text size={900}>
|
||||
没有文件
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{display: (!loading && files_list.length) ? 'block' : 'none'}}>
|
||||
<VirtualizerScrollView
|
||||
numItems={files_list.length}
|
||||
itemSize={32}
|
||||
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
|
||||
>
|
||||
{(idx) => {
|
||||
return <div
|
||||
className={styles.row} key={idx}
|
||||
onClick={async () => {
|
||||
await handleClick(files_list[idx])
|
||||
}}
|
||||
onContextMenu={async (e) => {
|
||||
await handleRightClick(e, files_list[idx])
|
||||
}}>
|
||||
<MenuItem className={styles.item}
|
||||
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>
|
||||
</MenuItem>
|
||||
</div>
|
||||
}}
|
||||
</VirtualizerScrollView>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
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/>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import {Button, Input, makeStyles, Text, tokens, Tooltip} from "@fluentui/react-components";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {ArchiveRegular, ArrowCurveUpLeftFilled} from "@fluentui/react-icons";
|
||||
import {useStoreFile, useStoreFileFilter} from "../../store/file";
|
||||
import React, {useState} from "react";
|
||||
import {debounce} from 'lodash'
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
import {ListFileComponent} from "./list_file";
|
||||
import {ListBucketComponent} from "../bucket/list_bucket";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
height: '4rem',
|
||||
width: '100%',
|
||||
borderBottom: '1px solid lightgray',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
show: {
|
||||
marginLeft: '0.5rem',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
show_line: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
show_text: {
|
||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
||||
padding: '0.5rem 0.5rem',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
display: 'block',
|
||||
alignItems: 'center',
|
||||
marginLeft: '0.5rem',
|
||||
overflow: 'hidden',
|
||||
maxWidth: '8rem',
|
||||
verticalAlign: 'middle',
|
||||
'&:hover': {
|
||||
textDecoration: 'none',
|
||||
backgroundColor: tokens.colorNeutralBackground1Pressed,
|
||||
},
|
||||
'& > div': {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
op_up: {},
|
||||
filter_prefix: {
|
||||
margin: '0.5rem',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
export function Path() {
|
||||
const styles = useStyles()
|
||||
const {conn_active} = useStoreConnection()
|
||||
const {bucket_active, bucket_get, bucket_set} = useStoreBucket()
|
||||
const {prefix, filter, prefix_set, filter_set} = useStoreFileFilter()
|
||||
|
||||
async function handleClickUp() {
|
||||
const dirs = prefix.split('/').filter((item => item))
|
||||
if (dirs.length > 0) {
|
||||
dirs.pop()
|
||||
await prefix_set(dirs.join('/'))
|
||||
return
|
||||
}
|
||||
|
||||
bucket_get(conn_active!, false)
|
||||
await bucket_set(null)
|
||||
}
|
||||
|
||||
|
||||
const handleFilterChange = debounce(async (e) => {
|
||||
await filter_set(e.target.value)
|
||||
}, 500)
|
||||
|
||||
return <div className={styles.container}>
|
||||
{bucket_active && (
|
||||
<>
|
||||
<div className={styles.show}>
|
||||
<Tooltip content="返回上一级" relationship="label">
|
||||
<Button className={styles.op_up}
|
||||
onClick={async () => {
|
||||
await handleClickUp()
|
||||
}}
|
||||
size="small" icon={<ArrowCurveUpLeftFilled/>}/>
|
||||
</Tooltip>
|
||||
<Tooltip content={bucket_active.name} relationship={'description'}>
|
||||
<Text className={styles.show_text}
|
||||
truncate
|
||||
wrap={false}
|
||||
align={'justify'}
|
||||
style={{maxWidth: '16rem'}}
|
||||
>
|
||||
<div>
|
||||
<ArchiveRegular style={{margin: '0rem 0.5rem 0 0'}}/>
|
||||
{bucket_active.name}
|
||||
</div>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
{prefix && (
|
||||
prefix.split("/").filter(item => item).map((item, idx) => {
|
||||
return <div className={styles.show_line} key={idx}>
|
||||
<Text style={{marginLeft: '0.5rem'}}>/</Text>
|
||||
<Text className={styles.show_text} truncate wrap={false}>{item}</Text>
|
||||
</div>
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.filter_prefix}>
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
handleFilterChange(e)
|
||||
}}
|
||||
placeholder={"输入前缀过滤"}
|
||||
// contentBefore={<Text>/</Text>}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
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, useStoreFileFilter} 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 {files_get} = useStoreFile()
|
||||
const {prefix } = useStoreFileFilter()
|
||||
|
||||
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>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {makeStyles} from "@fluentui/react-components";
|
||||
import {ConnectionList} from "../connection/list";
|
||||
import {Content} from "../file/content";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
body: {
|
||||
display: "flex",
|
||||
flexDirection: 'row',
|
||||
width: "100%",
|
||||
flex: '1',
|
||||
},
|
||||
})
|
||||
|
||||
export function Body() {
|
||||
const styles = useStyles();
|
||||
|
||||
return <div className={styles.body}>
|
||||
<ConnectionList/>
|
||||
<Content/>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function Footer() {
|
||||
return <div></div>
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components";
|
||||
import {ConnectionCreate} from "../connection/new";
|
||||
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",
|
||||
minHeight: '5rem',
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
alignItems: "center",
|
||||
borderBottom: "1px solid lightgray",
|
||||
},
|
||||
button_new: {
|
||||
margin: '0.5rem',
|
||||
},
|
||||
})
|
||||
|
||||
export function Header() {
|
||||
const styles = useStyles();
|
||||
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}>
|
||||
<Dialog
|
||||
open={open_create_conn}
|
||||
onOpenChange={(event, data) => set_open_create_conn(data.open)}>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="primary" icon={<PlugConnectedAddRegular/>}>
|
||||
新建连接
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<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>
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {Header} from "./header";
|
||||
import {Body} from "./body";
|
||||
import {makeStyles} from "@fluentui/react-components";
|
||||
import {Footer} from "./footer";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
})
|
||||
|
||||
export function Home() {
|
||||
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Header />
|
||||
<Body />
|
||||
<Footer/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {CardPreview, makeStyles} from "@fluentui/react-components";
|
||||
|
||||
const useStyle = makeStyles({
|
||||
container: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}
|
||||
})
|
||||
export function PreviewFile(props: {url:string, content_type:string}) {
|
||||
const styles = useStyle()
|
||||
return <div className={styles.container}>
|
||||
<CardPreview
|
||||
>
|
||||
<img src={props.url} />
|
||||
</CardPreview>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export function CanPreview(filename: string) {
|
||||
const fs = filename.split(".")
|
||||
switch (fs[fs.length - 1]) {
|
||||
case "jpg":
|
||||
return "image/jpg"
|
||||
case "jpeg":
|
||||
return "image/jpg"
|
||||
case "png":
|
||||
return "image/png"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function TrimSuffix(str: string, suffix: string) {
|
||||
if (str.lastIndexOf(suffix) === str.length - suffix.length) {
|
||||
return str.substring(0, str.length - suffix.length);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function GetBaseFileName(fullPath: string) {
|
||||
return fullPath.replace(/.*[\/\\]/, '');
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface Connection {
|
||||
id: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
deleted_at: number;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface Bucket {
|
||||
name: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
export interface S3File {
|
||||
name: string;
|
||||
key: string;
|
||||
last_modified: number;
|
||||
size: number;
|
||||
type: 0 | 1;
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
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 Home from "./page/home/home";
|
||||
import Connection from "./page/connection/connection";
|
||||
import {FluentProvider, webLightTheme} from '@fluentui/react-components';
|
||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||
import {Home} from "./component/home/home";
|
||||
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/>},
|
||||
])
|
||||
|
||||
root.render(
|
||||
<FluentProvider theme={webLightTheme}>
|
||||
<RouterProvider router={router} />
|
||||
<FluentProvider theme={webLightTheme} style={{height: '100%'}}>
|
||||
<ToastProvider>
|
||||
<RouterProvider router={router}/>
|
||||
</ToastProvider>
|
||||
</FluentProvider>,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
div.connection-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.connection-form {
|
||||
max-width: 700px;
|
||||
min-width: 500px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.connection-form-field {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
div.connection-form-field-actions {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import './connection.css'
|
||||
import {
|
||||
useId,
|
||||
Button,
|
||||
FieldProps,
|
||||
useToastController,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastIntent,
|
||||
Toaster
|
||||
} 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";
|
||||
|
||||
|
||||
const Connection = (props: Partial<FieldProps>) => {
|
||||
|
||||
const toasterId = useId("toaster");
|
||||
const {dispatchToast} = useToastController(toasterId);
|
||||
const navigate = useNavigate();
|
||||
const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
|
||||
name: '',
|
||||
endpoint: '',
|
||||
access: '',
|
||||
key: ''
|
||||
})
|
||||
|
||||
function test() {
|
||||
const val = JSON.stringify(value);
|
||||
console.log('[DEBUG] connection.test: value =', val)
|
||||
Dial<string>("/api/connection/test", value).then(res => {
|
||||
if (res.status === 200) {
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>连接成功!</ToastTitle>
|
||||
</Toast>,
|
||||
{position: "top-end", intent: "success"}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return <div className='connection-container'>
|
||||
<div className='connection-form'>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="name"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name} onChange={(e) => {
|
||||
setValue({...value, name: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="endpoint"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='地址 (example: https://ip_or_server-name:port)' value={value.endpoint}
|
||||
onChange={(e) => {
|
||||
setValue({...value, endpoint: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret access"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='' value={value.access} onChange={(e) => {
|
||||
setValue({...value, access: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret key"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='' value={value.key} onChange={(e) => {
|
||||
setValue({...value, key: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field connection-form-field-actions'>
|
||||
<Button appearance='transparent' 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster toasterId={toasterId}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Connection;
|
||||
@@ -1,50 +0,0 @@
|
||||
div.container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
div.header {
|
||||
height: 50px;
|
||||
border-bottom: 1px solid lightgray;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
div.body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.body div.body-connections {
|
||||
width: 200px;
|
||||
border-right: 1px solid lightgray;
|
||||
}
|
||||
|
||||
div.body-connections-search {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
input.body-connections-search-input {
|
||||
border: none;
|
||||
border-bottom: 1px solid lightgray;
|
||||
width: calc(100% - 4px);
|
||||
height: 30px;
|
||||
outline: none;
|
||||
text-indent: 5px;
|
||||
}
|
||||
div.body-connections-search-dismiss{
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
border-bottom: 1px solid lightgray;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import {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";
|
||||
|
||||
function Home() {
|
||||
const [connectionFilterKeywords, setConnectionFilterKeywords] = useState<string>('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<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 />
|
||||
</div>
|
||||
</div>
|
||||
<div className="body-connections-list"></div>
|
||||
</div>
|
||||
<div className="body-content"></div>
|
||||
</div>
|
||||
<div className="footer"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -0,0 +1,55 @@
|
||||
import {create} from 'zustand'
|
||||
import {Bucket, Connection} from "../interfaces/connection";
|
||||
import {Dial, Resp} from "../api";
|
||||
|
||||
interface StoreBucket {
|
||||
bucket_active: Bucket | null;
|
||||
bucket_set: (Bucket: Bucket | null) => Promise<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[] };
|
||||
|
||||
export const useStoreBucket = create<StoreBucket>()((set) => ({
|
||||
bucket_active: null,
|
||||
bucket_set: async (bucket: Bucket | null) => {
|
||||
set({bucket_active: bucket});
|
||||
},
|
||||
bucket_list: [],
|
||||
bucket_get: async (conn: Connection, refresh: boolean) => {
|
||||
let res: Resp<{ list: Bucket[]; }>;
|
||||
if (refresh) {
|
||||
res = await Dial<{ list: Bucket[] }>('/api/connection/buckets', {id: conn.id});
|
||||
if (res.status !== 200) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
if (refresh) {
|
||||
bucket_map = {...bucket_map, [conn.id]: res.data.list}
|
||||
return {bucket_list: res.data.list};
|
||||
}
|
||||
|
||||
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}]}
|
||||
})
|
||||
}
|
||||
}))
|
||||
@@ -0,0 +1,38 @@
|
||||
import {create} from 'zustand'
|
||||
import {Connection} from "../interfaces/connection";
|
||||
import {Dial} from "../api";
|
||||
|
||||
interface StoreConnection {
|
||||
conn_active: Connection | null;
|
||||
conn_list: Connection[];
|
||||
conn_get: () => Promise<void>;
|
||||
conn_set: (connection: Connection) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useStoreConnection = create<StoreConnection>()((set) => ({
|
||||
conn_active: null,
|
||||
conn_list: [],
|
||||
conn_get: async () => {
|
||||
const res = await Dial<{ list: Connection[] }>('/api/connection/list');
|
||||
if (res.status !== 200) {
|
||||
return
|
||||
}
|
||||
|
||||
set({conn_list: res.data.list})
|
||||
},
|
||||
|
||||
conn_set: async (connection: Connection) => {
|
||||
set((state) => {
|
||||
return {
|
||||
conn_active: connection.active? connection: null,
|
||||
conn_list: state.conn_list.map(item => {
|
||||
if (item.id === connection.id) {
|
||||
return connection
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
@@ -0,0 +1,53 @@
|
||||
import {create} from 'zustand'
|
||||
import {Bucket, Connection, S3File} from "../interfaces/connection";
|
||||
import {Dial} from "../api";
|
||||
|
||||
interface StoreFile {
|
||||
file_active: string | null,
|
||||
file_set: (key: string) => Promise<void>,
|
||||
files_list: S3File[];
|
||||
files_get: (conn: Connection, bucket: Bucket, prefix?: string, filter?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useStoreFile = create<StoreFile>()((set) => ({
|
||||
file_active: null,
|
||||
file_set: async (key: string) => {
|
||||
set((state) => {
|
||||
return {file_active: key}
|
||||
})
|
||||
},
|
||||
files_list: [],
|
||||
files_get: async (conn: Connection, bucket: Bucket, prefix = '', filter = '') => {
|
||||
const res = await Dial<{ list: S3File[] }>('/api/bucket/files', {
|
||||
conn_id: conn.id,
|
||||
bucket: bucket.name,
|
||||
prefix: prefix + filter,
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
return
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
return {files_list: res.data.list}
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
interface StoreFileFilter {
|
||||
prefix: string;
|
||||
filter: string;
|
||||
prefix_set: (prefix: string) => Promise<void>;
|
||||
filter_set: (filter: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useStoreFileFilter = create<StoreFileFilter>()((set) => ({
|
||||
prefix: '',
|
||||
filter: '',
|
||||
prefix_set: async (keyword: string) => {
|
||||
set(state => {return {prefix: keyword}})
|
||||
},
|
||||
filter_set: async (keyword: string) => {
|
||||
set(state => {return {filter: keyword}})
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,33 @@
|
||||
import {create} from 'zustand'
|
||||
import {Dial} from "../api";
|
||||
import {Bucket, Connection} from "../interfaces/connection";
|
||||
|
||||
interface StorePreview {
|
||||
preview_url: string;
|
||||
preview_content_type: string;
|
||||
preview_get: (conn:Connection,bucket: Bucket,key: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useStorePreview = create<StorePreview>()((set) => ({
|
||||
preview_url: '',
|
||||
preview_content_type: '',
|
||||
preview_get: async (conn: Connection, bucket: Bucket,key: string) => {
|
||||
if (key === '') {
|
||||
return set(()=>{return {preview_url: ''}})
|
||||
}
|
||||
|
||||
let res = await Dial<{url:string,method:string}>('/api/file/get', {
|
||||
conn_id: conn.id,
|
||||
bucket: bucket.name,
|
||||
key: key
|
||||
})
|
||||
|
||||
if(res.status!=200) {
|
||||
return set(()=>{return {preview_url: ''}})
|
||||
}
|
||||
|
||||
set(()=>{
|
||||
return {preview_url:res.data.url}
|
||||
})
|
||||
},
|
||||
}))
|
||||
@@ -1,3 +1,6 @@
|
||||
:root {
|
||||
font-size: 10px;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
|
||||
Regular → Executable
@@ -1,43 +1,78 @@
|
||||
module nf-disk
|
||||
module github.com/loveuer/nf-disk
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.31.0
|
||||
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/labstack/gommon v0.4.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/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
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect
|
||||
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/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
|
||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
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
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U=
|
||||
github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 h1:xDAuZTn4IMm8o1LnBZvmrL8JA1io4o3YWNXgohbf20g=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5/go.mod h1:wYSv6iDS621sEFLfKvpPE2ugjTuGlAG7iROg0hLOkfc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.38 h1:mMVyJJuSUdbD4zKXoxDgWrgM60QwlFEg+JhihCq6wCw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.38/go.mod h1:6xOiNEn58bj/64MPKx89r6G/el9JZn8pvVbquSqTKK4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.36 h1:zwI5WrT+oWWfzSKoTNmSyeBKQhsFRJRv+PGW/UZW+Yk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.36/go.mod h1:3AG/sY1rc9NJrNWcN/3KPU4SIDPGTrd/qegKB0TnFdE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 h1:OWYvKL53l1rbsUmW7bQyJVsYU/Ii3bbAAQIIFNbM0Tk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18/go.mod h1:CUx0G1v3wG6l01tUB+j7Y8kclA8NSqK4ef0YG79a4cg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 h1:rTWjG6AvWekO2B1LHeM3ktU7MqyX9rzWQ7hgzneZW7E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20/go.mod h1:RGW2DDpVc8hu6Y6yG8G5CHVmVOAn1oV8rNKOHRJyswg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.2 h1:1iXmXy8SJzQVMGvo40TSzBYS9ig6BSyXfRIMzLfmBfE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.2/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.23.2 h1:yzi/y/vKlLyzOfG7pSu5ONNGRxHIgLeDrV4w2AMRCo0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.23.2/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.2 h1:3gb6pYhYLjo8rB1h2Tqs61wpjRd3rQymYcVq/pp0yxI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.2/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.31.2 h1:O6tyji8mXmBGsHvTCB0VIhrDw19lGTUSbKIyjnw79s8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.31.2/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI=
|
||||
github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA=
|
||||
github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -7,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=
|
||||
@@ -28,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=
|
||||
@@ -39,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=
|
||||
@@ -67,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=
|
||||
@@ -84,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=
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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("/runtime/dialog/open", handler.DialogOpen(ctx))
|
||||
register("/runtime/dialog/save", handler.DialogSave(ctx))
|
||||
register("/api/connection/test", handler.ConnectionTest)
|
||||
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)
|
||||
register("/api/bucket/files", handler.BucketFiles)
|
||||
register("/api/bucket/create", handler.BucketCreate)
|
||||
register("/api/file/upload", handler.FileUpload)
|
||||
register("/api/file/info", handler.FileInfo)
|
||||
register("/api/file/get", handler.FileGet)
|
||||
register("/api/file/download", handler.FileDownload)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+25
-26
@@ -1,13 +1,19 @@
|
||||
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/manager"
|
||||
"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"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
app *App
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -15,31 +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) 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(manager.Init(ctx))
|
||||
tool.Must(api.Init(ctx))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf-disk/internal/manager"
|
||||
"github.com/loveuer/nf-disk/internal/s3"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
)
|
||||
|
||||
func BucketFiles(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
ConnId uint64 `json:"conn_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
list []*s3.ListFileRes
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(err.Error())
|
||||
}
|
||||
|
||||
if req.ConnId == 0 || req.Bucket == "" {
|
||||
return c.Send400(req, "缺少参数")
|
||||
}
|
||||
|
||||
if _, client, err = manager.Manager.Use(req.ConnId); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
if list, err = client.ListFile(c.Context(), req.Bucket, req.Prefix); err != nil {
|
||||
return c.Send500(err.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})
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"nf-disk/internal/ndh"
|
||||
"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/samber/lo"
|
||||
)
|
||||
|
||||
func ConnectionTest(c *ndh.Ctx) error {
|
||||
@@ -21,5 +27,187 @@ func ConnectionTest(c *ndh.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Send200("test success")
|
||||
if req.Endpoint == "" || req.Access == "" || req.Key == "" {
|
||||
return c.Send400(nil, "endpoint, secret_access, secret_key 是必填项")
|
||||
}
|
||||
|
||||
if _, err = s3.New(c.Context(), req.Endpoint, req.Access, req.Key); err != nil {
|
||||
return c.Send500(err.Error(), "连接失败")
|
||||
}
|
||||
|
||||
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.Manager.Register(connection, client); err != nil {
|
||||
return c.Send500(err.Error(), "创建连接失败(2)")
|
||||
}
|
||||
|
||||
return c.Send200(connection, "创建连接成功")
|
||||
}
|
||||
|
||||
func ConnectionList(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
list = make([]*model.Connection, 0)
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(nil, "参数错误")
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Model(&model.Connection{}).
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
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})
|
||||
}
|
||||
|
||||
func ConnectionConnect(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
)
|
||||
|
||||
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 client, err = s3.New(c.Context(), conn.Endpoint, conn.Access, conn.Key); err != nil {
|
||||
return c.Send500(err.Error(), "连接失败")
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
return c.Send200(conn)
|
||||
}
|
||||
|
||||
func ConnectionBuckets(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
buckets []*s3.ListBucketRes
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(nil, "参数错误")
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"github.com/samber/lo"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
func DialogSave(ctx context.Context) ndh.Handler {
|
||||
return func(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Title string `json:"title"`
|
||||
Filters []string `json:"filters"`
|
||||
DefaultDirectory string `json:"default_directory"`
|
||||
DefaultFilename string `json:"default_filename"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
opt = runtime.SaveDialogOptions{
|
||||
Title: "将文件保存到",
|
||||
}
|
||||
result any
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(err.Error())
|
||||
}
|
||||
|
||||
if req.Title != "" {
|
||||
opt.Title = req.Title
|
||||
}
|
||||
|
||||
if req.DefaultFilename != "" {
|
||||
opt.DefaultFilename = req.DefaultFilename
|
||||
}
|
||||
|
||||
if req.DefaultDirectory != "" {
|
||||
opt.DefaultDirectory = req.DefaultDirectory
|
||||
}
|
||||
|
||||
if opt.DefaultDirectory == "" {
|
||||
var home string
|
||||
if home, err = os.UserHomeDir(); err != nil {
|
||||
opt.DefaultDirectory = filepath.Join(home, "Downloads")
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Filters) > 0 {
|
||||
opt.Filters = lo.Map(req.Filters, func(item string, index int) runtime.FileFilter {
|
||||
return runtime.FileFilter{Pattern: item}
|
||||
})
|
||||
}
|
||||
|
||||
if result, err = runtime.SaveFileDialog(ctx, opt); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
return c.Send200(map[string]interface{}{"result": result})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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)
|
||||
}
|
||||
|
||||
func FileInfo(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
ConnId uint64 `json:"conn_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
info *s3.ObjectInfo
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(err.Error())
|
||||
}
|
||||
|
||||
if _, client, err = manager.Manager.Use(req.ConnId); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
if info, err = client.GetObjectInfo(c.Context(), req.Bucket, req.Key); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
return c.Send200(info)
|
||||
}
|
||||
|
||||
func FileGet(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
ConnId uint64 `json:"conn_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
link *s3.ObjectEntry
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(err.Error())
|
||||
}
|
||||
|
||||
if _, client, err = manager.Manager.Use(req.ConnId); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
if link, err = client.GetObjectEntry(c.Context(), req.Bucket, req.Key); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
return c.Send200(link)
|
||||
}
|
||||
|
||||
func FileDownload(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
ConnId uint64 `json:"conn_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
Key string `json:"key"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
obj *s3.ObjectEntity
|
||||
target *os.File
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(err.Error())
|
||||
}
|
||||
|
||||
if _, client, err = manager.Manager.Use(req.ConnId); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
if obj, err = client.GetObject(c.Context(), req.Bucket, req.Key); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
if target, err = os.OpenFile(filepath.Clean(req.Location), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
if _, err = io.Copy(target, obj.Body); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
return c.Send200(req)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package manager
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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 {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf-disk/internal/opt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func Init(tx *gorm.DB) (err error) {
|
||||
err = tx.AutoMigrate(
|
||||
&Connection{},
|
||||
)
|
||||
|
||||
if opt.Debug {
|
||||
err = tx.Create([]*Connection{
|
||||
{
|
||||
Name: "dev-minio",
|
||||
Endpoint: "http://10.220.10.15:9000",
|
||||
Access: "8ALV3DUZI31YG4BDRJ0Z",
|
||||
Key: "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H",
|
||||
},
|
||||
{
|
||||
Name: "test",
|
||||
Endpoint: "http://10.220.10.14:19000",
|
||||
Access: "5VCR05L4BSGNCTCD8DXP",
|
||||
Key: "FPTMYBEiHhWLJ05C3aGXW8bjFXXNmghc8Za3Fo2u",
|
||||
},
|
||||
}).Clauses(clause.OnConflict{
|
||||
DoNothing: true,
|
||||
}).Error
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"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"`
|
||||
|
||||
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
|
||||
}
|
||||
+7
-1
@@ -1,5 +1,11 @@
|
||||
package opt
|
||||
|
||||
const (
|
||||
Msg200 = "操作成功"
|
||||
Msg400 = "输入不正确"
|
||||
Msg500 = "发生错误"
|
||||
)
|
||||
|
||||
var (
|
||||
Debug bool
|
||||
Debug bool = false
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/labstack/gommon/log"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ObjectInfo struct {
|
||||
Bucket string `json:"bucket"`
|
||||
Key string `json:"key"`
|
||||
ContentType string `json:"content_type"`
|
||||
Expire int64 `json:"expire"`
|
||||
}
|
||||
|
||||
func (c *Client) GetObjectInfo(ctx context.Context, bucket string, key string) (*ObjectInfo, error) {
|
||||
var (
|
||||
err error
|
||||
input = &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
}
|
||||
output *s3.HeadObjectOutput
|
||||
)
|
||||
|
||||
if output, err = c.client.HeadObject(ctx, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: aws.ToString(output.ContentType),
|
||||
Expire: aws.ToTime(output.Expires).UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetObject
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html
|
||||
|
||||
type Presigner struct {
|
||||
PresignClient *s3.PresignClient
|
||||
}
|
||||
|
||||
func (presigner *Presigner) GetObject(ctx context.Context, bucketName string, objectKey string, lifetimeSecs int64) (*v4.PresignedHTTPRequest, error) {
|
||||
request, err := presigner.PresignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
}, func(opts *s3.PresignOptions) {
|
||||
opts.Expires = time.Duration(lifetimeSecs * int64(time.Second))
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Presigner: couldn't get a presigned request to get %v:%v. Here's why: %v\n",
|
||||
bucketName, objectKey, err)
|
||||
}
|
||||
return request, err
|
||||
}
|
||||
|
||||
type ObjectEntry struct {
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Header http.Header `json:"header"`
|
||||
}
|
||||
|
||||
func (c *Client) GetObjectEntry(ctx context.Context, bucket string, key string, lifetimes ...int64) (*ObjectEntry, error) {
|
||||
var (
|
||||
err error
|
||||
lifetime int64 = 5 * 60
|
||||
pc = &Presigner{PresignClient: s3.NewPresignClient(c.client)}
|
||||
output *v4.PresignedHTTPRequest
|
||||
)
|
||||
|
||||
if len(lifetimes) > 0 && lifetimes[0] > 0 {
|
||||
lifetime = lifetimes[0]
|
||||
}
|
||||
|
||||
if output, err = pc.GetObject(ctx, bucket, key, lifetime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ObjectEntry{
|
||||
URL: output.URL,
|
||||
Method: output.Method,
|
||||
Header: output.SignedHeader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ObjectEntity struct {
|
||||
ObjectInfo
|
||||
Body io.ReadCloser `json:"body"`
|
||||
}
|
||||
|
||||
func (c *Client) GetObject(ctx context.Context, bucket string, key string) (*ObjectEntity, error) {
|
||||
var (
|
||||
err error
|
||||
input = &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
}
|
||||
output *s3.GetObjectOutput
|
||||
)
|
||||
|
||||
if output, err = c.client.GetObject(ctx, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ObjectEntity{
|
||||
ObjectInfo: ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: aws.ToString(output.ContentType),
|
||||
Expire: aws.ToTime(output.Expires).UnixMilli(),
|
||||
},
|
||||
Body: output.Body,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ListBucketRes struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ListFileType int64
|
||||
|
||||
const (
|
||||
ListFileTypeFile ListFileType = iota
|
||||
ListFileTypeDir
|
||||
)
|
||||
|
||||
type ListFileRes struct {
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
LastModified time.Time `json:"last_modified"`
|
||||
Size int64 `json:"size"`
|
||||
Type ListFileType `json:"type"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *Client) ListFile(ctx context.Context, bucket string, prefix string) ([]*ListFileRes, error) {
|
||||
var (
|
||||
err error
|
||||
input = &s3.ListObjectsV2Input{
|
||||
Delimiter: aws.String("/"),
|
||||
MaxKeys: aws.Int32(1000),
|
||||
Bucket: aws.String(bucket),
|
||||
}
|
||||
output *s3.ListObjectsV2Output
|
||||
)
|
||||
|
||||
if prefix != "" {
|
||||
input.Prefix = aws.String(prefix)
|
||||
}
|
||||
|
||||
if output, err = c.client.ListObjectsV2(ctx, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folder := lo.FilterMap(
|
||||
output.CommonPrefixes,
|
||||
func(item types.CommonPrefix, index int) (*ListFileRes, bool) {
|
||||
name := strings.TrimPrefix(*item.Prefix, prefix)
|
||||
return &ListFileRes{
|
||||
Name: name,
|
||||
Key: *item.Prefix,
|
||||
Type: ListFileTypeDir,
|
||||
}, name != ""
|
||||
},
|
||||
)
|
||||
|
||||
list := lo.Map(
|
||||
output.Contents,
|
||||
func(item types.Object, index int) *ListFileRes {
|
||||
return &ListFileRes{
|
||||
Key: strings.Clone(*item.Key),
|
||||
Name: strings.TrimPrefix(*item.Key, prefix),
|
||||
LastModified: *item.LastModified,
|
||||
Size: *item.Size,
|
||||
Type: ListFileTypeFile,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return append(folder, list...), nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"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"
|
||||
)
|
||||
|
||||
type resolverV2 struct{}
|
||||
|
||||
func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (smithyendpoints.Endpoint, error) {
|
||||
u, err := url.Parse(*params.Endpoint)
|
||||
if err != nil {
|
||||
log.Warn("resolver v2: parse url = %s, err = %s", params.Endpoint, err.Error())
|
||||
return smithyendpoints.Endpoint{}, err
|
||||
}
|
||||
return smithyendpoints.Endpoint{
|
||||
URI: *u,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
client *s3.Client
|
||||
}
|
||||
|
||||
func New(ctx context.Context, endpoint string, access string, key string) (*Client, error) {
|
||||
var (
|
||||
err error
|
||||
sdkConfig aws.Config
|
||||
output *s3.ListBucketsOutput
|
||||
)
|
||||
|
||||
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
return aws.Endpoint{
|
||||
URL: endpoint,
|
||||
}, nil
|
||||
})
|
||||
|
||||
if sdkConfig, err = config.LoadDefaultConfig(
|
||||
ctx,
|
||||
config.WithEndpointResolverWithOptions(customResolver),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
|
||||
//o.BaseEndpoint = aws.String(endpoint)
|
||||
//o.EndpointResolverV2 = &resolverV2{}
|
||||
o.Credentials = aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(access, key, ""))
|
||||
o.UsePathStyle = true
|
||||
o.Region = "auto"
|
||||
})
|
||||
|
||||
if output, err = s3Client.ListBuckets(tool.Timeout(5), &s3.ListBucketsInput{
|
||||
MaxBuckets: aws.Int32(2),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range output.Buckets {
|
||||
log.Debug("s3.New: list bucket name = %s", *item.Name)
|
||||
}
|
||||
|
||||
return &Client{client: s3Client}, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/internal/tool"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
_, err := New(context.TODO(), "http://10.220.10.15:9000/", "8ALV3DUZI31YG4BDRJ0Z", "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H")
|
||||
if err != nil {
|
||||
t.Fatalf("call s3.New err = %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFile(t *testing.T) {
|
||||
//log.SetLogLevel(log.LogLevelDebug)
|
||||
|
||||
//cli, err := New(context.TODO(), "http://10.220.10.14:19000", "5VCR05L4BSGNCTCD8DXP", "FPTMYBEiHhWLJ05C3aGXW8bjFXXNmghc8Za3Fo2u")
|
||||
cli, err := New(context.TODO(), "http://10.220.10.15:9000/", "8ALV3DUZI31YG4BDRJ0Z", "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H")
|
||||
if err != nil {
|
||||
t.Fatalf("call s3.New err = %s", err.Error())
|
||||
}
|
||||
|
||||
files, err := cli.ListFile(tool.Timeout(30), "topic-audit", "")
|
||||
if err != nil {
|
||||
t.Fatalf("call s3.ListFile err = %s", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("[x] file length = %d", len(files))
|
||||
|
||||
for _, item := range files {
|
||||
t.Logf("[x: %d] file = %s, size = %d", item.Type, item.Name, item.Size)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"github.com/loveuer/nf/nft/nfctl/opt"
|
||||
"nf-disk/internal/controller"
|
||||
"github.com/loveuer/nf-disk/internal/controller"
|
||||
"github.com/loveuer/nf-disk/internal/opt"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
@@ -15,17 +18,18 @@ import (
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&opt.Debug, "debug", false, "debug mode")
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer cancel()
|
||||
|
||||
flag.BoolVar(&opt.Debug, "debug", true, "debug mode")
|
||||
flag.Parse()
|
||||
|
||||
if opt.Debug {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := controller.NewApp()
|
||||
app := controller.NewApp(ctx)
|
||||
|
||||
if err := wails.Run(&options.App{
|
||||
Title: "nf-disk",
|
||||
|
||||
@@ -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,
|
||||
@@ -20,12 +20,16 @@ func NewCtx(ctx context.Context, req *json.Decoder, res io.Writer) *Ctx {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Ctx) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *Ctx) Write(bs []byte) (int, error) {
|
||||
return c.res.Write(bs)
|
||||
}
|
||||
|
||||
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 {
|
||||
Generated
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "nf-disk",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"zustand": "^5.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"zustand": "^5.0.0-rc.2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user