Compare commits
8
Commits
master
..
3c8c559ac7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c8c559ac7 | ||
|
|
468759a437 | ||
|
|
bea30130f1 | ||
|
|
5e0885f22d | ||
|
|
b2c13508f4 | ||
|
|
77dff6649d | ||
|
|
65f702c6e9 | ||
|
|
fcb2a349e7 |
@@ -1,39 +0,0 @@
|
|||||||
name: Auto Build Windows
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-job:
|
|
||||||
runs-on: windows-latest
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
repository-projects: write
|
|
||||||
steps:
|
|
||||||
- name: install node
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: install golang
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
|
|
||||||
- name: install wails
|
|
||||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
run: wails build -ldflags='-s -w'
|
|
||||||
|
|
||||||
- name: create release
|
|
||||||
id: create_release
|
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
|
||||||
with:
|
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
title: "Release_Windows_${{ github.ref_name }}"
|
|
||||||
files: |
|
|
||||||
build/bin/nf-disk.exe
|
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
<title>nf-disk</title>
|
<title>nf-disk</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" style="height: 100%"></div>
|
<div id="root"></div>
|
||||||
<script src="./src/main.tsx" type="module"></script>
|
<script src="./src/main.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
-45
@@ -11,15 +11,12 @@
|
|||||||
"@fluentui/react-components": "^9.54.16",
|
"@fluentui/react-components": "^9.54.16",
|
||||||
"@fluentui/react-icons": "^2.0.258",
|
"@fluentui/react-icons": "^2.0.258",
|
||||||
"jotai": "^2.10.0",
|
"jotai": "^2.10.0",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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"
|
"zustand": "^5.0.0-rc.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4.17.10",
|
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react": "^2.0.1",
|
"@vitejs/plugin-react": "^2.0.1",
|
||||||
@@ -2375,13 +2372,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.13",
|
"version": "15.7.13",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.13.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||||
@@ -3070,12 +3060,6 @@
|
|||||||
"integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==",
|
"integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -3466,18 +3450,6 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"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": {
|
"node_modules/use-disposable": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/use-disposable/-/use-disposable-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/use-disposable/-/use-disposable-1.0.2.tgz",
|
||||||
@@ -5273,12 +5245,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.13",
|
"version": "15.7.13",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.13.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||||
@@ -5663,11 +5629,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/keyborg/-/keyborg-2.6.0.tgz",
|
"resolved": "https://registry.npmmirror.com/keyborg/-/keyborg-2.6.0.tgz",
|
||||||
"integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA=="
|
"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": {
|
"loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -5928,12 +5889,6 @@
|
|||||||
"picocolors": "^1.0.1"
|
"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": {
|
"use-disposable": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/use-disposable/-/use-disposable-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/use-disposable/-/use-disposable-1.0.2.tgz",
|
||||||
|
|||||||
@@ -12,15 +12,12 @@
|
|||||||
"@fluentui/react-components": "^9.54.16",
|
"@fluentui/react-components": "^9.54.16",
|
||||||
"@fluentui/react-icons": "^2.0.258",
|
"@fluentui/react-icons": "^2.0.258",
|
||||||
"jotai": "^2.10.0",
|
"jotai": "^2.10.0",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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"
|
"zustand": "^5.0.0-rc.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4.17.10",
|
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react": "^2.0.1",
|
"@vitejs/plugin-react": "^2.0.1",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
674272f31b23a798d77a17321c6a8785
|
f23304e575da740e9b738508a43df31e
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
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,28 @@
|
|||||||
|
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,273 +0,0 @@
|
|||||||
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
|
|
||||||
onClick={() => {
|
|
||||||
dispatchMessage("暂未实现", "warning");
|
|
||||||
}}
|
|
||||||
icon={<SettingsRegular />}
|
|
||||||
>
|
|
||||||
设置
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
dispatchMessage("暂未实现", "warning");
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,7 @@ import {
|
|||||||
DialogBody,
|
DialogBody,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
Button,
|
Button, Spinner, Field, Input, FieldProps, makeStyles, tokens,
|
||||||
Spinner,
|
|
||||||
Field,
|
|
||||||
Input,
|
|
||||||
makeStyles,
|
|
||||||
tokens,
|
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
|
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
|
||||||
@@ -27,20 +22,14 @@ const useActionStyle = makeStyles({
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
gridColumnStart: 0,
|
gridColumnStart: 0,
|
||||||
},
|
},
|
||||||
test: {},
|
test: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ConnectionCreateProps {
|
export function ConnectionCreate(){
|
||||||
openFn: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConnectionCreate(props: ConnectionCreateProps) {
|
|
||||||
const actionStyle = useActionStyle();
|
const actionStyle = useActionStyle();
|
||||||
const {dispatchMessage} = useToast();
|
const {dispatchMessage} = useToast();
|
||||||
const [testLoading, setTestLoading] = useState<
|
const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial");
|
||||||
"initial" | "loading" | "success" | "error"
|
const {conn_list, get_conn} = useStoreConnection();
|
||||||
>("initial");
|
|
||||||
const { conn_get } = useStoreConnection();
|
|
||||||
const buttonIcon =
|
const buttonIcon =
|
||||||
testLoading === "loading" ? (
|
testLoading === "loading" ? (
|
||||||
<Spinner size="tiny"/>
|
<Spinner size="tiny"/>
|
||||||
@@ -49,117 +38,99 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
|
|||||||
) : testLoading === "error" ? (
|
) : testLoading === "error" ? (
|
||||||
<DismissRegular/>
|
<DismissRegular/>
|
||||||
) : null;
|
) : null;
|
||||||
const [value, setValue] = useState<{
|
const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
|
||||||
name: string;
|
name: '',
|
||||||
endpoint: string;
|
endpoint: '',
|
||||||
access: string;
|
access: '',
|
||||||
key: string;
|
key: ''
|
||||||
}>({
|
})
|
||||||
name: "",
|
|
||||||
endpoint: "",
|
|
||||||
access: "",
|
|
||||||
key: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
async function test() {
|
async function test() {
|
||||||
setTestLoading("loading");
|
setTestLoading("loading")
|
||||||
let res = await Dial<string>("/api/connection/test", value);
|
let res = await Dial<string>("/api/connection/test", value)
|
||||||
const status = res.status === 200 ? "success" : "error";
|
const status = res.status === 200 ? "success" : "error"
|
||||||
setTestLoading(status);
|
setTestLoading(status);
|
||||||
dispatchMessage(res.msg, status);
|
dispatchMessage(res.msg, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
let res = await Dial("/api/connection/create", value);
|
// qUvfW8xpOTc23O96
|
||||||
|
// eTcuc8BebHPVpZZwIaNmzfwxRxPYGfTj
|
||||||
|
let res = await Dial("/api/connection/create", value)
|
||||||
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
|
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
dispatchMessage("新建连接成功", "success");
|
get_conn()
|
||||||
await conn_get();
|
|
||||||
props.openFn(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<>
|
|
||||||
<DialogSurface>
|
<DialogSurface>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<DialogTitle>新建S3连接</DialogTitle>
|
<DialogTitle>新建S3连接</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="connection-container">
|
<div className='connection-container'>
|
||||||
<div className="connection-form">
|
<div className='connection-form'>
|
||||||
<div className="connection-form-field">
|
<div className='connection-form-field'>
|
||||||
<Field label="name">
|
<Field
|
||||||
<Input
|
label="name"
|
||||||
placeholder="名称 (example: 测试S3-minio)"
|
validationState="success"
|
||||||
value={value.name}
|
validationMessage="This is a success message."
|
||||||
|
>
|
||||||
|
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValue({...value, name: e.target.value});
|
setValue({...value, name: e.target.value});
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="connection-form-field">
|
<div className='connection-form-field'>
|
||||||
<Field label="endpoint" required>
|
<Field
|
||||||
<Input
|
label="endpoint"
|
||||||
placeholder="地址 (example: https://ip_or_server-name:port)"
|
validationState="success"
|
||||||
|
validationMessage="This is a success message."
|
||||||
|
>
|
||||||
|
<Input placeholder='地址 (example: https://ip_or_server-name:port)'
|
||||||
value={value.endpoint}
|
value={value.endpoint}
|
||||||
required
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValue({...value, endpoint: e.target.value});
|
setValue({...value, endpoint: e.target.value});
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="connection-form-field">
|
<div className='connection-form-field'>
|
||||||
<Field label="secret access" required>
|
<Field
|
||||||
<Input
|
label="secret access"
|
||||||
placeholder=""
|
validationState="success"
|
||||||
required
|
validationMessage="This is a success message."
|
||||||
value={value.access}
|
>
|
||||||
onChange={(e) => {
|
<Input placeholder='' value={value.access} onChange={(e) => {
|
||||||
setValue({...value, access: e.target.value});
|
setValue({...value, access: e.target.value});
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="connection-form-field">
|
<div className='connection-form-field'>
|
||||||
<Field label="secret key" required>
|
<Field
|
||||||
<Input
|
label="secret key"
|
||||||
placeholder=""
|
validationState="success"
|
||||||
required
|
validationMessage="This is a success message."
|
||||||
value={value.key}
|
>
|
||||||
onChange={(e) => {
|
<Input placeholder='' value={value.key} onChange={(e) => {
|
||||||
setValue({...value, key: e.target.value});
|
setValue({...value, key: e.target.value});
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions className={actionStyle.container}>
|
<DialogActions className={actionStyle.container}>
|
||||||
<Button
|
<Button className={actionStyle.test} appearance='transparent' icon={buttonIcon}
|
||||||
className={actionStyle.test}
|
onClick={async () => await test()}>测试连接</Button>
|
||||||
appearance="transparent"
|
|
||||||
icon={buttonIcon}
|
|
||||||
onClick={async () => await test()}
|
|
||||||
>
|
|
||||||
测试连接
|
|
||||||
</Button>
|
|
||||||
<DialogTrigger disableButtonEnhancement>
|
<DialogTrigger disableButtonEnhancement>
|
||||||
<Button appearance="secondary">取消</Button>
|
<Button appearance="secondary">取消</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<Button
|
<Button onClick={async () => {
|
||||||
onClick={async () => {
|
await create()
|
||||||
await create();
|
}} appearance="primary">新建</Button>
|
||||||
}}
|
|
||||||
appearance="primary"
|
|
||||||
>
|
|
||||||
新建
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</>
|
</>
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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()
|
|
||||||
|
|
||||||
const closeFn = () => {
|
|
||||||
set_preview({url: '', content_type: ''})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={styles.content}>
|
|
||||||
<Path/>
|
|
||||||
{
|
|
||||||
preview.url ? <PreviewFile url={preview.url} content_type={preview.content_type} close={closeFn}/> :
|
|
||||||
(
|
|
||||||
bucket_active ?
|
|
||||||
<ListFileComponent set_preview_fn={set_preview}/> : <ListBucketComponent/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
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",
|
|
||||||
lineHeight: "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 [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
|
|
||||||
onClick={() => {
|
|
||||||
dispatchMessage("敬请期待...", "warning");
|
|
||||||
}}
|
|
||||||
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 - 10rem)" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(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.toLowerCase()) {
|
|
||||||
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 />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function Footer() {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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,60 @@
|
|||||||
|
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%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body div.body-connections {
|
||||||
|
width: 200px;
|
||||||
|
border-right: 1px solid lightgray;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body-connections-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
div.body-connections-list-item.active {
|
||||||
|
color: var(--colorNeutralForeground2BrandSelected);
|
||||||
|
}
|
||||||
@@ -1,26 +1,152 @@
|
|||||||
import {Header} from "./header";
|
import {useEffect, useState} from 'react';
|
||||||
import {Body} from "./body";
|
import './home.css';
|
||||||
import {makeStyles} from "@fluentui/react-components";
|
import {
|
||||||
import {Footer} from "./footer";
|
Button, Dialog, DialogTrigger, makeStyles,mergeClasses, MenuItem, MenuList, tokens, Tooltip,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import {
|
||||||
|
CloudAddFilled, DismissRegular
|
||||||
|
} from "@fluentui/react-icons";
|
||||||
|
import {Dial} from "../../api";
|
||||||
|
import {useToast} from "../../message";
|
||||||
|
import {Connection} from "../../interfaces/connection";
|
||||||
|
import {ConnectionCreate} from "../connection/new";
|
||||||
|
import {useStoreConnection} from "../../store/connection";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useMenuListContainerStyles = makeStyles({
|
||||||
container: {
|
container: {
|
||||||
height: '100%',
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
width: '100%',
|
flex: 1,
|
||||||
display: 'flex',
|
width: "100%",
|
||||||
flexDirection: 'column',
|
paddingTop: "4px",
|
||||||
|
paddingBottom: "4px",
|
||||||
},
|
},
|
||||||
})
|
item: {
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
fontSize: '15px',
|
||||||
|
'& span': {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
'&.active': {
|
||||||
|
color: tokens.colorNeutralForeground2BrandHover,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
item_icon: {
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
"&:hover" : {
|
||||||
|
color: tokens.colorNeutralForeground2BrandHover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export function Home() {
|
|
||||||
|
|
||||||
const styles = useStyles()
|
function Home() {
|
||||||
|
const styles = useMenuListContainerStyles();
|
||||||
|
const {dispatchMessage} = useToast();
|
||||||
|
const [openCreate, setOpenCreate] = useState(false);
|
||||||
|
const [connectionFilterKeywords, setConnectionFilterKeywords] = useState<string>('');
|
||||||
|
const [connections, setConnections] = useState<Connection[]>([]);
|
||||||
|
const {conn_list, get_conn} = useStoreConnection();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
get_conn()
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleConnect(item: Connection) {
|
||||||
|
console.log('[DEBUG] db click item =', item)
|
||||||
|
for (const c of connections) {
|
||||||
|
if (item.id === c.id && c.active) {
|
||||||
|
console.log('[DEBUG] conn is already connected:', c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await Dial("/api/connection/connect", {id: item.id})
|
||||||
|
if (res.status === 200) {
|
||||||
|
dispatchMessage("连接成功", "success")
|
||||||
|
setConnections(connections.map(one => {
|
||||||
|
if (one.id === item.id) {
|
||||||
|
one.active = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return one
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDisconnect(item: Connection) {
|
||||||
|
let res = await Dial('/api/connection/disconnect', {id: item.id})
|
||||||
|
if (res.status === 200) {
|
||||||
|
setConnections(connections.map(c => {
|
||||||
|
if (item.id === c.id) {
|
||||||
|
c.active = false
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<Dialog
|
||||||
|
open={openCreate}
|
||||||
|
onOpenChange={(event, data) => setOpenCreate(data.open)}>
|
||||||
|
<DialogTrigger disableButtonEnhancement>
|
||||||
|
<Button appearance="primary" icon={<CloudAddFilled/>}>
|
||||||
|
新建连接
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<ConnectionCreate />
|
||||||
|
</Dialog>
|
||||||
|
</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 className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Header />
|
<MenuList>
|
||||||
<Body />
|
{conn_list.map(item => {
|
||||||
<Footer/>
|
return <MenuItem
|
||||||
|
onDoubleClick={async () => {
|
||||||
|
await handleConnect(item)
|
||||||
|
}}
|
||||||
|
className={item.active ? mergeClasses(styles.item, 'active') : styles.item}
|
||||||
|
key={item.id}>
|
||||||
|
{item.name}
|
||||||
|
<Tooltip content="断开连接" relationship="label">
|
||||||
|
<Button onClick={async () => {await handleDisconnect(item)}} size="small" className={styles.item_icon} icon={<DismissRegular />} />
|
||||||
|
</Tooltip>
|
||||||
|
</MenuItem>
|
||||||
|
})}
|
||||||
|
</MenuList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="body-content"></div>
|
||||||
|
</div>
|
||||||
|
<div className="footer"></div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Home
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import {Button, makeStyles,tokens, Text } from "@fluentui/react-components";
|
|
||||||
import { DismissRegular } from "@fluentui/react-icons";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
const useStyle = makeStyles({
|
|
||||||
container: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
width: '100vw',
|
|
||||||
maxWidth: '100vw',
|
|
||||||
height: '100vh',
|
|
||||||
maxHeight: '100vh',
|
|
||||||
zIndex: 100,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
background: tokens.colorNeutralBackground1,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
height: '4rem',
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
header_close_button: {
|
|
||||||
marginLeft: 'auto',
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: '100%',
|
|
||||||
height: '100%',
|
|
||||||
maxHeight: '100%',
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems:'center',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export function PreviewFile(props: { url: string, content_type: string, close: () => void }) {
|
|
||||||
const styles = useStyle()
|
|
||||||
|
|
||||||
const category = props.content_type.split('/')[0]
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('keyup', (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
props.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keyup', () => { })
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
switch (category) {
|
|
||||||
case "image":
|
|
||||||
return <div className={styles.container}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
appearance="transparent"
|
|
||||||
className={styles.header_close_button}
|
|
||||||
onClick={() => { props.close() }}>
|
|
||||||
<DismissRegular />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.body}>
|
|
||||||
<img src={props.url}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
default:
|
|
||||||
return <div className={styles.container}>
|
|
||||||
<Text>该文件无法预览</Text>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export function CanPreview(filename: string) {
|
|
||||||
const fs = filename.split(".")
|
|
||||||
switch (fs[fs.length - 1].toLowerCase()) {
|
|
||||||
case "jpg":
|
|
||||||
return "image/jpg"
|
|
||||||
case "jpeg":
|
|
||||||
return "image/jpg"
|
|
||||||
case "png":
|
|
||||||
return "image/png"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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(/.*[\/\\]/, '');
|
|
||||||
}
|
|
||||||
@@ -7,16 +7,3 @@ export interface Connection {
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
active: boolean;
|
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;
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import {createRoot} from 'react-dom/client'
|
|||||||
import './style.css'
|
import './style.css'
|
||||||
import {FluentProvider, webLightTheme} from '@fluentui/react-components';
|
import {FluentProvider, webLightTheme} from '@fluentui/react-components';
|
||||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||||
import {Home} from "./component/home/home";
|
import Home from "./component/home/home";
|
||||||
import {ToastProvider} from "./message";
|
import {ToastProvider} from "./message";
|
||||||
|
|
||||||
const container = document.getElementById('root')
|
const container = document.getElementById('root')
|
||||||
@@ -15,7 +15,7 @@ const router = createBrowserRouter([
|
|||||||
])
|
])
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<FluentProvider theme={webLightTheme} style={{height: '100%'}}>
|
<FluentProvider theme={webLightTheme}>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<RouterProvider router={router}/>
|
<RouterProvider router={router}/>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
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}]}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
@@ -2,37 +2,15 @@ import {create} from 'zustand'
|
|||||||
import {Connection} from "../interfaces/connection";
|
import {Connection} from "../interfaces/connection";
|
||||||
import {Dial} from "../api";
|
import {Dial} from "../api";
|
||||||
|
|
||||||
interface StoreConnection {
|
type StoreConnection = {
|
||||||
conn_active: Connection | null;
|
conn_list: Connection[]
|
||||||
conn_list: Connection[];
|
get_conn: () => void
|
||||||
conn_get: () => Promise<void>;
|
|
||||||
conn_set: (connection: Connection) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useStoreConnection = create<StoreConnection>()((set) => ({
|
export const useStoreConnection = create<StoreConnection>()((set) => ({
|
||||||
conn_active: null,
|
|
||||||
conn_list: [],
|
conn_list: [],
|
||||||
conn_get: async () => {
|
get_conn: async () => {
|
||||||
const res = await Dial<{list: Connection[]}>('/api/connection/list');
|
const res = await Dial<{list: Connection[]}>('/api/connection/list');
|
||||||
if (res.status !== 200) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
set({conn_list: res.data.list})
|
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
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}})
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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,6 +1,3 @@
|
|||||||
:root {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
+3
@@ -1,4 +1,7 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
import {context} from '../models';
|
||||||
|
|
||||||
|
export function Init(arg1:context.Context):Promise<void>;
|
||||||
|
|
||||||
export function Invoke(arg1:string,arg2:string):Promise<string>;
|
export function Invoke(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function Init(arg1) {
|
||||||
|
return window['go']['controller']['App']['Init'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function Invoke(arg1, arg2) {
|
export function Invoke(arg1, arg2) {
|
||||||
return window['go']['controller']['App']['Invoke'](arg1, arg2);
|
return window['go']['controller']['App']['Invoke'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/loveuer/nf-disk
|
module github.com/loveuer/nf-disk
|
||||||
|
|
||||||
go 1.22
|
go 1.21
|
||||||
|
|
||||||
toolchain go1.23.0
|
toolchain go1.23.0
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.36
|
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/aws-sdk-go-v2/service/s3 v1.63.2
|
||||||
github.com/aws/smithy-go v1.21.0
|
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/go-sqlite3 v1.0.2
|
||||||
github.com/loveuer/nf v0.2.11
|
github.com/loveuer/nf v0.2.11
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.18.4
|
github.com/ncruces/go-sqlite3/gormlite v0.18.4
|
||||||
@@ -50,6 +49,7 @@ require (
|
|||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/labstack/echo/v4 v4.10.2 // 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/go-ansi-parser v1.6.0 // indirect
|
||||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||||
|
|||||||
@@ -24,20 +24,12 @@ func Resolve(path string) (ndh.Handler, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Init(ctx context.Context) error {
|
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/test", handler.ConnectionTest)
|
||||||
register("/api/connection/create", handler.ConnectionCreate)
|
register("/api/connection/create", handler.ConnectionCreate)
|
||||||
register("/api/connection/list", handler.ConnectionList)
|
register("/api/connection/list", handler.ConnectionList)
|
||||||
register("/api/connection/connect", handler.ConnectionConnect)
|
register("/api/connection/connect", handler.ConnectionConnect)
|
||||||
register("/api/connection/disconnect", handler.ConnectionDisconnect)
|
register("/api/connection/disconnect", handler.ConnectionDisconnect)
|
||||||
register("/api/connection/buckets", handler.ConnectionBuckets)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-20
@@ -2,20 +2,13 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"github.com/loveuer/nf-disk/internal/api"
|
"github.com/loveuer/nf-disk/internal/api"
|
||||||
"github.com/loveuer/nf-disk/internal/db"
|
"github.com/loveuer/nf-disk/internal/db"
|
||||||
"github.com/loveuer/nf-disk/internal/manager"
|
"github.com/loveuer/nf-disk/internal/manager"
|
||||||
"github.com/loveuer/nf-disk/internal/model"
|
"github.com/loveuer/nf-disk/internal/model"
|
||||||
"github.com/loveuer/nf-disk/internal/opt"
|
|
||||||
"github.com/loveuer/nf-disk/internal/tool"
|
"github.com/loveuer/nf-disk/internal/tool"
|
||||||
"github.com/loveuer/nf-disk/ndh"
|
"github.com/loveuer/nf-disk/ndh"
|
||||||
"github.com/loveuer/nf/nft/log"
|
"github.com/loveuer/nf/nft/log"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
app *App
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
@@ -23,25 +16,23 @@ type App struct {
|
|||||||
handlers map[string]ndh.Handler
|
handlers map[string]ndh.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(gctx context.Context) *App {
|
func NewApp() *App {
|
||||||
app = &App{
|
return &App{
|
||||||
handlers: make(map[string]ndh.Handler),
|
handlers: make(map[string]ndh.Handler),
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-gctx.Done()
|
|
||||||
runtime.Quit(app.ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) {
|
func (a *App) Init(ctx context.Context) {
|
||||||
log.Info("app startup!!!")
|
log.Info("app init!!!")
|
||||||
|
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
tool.Must(opt.Init())
|
|
||||||
tool.Must(db.Init(ctx, fmt.Sprintf("sqlite::%s", opt.ConfigFile)))
|
tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
|
||||||
tool.Must(model.Init(db.Default.Session()))
|
tool.Must(model.Init(db.Default.Session()))
|
||||||
tool.Must(manager.Init(ctx))
|
tool.Must(manager.Init(ctx))
|
||||||
tool.Must(api.Init(ctx))
|
tool.Must(api.Init(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) Startup(ctx context.Context) {
|
||||||
|
log.Info("app startup!!!")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
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,113 +0,0 @@
|
|||||||
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})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
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,21 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "github.com/loveuer/nf-disk/ndh"
|
||||||
|
|
||||||
|
func ListItem(c *ndh.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Bucket string `json:"bucket"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
req = new(Req)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = c.ReqParse(req); err != nil {
|
||||||
|
return c.Send400(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("implement me!!!")
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/loveuer/nf-disk/internal/opt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(tx *gorm.DB) (err error) {
|
func Init(tx *gorm.DB) (err error) {
|
||||||
@@ -9,5 +11,24 @@ func Init(tx *gorm.DB) (err error) {
|
|||||||
&Connection{},
|
&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
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package opt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if ConfigDir, err = os.UserConfigDir(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.MkdirAll(filepath.Join(ConfigDir, "nf-disk"), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigFile = filepath.Join(ConfigDir, "nf-disk", "config.db")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,4 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
Debug bool = false
|
Debug bool = false
|
||||||
ConfigDir string
|
|
||||||
ConfigFile string
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
+2
-66
@@ -6,28 +6,11 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListBucketRes struct {
|
type ListBucketRes struct {
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64
|
||||||
Name string `json:"name"`
|
Name string
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
func (c *Client) ListBucket(ctx context.Context) ([]*ListBucketRes, error) {
|
||||||
@@ -52,50 +35,3 @@ func (c *Client) ListBucket(ctx context.Context) ([]*ListBucketRes, error) {
|
|||||||
|
|
||||||
return res, nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import (
|
|||||||
|
|
||||||
type resolverV2 struct{}
|
type resolverV2 struct{}
|
||||||
|
|
||||||
func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (smithyendpoints.Endpoint, error) {
|
func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (
|
||||||
|
smithyendpoints.Endpoint, error,
|
||||||
|
) {
|
||||||
u, err := url.Parse(*params.Endpoint)
|
u, err := url.Parse(*params.Endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("resolver v2: parse url = %s, err = %s", params.Endpoint, err.Error())
|
|
||||||
return smithyendpoints.Endpoint{}, err
|
return smithyendpoints.Endpoint{}, err
|
||||||
}
|
}
|
||||||
return smithyendpoints.Endpoint{
|
return smithyendpoints.Endpoint{
|
||||||
@@ -37,25 +38,14 @@ func New(ctx context.Context, endpoint string, access string, key string) (*Clie
|
|||||||
output *s3.ListBucketsOutput
|
output *s3.ListBucketsOutput
|
||||||
)
|
)
|
||||||
|
|
||||||
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
if sdkConfig, err = config.LoadDefaultConfig(ctx); err != nil {
|
||||||
return aws.Endpoint{
|
|
||||||
URL: endpoint,
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if sdkConfig, err = config.LoadDefaultConfig(
|
|
||||||
ctx,
|
|
||||||
config.WithEndpointResolverWithOptions(customResolver),
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
|
s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
|
||||||
//o.BaseEndpoint = aws.String(endpoint)
|
o.BaseEndpoint = aws.String(endpoint)
|
||||||
//o.EndpointResolverV2 = &resolverV2{}
|
o.EndpointResolverV2 = &resolverV2{}
|
||||||
o.Credentials = aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(access, key, ""))
|
o.Credentials = aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(access, key, ""))
|
||||||
o.UsePathStyle = true
|
|
||||||
o.Region = "auto"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if output, err = s3Client.ListBuckets(tool.Timeout(5), &s3.ListBucketsInput{
|
if output, err = s3Client.ListBuckets(tool.Timeout(5), &s3.ListBucketsInput{
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,18 +18,23 @@ import (
|
|||||||
//go:embed all:frontend/dist
|
//go:embed all:frontend/dist
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
flag.BoolVar(&opt.Debug, "debug", false, "debug mode")
|
flag.BoolVar(&opt.Debug, "debug", true, "debug mode")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if opt.Debug {
|
if opt.Debug {
|
||||||
log.SetLogLevel(log.LogLevelDebug)
|
log.SetLogLevel(log.LogLevelDebug)
|
||||||
}
|
}
|
||||||
|
|
||||||
app := controller.NewApp(ctx)
|
app := controller.NewApp()
|
||||||
|
|
||||||
|
app.Init(ctx)
|
||||||
|
|
||||||
if err := wails.Run(&options.App{
|
if err := wails.Run(&options.App{
|
||||||
Title: "nf-disk",
|
Title: "nf-disk",
|
||||||
|
|||||||
Reference in New Issue
Block a user