Compare commits

..

1 Commits

Author SHA1 Message Date
0b58c3347b wip: file download 2024-10-13 22:19:41 +08:00
28 changed files with 583 additions and 1126 deletions

View File

@ -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

View File

@ -4,7 +4,7 @@ import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
import React from "react"; import React from "react";
import {useStoreBucket} from "../../store/bucket"; import {useStoreBucket} from "../../store/bucket";
import {Bucket} from "../../interfaces/connection"; import {Bucket} from "../../interfaces/connection";
import {useStoreFile, useStoreFileFilter} from "../../store/file"; import {useStoreFile} from "../../store/file";
import {useStoreConnection} from "../../store/connection"; import {useStoreConnection} from "../../store/connection";
const useStyles = makeStyles({ const useStyles = makeStyles({
@ -35,13 +35,13 @@ const useStyles = makeStyles({
export function ListBucketComponent() { export function ListBucketComponent() {
const styles = useStyles(); const styles = useStyles();
const {conn_active} = useStoreConnection()
const {bucket_set, bucket_list} = useStoreBucket() const {bucket_set, bucket_list} = useStoreBucket()
const {filter_set, prefix_set} = useStoreFileFilter() const {files_get} = useStoreFile()
async function handleClick(item: Bucket) { async function handleClick(item: Bucket) {
await bucket_set(item) bucket_set(item)
await filter_set('') files_get(conn_active!, item, "")
await prefix_set('')
} }
function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: Bucket) { function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: Bucket) {
@ -50,7 +50,7 @@ export function ListBucketComponent() {
return <MenuList className={styles.container}> return <MenuList className={styles.container}>
<VirtualizerScrollView <VirtualizerScrollView
numItems={bucket_list ? bucket_list.length : 0} numItems={bucket_list?bucket_list.length:0}
itemSize={32} itemSize={32}
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}} container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
> >

View File

@ -1,273 +1,222 @@
import { import {
Button, Button,
Input, Input,
makeStyles, makeStyles,
MenuItem,
MenuList, MenuItem,
mergeClasses, MenuList,
tokens, mergeClasses,
Tooltip, tokens,
} from "@fluentui/react-components"; Tooltip
} from "@fluentui/react-components"
import { import {
DatabaseLinkRegular, DatabaseLinkRegular,
DeleteRegular, DeleteRegular,
DismissRegular, DismissRegular,
PlugConnectedRegular, PlugConnectedRegular,
PlugDisconnectedRegular, SettingsRegular
SettingsRegular,
} from "@fluentui/react-icons"; } from "@fluentui/react-icons";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState} from "react";
import { Connection } from "../../interfaces/connection"; import { Connection} from "../../interfaces/connection";
import { useToast } from "../../message"; import {useToast} from "../../message";
import { Dial } from "../../api"; import {Dial} from "../../api";
import { useStoreConnection } from "../../store/connection"; import {useStoreConnection} from "../../store/connection";
import { useStoreBucket } from "../../store/bucket"; import {useStoreBucket} from "../../store/bucket";
const useStyles = makeStyles({ const useStyles = makeStyles({
list: { list: {
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
height: "100%", 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": { content: {
color: tokens.colorNeutralForeground2BrandPressed, height: "100%",
fontWeight: "bold", width: "25rem",
display: "flex",
flexDirection: "column",
}, },
"& > span": { filter: {
display: "flex", height: "4rem",
width: "100%",
display: "flex",
alignItems: "center",
}, },
}, filter_input: {
items_disconn: { width: "100%",
marginLeft: "auto", marginLeft: "0.5rem",
}, marginRight: "0.5rem",
slider: {
height: "100%",
width: "1px",
// todo: resize
// cursor: 'ew-resize',
"& > div": {
height: "100%",
width: "1px",
backgroundColor: "lightgray",
}, },
}, 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() { export function ConnectionList() {
const styles = useStyles(); const styles = useStyles()
const { dispatchMessage } = useToast(); const {dispatchMessage} = useToast();
const { conn_get, conn_list, conn_set } = useStoreConnection(); const {conn_list, conn_update} = useStoreConnection();
const [conn_filter, set_conn_filter] = useState<string>(""); const [conn_filter, set_conn_filter] = useState<string>('');
const { bucket_get, bucket_set } = useStoreBucket(); const {bucket_get, bucket_set} = useStoreBucket()
const [ctx_menu, set_ctx_menu] = useState<{ const [ctx_menu, set_ctx_menu] = useState<{
x: number; x: number,
y: number; y: number,
display: "none" | "block"; display: 'none' | 'block'
}>({ x: 0, y: 0, display: "none" }); }>({x: 0, y: 0, display: 'none'});
const [menu_conn, set_menu_conn] = useState<Connection | null>(null); const [menu_conn, set_menu_conn] = useState<Connection | null>(null);
useEffect(() => { useEffect(() => {
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
set_ctx_menu({ x: 0, y: 0, display: "none" }); set_ctx_menu({x: 0, y: 0, display: 'none'});
}); })
setTimeout(() => { return () => {
conn_get().then(); document.removeEventListener("click", (e) => {})
}, 1000); }
return () => { }, [])
document.removeEventListener("click", (e) => {});
};
}, []);
async function handleSelect(item: Connection) { async function handleSelect(item: Connection) {
conn_list.map((one: Connection) => { conn_list.map((one: Connection) => {
if (item.id === one.id && one.active) { if (item.id === one.id && one.active) {
conn_set(one); conn_update(one)
bucket_get(one, false); bucket_get(one, false)
bucket_set(null); 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)} }
/>
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
}
conn_update({...item, active: true})
bucket_get(item, true)
bucket_set(null)
}
async function handleDisconnect(item: Connection) {
let res = await Dial('/api/connection/disconnect', {id: item.id})
if (res.status !== 200) {
dispatchMessage(res.msg, "error")
return
}
conn_update({...item, active: false})
}
async function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: Connection) {
e.preventDefault()
console.log('[DEBUG] right click connection =', item, 'event =', e)
console.log(`[DEBUG] click position: [${e.pageX}, ${e.pageY}]`)
set_menu_conn(item)
set_ctx_menu({
x: e.pageX,
y: e.pageY,
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 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 icon={<SettingsRegular />}></MenuItem>
<MenuItem icon={<DeleteRegular />}></MenuItem>
</MenuList>
</div>
<div className={styles.items}>
<MenuList>
{conn_list.filter(item => item.name.includes(conn_filter)).map(item => {
return <MenuItem
className={item.active ? mergeClasses(styles.items_one, "active") : styles.items_one}
onClick={async () => {
await handleSelect(item)
}}
onDoubleClick={async () => {
await handleConnect(item)
}}
onContextMenu={async (e) => {
await handleRightClick(e, item)
}}
icon={<DatabaseLinkRegular/>}
key={item.id}>
{item.name}
<Tooltip
content="断开连接"
relationship="label">
<Button
appearance={'transparent'}
size="small"
icon={<DismissRegular/>}
className={styles.items_disconn}
onClick={async () => {
await handleDisconnect(item)
}}/>
</Tooltip>
</MenuItem>
})}
</MenuList>
</div>
</div>
<div className={styles.slider}>
<div></div>
</div>
</div> </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>
);
} }

View File

@ -1,165 +1,147 @@
import { import {
DialogTrigger, DialogTrigger,
DialogSurface, DialogSurface,
DialogTitle, DialogTitle,
DialogBody, DialogBody,
DialogActions, DialogActions,
DialogContent, DialogContent,
Button, Button, Spinner, Field, Input, 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";
import { useToast } from "../../message"; import {useToast} from "../../message";
import { Dial } from "../../api"; import {Dial} from "../../api";
import { useStoreConnection } from "../../store/connection"; import {useStoreConnection} from "../../store/connection";
const useActionStyle = makeStyles({ const useActionStyle = makeStyles({
container: { container: {
backgroundColor: tokens.colorNeutralBackground1, backgroundColor: tokens.colorNeutralBackground1,
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
height: "100%", height: "100%",
width: "100%", width: "100%",
gridColumnStart: 0, gridColumnStart: 0,
}, },
test: {}, test: {}
}); });
export interface ConnectionCreateProps { export interface ConnectionCreateProps {
openFn: (open: boolean) => void; openFn: (open: boolean) => void;
} }
export function ConnectionCreate(props: ConnectionCreateProps) { 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_get} = useStoreConnection();
>("initial"); const buttonIcon =
const { conn_get } = useStoreConnection(); testLoading === "loading" ? (
const buttonIcon = <Spinner size="tiny"/>
testLoading === "loading" ? ( ) : testLoading === "success" ? (
<Spinner size="tiny" /> <CheckmarkFilled/>
) : testLoading === "success" ? ( ) : testLoading === "error" ? (
<CheckmarkFilled /> <DismissRegular/>
) : testLoading === "error" ? ( ) : null;
<DismissRegular /> const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
) : null; name: '',
const [value, setValue] = useState<{ endpoint: '',
name: string; access: '',
endpoint: string; key: ''
access: string; })
key: string;
}>({
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() {
let res = await Dial("/api/connection/create", value);
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
if (res.status === 200) {
dispatchMessage("新建连接成功", "success");
await conn_get();
props.openFn(false);
} }
}
return ( async function create() {
<> // self
<DialogSurface> // qUvfW8xpOTc23O96
<DialogBody> // eTcuc8BebHPVpZZwIaNmzfwxRxPYGfTj
<DialogTitle>S3连接</DialogTitle>
<DialogContent> // 48-dev
<div className="connection-container"> // OSIsqPrl0TkAUj3R
<div className="connection-form"> // FYF4BBzL2j2ObbVYH0FrvOZqJf1EACRy
<div className="connection-form-field"> let res = await Dial("/api/connection/create", value)
<Field label="name"> dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
<Input if (res.status === 200) {
placeholder="名称 (example: 测试S3-minio)" dispatchMessage("新建连接成功", "success");
value={value.name} conn_get()
onChange={(e) => { props.openFn(false)
setValue({ ...value, name: e.target.value }); }
}} }
/>
</Field> return <>
</div> <DialogSurface>
<div className="connection-form-field"> <DialogBody>
<Field label="endpoint" required> <DialogTitle>S3连接</DialogTitle>
<Input <DialogContent>
placeholder="地址 (example: https://ip_or_server-name:port)" <div className='connection-container'>
value={value.endpoint} <div className='connection-form'>
required <div className='connection-form-field'>
onChange={(e) => { <Field
setValue({ ...value, endpoint: e.target.value }); label="name"
}} >
/> <Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
</Field> onChange={(e) => {
</div> setValue({...value, name: e.target.value});
<div className="connection-form-field"> }}/>
<Field label="secret access" required> </Field>
<Input </div>
placeholder="" <div className='connection-form-field'>
required <Field
value={value.access} label="endpoint"
onChange={(e) => { required
setValue({ ...value, access: e.target.value }); >
}} <Input placeholder='地址 (example: https://ip_or_server-name:port)'
/> value={value.endpoint}
</Field> required
</div> onChange={(e) => {
<div className="connection-form-field"> setValue({...value, endpoint: e.target.value});
<Field label="secret key" required> }}/>
<Input </Field>
placeholder="" </div>
required <div className='connection-form-field'>
value={value.key} <Field
onChange={(e) => { label="secret access"
setValue({ ...value, key: e.target.value }); required
}} >
/> <Input placeholder=''
</Field> required
</div> value={value.access} onChange={(e) => {
</div> setValue({...value, access: e.target.value});
</div> }}/>
</DialogContent> </Field>
<DialogActions className={actionStyle.container}> </div>
<Button <div className='connection-form-field'>
className={actionStyle.test} <Field
appearance="transparent" label="secret key"
icon={buttonIcon} required
onClick={async () => await test()} >
> <Input placeholder=''
required
</Button> value={value.key} onChange={(e) => {
<DialogTrigger disableButtonEnhancement> setValue({...value, key: e.target.value});
<Button appearance="secondary"></Button> }}/>
</DialogTrigger> </Field>
<Button </div>
onClick={async () => { </div>
await create(); </div>
}} </DialogContent>
appearance="primary" <DialogActions className={actionStyle.container}>
> <Button className={actionStyle.test} appearance='transparent' icon={buttonIcon}
onClick={async () => await test()}></Button>
</Button> <DialogTrigger disableButtonEnhancement>
</DialogActions> <Button appearance="secondary"></Button>
</DialogBody> </DialogTrigger>
</DialogSurface> <Button onClick={async () => {
await create()
}} appearance="primary"></Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</> </>
);
} }

View File

@ -3,8 +3,6 @@ import {ListBucketComponent} from "../bucket/list_bucket";
import {makeStyles} from "@fluentui/react-components"; import {makeStyles} from "@fluentui/react-components";
import {useStoreBucket} from "../../store/bucket"; import {useStoreBucket} from "../../store/bucket";
import {ListFileComponent} from "./list_file"; import {ListFileComponent} from "./list_file";
import {useState} from "react";
import {PreviewFile} from "../preview/preview";
const useStyles = makeStyles({ const useStyles = makeStyles({
content: { content: {
@ -19,21 +17,13 @@ const useStyles = makeStyles({
export function Content() { export function Content() {
const styles = useStyles() const styles = useStyles()
const [preview, set_preview] = useState<{ url: string, content_type: string }>({url: '', content_type: ''}) const {bucket_active } = useStoreBucket()
const {bucket_active} = useStoreBucket()
const closeFn = () => {
set_preview({url: '', content_type: ''})
}
return <div className={styles.content}> return <div className={styles.content}>
<Path/> <Path/>
{ {
preview.url ? <PreviewFile url={preview.url} content_type={preview.content_type} close={closeFn}/> : bucket_active ?
( <ListFileComponent/> :
bucket_active ? <ListBucketComponent/>
<ListFileComponent set_preview_fn={set_preview}/> : <ListBucketComponent/>
)
} }
</div> </div>
} }

View File

@ -1,340 +1,155 @@
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
import { import {
makeStyles,
MenuItem, DocumentBulletListRegular, DocumentChevronDoubleRegular, DocumentCssRegular, DocumentDatabaseRegular,
MenuList, DocumentDismissRegular,
Spinner, DocumentImageRegular, DocumentJavascriptRegular, DocumentPdfRegular, DocumentYmlRegular,
Text, FolderRegular
tokens,
} from "@fluentui/react-components";
import {
ArrowDownloadFilled,
DeleteRegular,
DocumentBulletListRegular,
DocumentChevronDoubleRegular,
DocumentCssRegular,
DocumentDatabaseRegular,
DocumentDismissRegular,
DocumentImageRegular,
DocumentJavascriptRegular,
DocumentPdfRegular,
DocumentYmlRegular,
FolderRegular,
PreviewLinkRegular,
} from "@fluentui/react-icons"; } from "@fluentui/react-icons";
import { VirtualizerScrollView } from "@fluentui/react-components/unstable"; import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
import React, { useEffect, useState } from "react"; import React from "react";
import { useStoreBucket } from "../../store/bucket"; import {useStoreBucket} from "../../store/bucket";
import { S3File } from "../../interfaces/connection"; import { S3File} from "../../interfaces/connection";
import { useStoreFile, useStoreFileFilter } from "../../store/file"; import {useStoreFile} from "../../store/file";
import { useStoreConnection } from "../../store/connection"; import {useStoreConnection} from "../../store/connection";
import { TrimSuffix } from "../../hook/strings"; import {TrimSuffix} from "../../hook/strings";
import { Dial } from "../../api";
import { useToast } from "../../message";
import { CanPreview } from "../../hook/preview";
const useStyles = makeStyles({ const useStyles = makeStyles({
container: { container: {
marginTop: "0.5rem", marginTop: '0.5rem',
maxWidth: "calc(100vw - 25.2rem)", maxWidth: 'calc(100vw - 25rem - 1px)',
width: "calc(100vw - 25.2rem)", width: 'calc(100vw - 25rem - 1px)',
maxHeight: "calc(100vh - 10rem)", height: 'calc(100vh - 9rem)',
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,
}, },
}, row: {
text: { height: '32px',
overflow: "hidden", display: 'flex',
width: "calc(100vw - 32rem)", marginLeft: '0.5rem',
display: "block", marginRight: '0.5rem',
}, },
ctx_menu: { item: {
position: "absolute", width: '100%',
zIndex: "1000", maxWidth: '100%',
width: "15rem", "&:hover": {
backgroundColor: tokens.colorNeutralBackground1, color: tokens.colorNeutralForeground2BrandPressed,
boxShadow: `${tokens.shadow16}`, }
paddingTop: "4px", },
paddingBottom: "4px", text: {
}, overflow: 'hidden',
}); width: 'calc(100vw - 32rem)',
display: "block",
},
no_data: {
flex: "1",
height: '100%',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '8rem',
flexDirection: 'column',
},
})
export interface ListFileComponentProps { export function ListFileComponent() {
set_preview_fn: React.Dispatch<
React.SetStateAction<{ url: string; content_type: string }>
>;
}
export function ListFileComponent(props: ListFileComponentProps) { const styles = useStyles();
const styles = useStyles(); const {conn_active} = useStoreConnection();
const { dispatchMessage } = useToast(); const {bucket_active} = useStoreBucket()
const { conn_active } = useStoreConnection(); const {files_get, files_list} = useStoreFile()
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(() => { const filename = (key: string) => {
document.addEventListener("click", (e) => { let strs = TrimSuffix(key, "/").split("/")
set_ctx_menu({ x: 0, y: 0, display: "none" }); return strs[strs.length - 1]
});
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( async function handleClick(item: S3File) {
e: React.MouseEvent<HTMLDivElement>, if (item.type === 1) {
item: S3File files_get(conn_active!, bucket_active!, item.key)
) { return
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() { function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: S3File) {
const res = await Dial<{ url: string; method: string }>("/api/file/get", { e.preventDefault()
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 ( return <MenuList className={styles.container}>
<div className={styles.container}> {files_list.length ?
<div <VirtualizerScrollView
id={"list-file-container"} numItems={files_list.length}
className={styles.ctx_menu} itemSize={32}
style={{ left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display }} container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
> >
<MenuList> {(idx) => {
<MenuItem return <div
onClick={async () => { className={styles.row} key={idx}
await handleDownload(file_active); onClick={async () => {
}} await handleClick(files_list[idx])
icon={<ArrowDownloadFilled />} }}
> onContextMenu={async (e) => {
handleRightClick(e, files_list[idx])
</MenuItem> }}>
<MenuItem <MenuItem className={styles.item}
disabled={!preview_content_type} icon={files_list[idx].type ? <FolderRegular/> :
onClick={async () => { <FileIcon name={files_list[idx].name}/>}>
await handlePreview(); <Text truncate wrap={false} className={styles.text}>
}} {filename(files_list[idx].key)}
icon={<PreviewLinkRegular />} </Text>
> </MenuItem>
</div>
</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) => { </VirtualizerScrollView> : <div className={styles.no_data}>
await handleRightClick(e, files_list[idx]); <div>
}} <DocumentDismissRegular/>
> </div>
<MenuItem <Text size={900}>
className={styles.item}
icon={ </Text>
files_list[idx].type ? ( </div>
<FolderRegular /> }
) : ( </MenuList>
<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 = { type FileIconProps = {
name: string; name: string
}; }
function FileIcon(props: FileIconProps) { function FileIcon(props: FileIconProps) {
const strings = props.name.split("."); const strings = props.name.split(".")
const suffix = strings[strings.length - 1]; const suffix = strings[strings.length - 1]
switch (suffix.toLowerCase()) { switch (suffix) {
case "png": case "png":
return <DocumentImageRegular />; return <DocumentImageRegular/>
case "jpg": case "jpg":
return <DocumentImageRegular />; return <DocumentImageRegular/>
case "jpeg": case "jpeg":
return <DocumentImageRegular />; return <DocumentImageRegular/>
case "gif": case "gif":
return <DocumentImageRegular />; return <DocumentImageRegular/>
case "db": case "db":
return <DocumentDatabaseRegular />; return <DocumentDatabaseRegular/>
case "sqlite": case "sqlite":
return <DocumentDatabaseRegular />; return <DocumentDatabaseRegular/>
case "sqlite3": case "sqlite3":
return <DocumentDatabaseRegular />; return <DocumentDatabaseRegular/>
case "pdf": case "pdf":
return <DocumentPdfRegular />; return <DocumentPdfRegular/>
case "css": case "css":
return <DocumentCssRegular />; return <DocumentCssRegular />
case "js": case "js":
return <DocumentJavascriptRegular />; return <DocumentJavascriptRegular/>
case "yaml": case "yaml":
return <DocumentYmlRegular />; return <DocumentYmlRegular/>
case "yml": case "yml":
return <DocumentYmlRegular />; return <DocumentYmlRegular/>
case "html": case "html":
return <DocumentChevronDoubleRegular />; return <DocumentChevronDoubleRegular/>
case "json": case "json":
return <DocumentChevronDoubleRegular />; return <DocumentChevronDoubleRegular/>
case "go": case "go":
return <DocumentChevronDoubleRegular />; return <DocumentChevronDoubleRegular/>
default: default:
return <DocumentBulletListRegular />; return <DocumentBulletListRegular/>
} }
} }

View File

@ -1,12 +1,10 @@
import {Button, Input, makeStyles, Text, tokens, Tooltip} from "@fluentui/react-components"; import {Button, Input, makeStyles, Text, tokens, Tooltip} from "@fluentui/react-components";
import {useStoreBucket} from "../../store/bucket"; import {useStoreBucket} from "../../store/bucket";
import {ArchiveRegular, ArrowCurveUpLeftFilled} from "@fluentui/react-icons"; import {ArchiveRegular, ArrowCurveUpLeftFilled} from "@fluentui/react-icons";
import {useStoreFile, useStoreFileFilter} from "../../store/file"; import {useStoreFile} from "../../store/file";
import React, {useState} from "react"; import React from "react";
import {debounce} from 'lodash' import {debounce} from 'lodash'
import {useStoreConnection} from "../../store/connection"; import {useStoreConnection} from "../../store/connection";
import {ListFileComponent} from "./list_file";
import {ListBucketComponent} from "../bucket/list_bucket";
const useStyles = makeStyles({ const useStyles = makeStyles({
container: { container: {
@ -53,28 +51,27 @@ const useStyles = makeStyles({
}, },
}) })
export function Path() { export function Path() {
const styles = useStyles() const styles = useStyles()
const {conn_active} = useStoreConnection() const {conn_active} = useStoreConnection()
const {bucket_active, bucket_get, bucket_set} = useStoreBucket() const {bucket_active, bucket_get, bucket_set} = useStoreBucket()
const {prefix, filter, prefix_set, filter_set} = useStoreFileFilter() const {prefix, files_get} = useStoreFile()
async function handleClickUp() { async function handleClickUp() {
const dirs = prefix.split('/').filter((item => item)) const dirs = prefix.split('/').filter((item => item))
if (dirs.length > 0) { if (dirs.length > 0) {
dirs.pop() dirs.pop()
await prefix_set(dirs.join('/')) files_get(conn_active!, bucket_active!, dirs.join("/"))
return return
} }
bucket_get(conn_active!, false) bucket_get(conn_active!, false)
await bucket_set(null) bucket_set(null)
} }
const handleFilterChange = debounce(async (e) => { const handleFilterChange = debounce((e) => {
await filter_set(e.target.value) files_get(conn_active!, bucket_active!, prefix, e.target.value)
}, 500) }, 500)
return <div className={styles.container}> return <div className={styles.container}>

View File

@ -12,7 +12,7 @@ import {useToast} from "../../message";
import {Dial} from "../../api"; import {Dial} from "../../api";
import {useStoreConnection} from "../../store/connection"; import {useStoreConnection} from "../../store/connection";
import {useStoreBucket} from "../../store/bucket"; import {useStoreBucket} from "../../store/bucket";
import {useStoreFile, useStoreFileFilter} from "../../store/file"; import {useStoreFile} from "../../store/file";
import {MoreHorizontalRegular} from "@fluentui/react-icons"; import {MoreHorizontalRegular} from "@fluentui/react-icons";
const useStyle = makeStyles({ const useStyle = makeStyles({
@ -43,8 +43,7 @@ export function UploadFiles(props: UploadFilesProps) {
const { conn_active} = useStoreConnection(); const { conn_active} = useStoreConnection();
const {bucket_active} = useStoreBucket(); const {bucket_active} = useStoreBucket();
const {files_get} = useStoreFile() const {prefix, files_get} = useStoreFile()
const {prefix } = useStoreFileFilter()
const [selected, set_selected] = useState<string[]>([]); const [selected, set_selected] = useState<string[]>([]);

View File

@ -1,4 +1,11 @@
import {makeStyles} from "@fluentui/react-components"; import {Button, Input, makeStyles, MenuItem, MenuList, mergeClasses, tokens, Tooltip} from "@fluentui/react-components";
import {DismissRegular} from "@fluentui/react-icons";
import {useEffect, useState} from "react";
import {Connection} from "../../interfaces/connection";
import {useStoreBucket} from "../../store/bucket";
import {useStoreConnection} from "../../store/connection";
import {Dial} from "../../api";
import {useToast} from "../../message";
import {ConnectionList} from "../connection/list"; import {ConnectionList} from "../connection/list";
import {Content} from "../file/content"; import {Content} from "../file/content";
@ -13,9 +20,15 @@ const useStyles = makeStyles({
export function Body() { export function Body() {
const styles = useStyles(); const styles = useStyles();
const {conn_get} = useStoreConnection();
useEffect(() => {
conn_get()
}, []);
return <div className={styles.body}> return <div className={styles.body}>
<ConnectionList/> <ConnectionList/>
<Content/> <Content />
</div> </div>
} }

View File

@ -1,6 +1,6 @@
import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components"; import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components";
import {ConnectionCreate} from "../connection/new"; import {ConnectionCreate} from "../connection/new";
import {AppsAddInRegular, DocumentArrowUpRegular, PlugConnectedAddRegular} from "@fluentui/react-icons"; import {AppsAddInRegular, DocumentArrowUpRegular, PlugConnectedAddRegular} from "@fluentui/react-icons";
import {useState} from "react"; import {useState} from "react";
import {useStoreConnection} from "../../store/connection"; import {useStoreConnection} from "../../store/connection";
import {BucketCreate} from "../bucket/new"; import {BucketCreate} from "../bucket/new";
@ -10,7 +10,6 @@ import {UploadFiles} from "../file/upload_files";
const useStyles = makeStyles({ const useStyles = makeStyles({
header: { header: {
height: "5rem", height: "5rem",
minHeight: '5rem',
width: "100%", width: "100%",
display: 'flex', display: 'flex',
alignItems: "center", alignItems: "center",
@ -65,7 +64,7 @@ export function Header() {
open={open_upload} open={open_upload}
onOpenChange={(event, data) => set_open_upload(data.open)}> onOpenChange={(event, data) => set_open_upload(data.open)}>
<DialogTrigger disableButtonEnhancement> <DialogTrigger disableButtonEnhancement>
<Button appearance="primary" icon={<DocumentArrowUpRegular/>}> <Button appearance="primary" icon={<DocumentArrowUpRegular />}>
</Button> </Button>
</DialogTrigger> </DialogTrigger>

View File

@ -1,81 +1,3 @@
import {Button, makeStyles,tokens, Text } from "@fluentui/react-components"; export function PreviewFile() {
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>
}
} }

View File

@ -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 ""
}
}

View File

@ -4,7 +4,7 @@ import {Dial, Resp} from "../api";
interface StoreBucket { interface StoreBucket {
bucket_active: Bucket | null; bucket_active: Bucket | null;
bucket_set: (Bucket: Bucket | null) => Promise<void>; bucket_set: (Bucket: Bucket | null) => void;
bucket_list: Bucket[]; bucket_list: Bucket[];
bucket_get: (conn: Connection, refresh: boolean) => void; bucket_get: (conn: Connection, refresh: boolean) => void;
bucket_create: (conn: Connection, name: string, public_read: boolean, public_read_write: boolean) => void; bucket_create: (conn: Connection, name: string, public_read: boolean, public_read_write: boolean) => void;

View File

@ -5,8 +5,8 @@ import {Dial} from "../api";
interface StoreConnection { interface StoreConnection {
conn_active: Connection | null; conn_active: Connection | null;
conn_list: Connection[]; conn_list: Connection[];
conn_get: () => Promise<void>; conn_get: () => void;
conn_set: (connection: Connection) => Promise<void>; conn_update: (connection: Connection) => void;
} }
export const useStoreConnection = create<StoreConnection>()((set) => ({ export const useStoreConnection = create<StoreConnection>()((set) => ({
@ -21,7 +21,7 @@ export const useStoreConnection = create<StoreConnection>()((set) => ({
set({conn_list: res.data.list}) set({conn_list: res.data.list})
}, },
conn_set: async (connection: Connection) => { conn_update: async (connection: Connection) => {
set((state) => { set((state) => {
return { return {
conn_active: connection.active? connection: null, conn_active: connection.active? connection: null,

View File

@ -3,19 +3,15 @@ import {Bucket, Connection, S3File} from "../interfaces/connection";
import {Dial} from "../api"; import {Dial} from "../api";
interface StoreFile { interface StoreFile {
file_active: string | null, prefix: string;
file_set: (key: string) => Promise<void>, filter:string;
files_list: S3File[]; files_list: S3File[];
files_get: (conn: Connection, bucket: Bucket, prefix?: string, filter?: string) => Promise<void>; files_get: (conn: Connection, bucket: Bucket, prefix?: string, filter?: string) => void;
} }
export const useStoreFile = create<StoreFile>()((set) => ({ export const useStoreFile = create<StoreFile>()((set) => ({
file_active: null, prefix: "",
file_set: async (key: string) => { filter: "",
set((state) => {
return {file_active: key}
})
},
files_list: [], files_list: [],
files_get: async (conn: Connection, bucket: Bucket, prefix = '', filter = '') => { files_get: async (conn: Connection, bucket: Bucket, prefix = '', filter = '') => {
const res = await Dial<{ list: S3File[] }>('/api/bucket/files', { const res = await Dial<{ list: S3File[] }>('/api/bucket/files', {
@ -29,25 +25,7 @@ export const useStoreFile = create<StoreFile>()((set) => ({
} }
set((state) => { set((state) => {
return {files_list: res.data.list} return {files_list: res.data.list, prefix: prefix, filter: filter}
}) })
}, }
}))
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}})
},
})) }))

View File

@ -1,33 +1,18 @@
import {create} from 'zustand' import {create} from 'zustand'
import {Dial} from "../api";
import {Bucket, Connection} from "../interfaces/connection"; import {Bucket, Connection} from "../interfaces/connection";
interface StorePreview { interface StorePreview {
preview_key: string;
preview_url: string; preview_url: string;
preview_content_type: string; preview_content_type: string;
preview_get: (conn:Connection,bucket: Bucket,key: string) => Promise<void>; preview_set: (conn:Connection,bucket: Bucket,key: string) => void;
} }
export const useStoreFile = create<StorePreview>()((set) => ({
export const useStorePreview = create<StorePreview>()((set) => ({ preview_key: '',
preview_url: '', preview_url: '',
preview_content_type: '', preview_content_type: '',
preview_get: async (conn: Connection, bucket: Bucket,key: string) => { preview_set: (conn:Connection,bucket: Bucket,key: string) => set(state => {
if (key === '') { return {preview_key: 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}
})
},
})) }))

2
go.mod
View File

@ -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

View File

@ -25,7 +25,6 @@ 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/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)
@ -37,7 +36,6 @@ func Init(ctx context.Context) error {
register("/api/file/upload", handler.FileUpload) register("/api/file/upload", handler.FileUpload)
register("/api/file/info", handler.FileInfo) register("/api/file/info", handler.FileInfo)
register("/api/file/get", handler.FileGet) register("/api/file/get", handler.FileGet)
register("/api/file/download", handler.FileDownload)
return nil return nil
} }

View File

@ -2,12 +2,10 @@ 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"
@ -39,8 +37,7 @@ func NewApp(gctx context.Context) *App {
func (a *App) Startup(ctx context.Context) { func (a *App) Startup(ctx context.Context) {
log.Info("app startup!!!") log.Info("app startup!!!")
a.ctx = ctx a.ctx = ctx
tool.Must(opt.Init()) tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
tool.Must(db.Init(ctx, fmt.Sprintf("sqlite::%s", opt.ConfigFile)))
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))

View File

@ -8,6 +8,7 @@ import (
"github.com/loveuer/nf-disk/internal/s3" "github.com/loveuer/nf-disk/internal/s3"
"github.com/loveuer/nf-disk/ndh" "github.com/loveuer/nf-disk/ndh"
"github.com/samber/lo" "github.com/samber/lo"
"time"
) )
func ConnectionTest(c *ndh.Ctx) error { func ConnectionTest(c *ndh.Ctx) error {
@ -209,5 +210,11 @@ func ConnectionBuckets(c *ndh.Ctx) error {
return c.Send500(err.Error()) return c.Send500(err.Error())
} }
// todo: for frontend test
buckets = append(buckets, &s3.ListBucketRes{
Name: "这是一个非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长的名字",
CreatedAt: time.Now().UnixMilli(),
})
return c.Send200(map[string]any{"list": buckets}) return c.Send200(map[string]any{"list": buckets})
} }

View File

@ -5,8 +5,6 @@ import (
"github.com/loveuer/nf-disk/ndh" "github.com/loveuer/nf-disk/ndh"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"os"
"path/filepath"
) )
func DialogOpen(ctx context.Context) ndh.Handler { func DialogOpen(ctx context.Context) ndh.Handler {
@ -56,58 +54,3 @@ func DialogOpen(ctx context.Context) ndh.Handler {
return c.Send200(map[string]interface{}{"result": result}) 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})
}
}

View File

@ -138,48 +138,9 @@ func FileGet(c *ndh.Ctx) error {
return c.Send500(err.Error()) return c.Send500(err.Error())
} }
if link, err = client.GetObjectEntry(c.Context(), req.Bucket, req.Key); err != nil { if link, err = client.GetObject(c.Context(), req.Bucket, req.Key); err != nil {
return c.Send500(err.Error()) return c.Send500(err.Error())
} }
return c.Send200(link) 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)
}

View File

@ -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
} }

View File

@ -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
}

View File

@ -7,7 +7,5 @@ const (
) )
var ( var (
Debug bool = false Debug bool = false
ConfigDir string
ConfigFile string
) )

View File

@ -6,16 +6,15 @@ import (
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"io"
"net/http" "net/http"
"time" "time"
) )
type ObjectInfo struct { type ObjectInfo struct {
Bucket string `json:"bucket"` Bucket string
Key string `json:"key"` Key string
ContentType string `json:"content_type"` ContentType string
Expire int64 `json:"expire"` Expire int64
} }
func (c *Client) GetObjectInfo(ctx context.Context, bucket string, key string) (*ObjectInfo, error) { func (c *Client) GetObjectInfo(ctx context.Context, bucket string, key string) (*ObjectInfo, error) {
@ -62,12 +61,12 @@ func (presigner *Presigner) GetObject(ctx context.Context, bucketName string, ob
} }
type ObjectEntry struct { type ObjectEntry struct {
URL string `json:"url"` URL string
Method string `json:"method"` Method string
Header http.Header `json:"header"` Header http.Header
} }
func (c *Client) GetObjectEntry(ctx context.Context, bucket string, key string, lifetimes ...int64) (*ObjectEntry, error) { func (c *Client) GetObject(ctx context.Context, bucket string, key string, lifetimes ...int64) (*ObjectEntry, error) {
var ( var (
err error err error
lifetime int64 = 5 * 60 lifetime int64 = 5 * 60
@ -89,33 +88,3 @@ func (c *Client) GetObjectEntry(ctx context.Context, bucket string, key string,
Header: output.SignedHeader, Header: output.SignedHeader,
}, nil }, 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
}

View File

@ -22,7 +22,7 @@ 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 {

10
xtest/path.js Normal file
View File

@ -0,0 +1,10 @@
function getBaseFileName(fullPath) {
return fullPath.replace(/.*[\/\\]/, '');
}
// 测试
const filePath = 'C:\\Users\\username\\Documents\\example.txt';
console.log(getBaseFileName(filePath)); // 输出: example.txt
const filePath2 = '/home/user/documents/example.txt';
console.log(getBaseFileName(filePath2)); // 输出: example.txt