feat: 完成了 新建桶; 上传文件(基本功能)
todo: 上传 rename, 上传 public 权限选择 bug: 首次加载 conns list; 上传的时候前缀过滤失败
This commit is contained in:
parent
1c818daf16
commit
777253063b
@ -1 +1 @@
|
||||
b20ef5a27687e07e09878451f9a2e1aa
|
||||
674272f31b23a798d77a17321c6a8785
|
@ -1,5 +1,5 @@
|
||||
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
|
||||
import {ArchiveRegular, DocumentBulletListRegular} from "@fluentui/react-icons";
|
||||
import {ArchiveRegular} from "@fluentui/react-icons";
|
||||
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
|
||||
import React from "react";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
@ -50,7 +50,7 @@ export function ListBucketComponent() {
|
||||
|
||||
return <MenuList className={styles.container}>
|
||||
<VirtualizerScrollView
|
||||
numItems={bucket_list.length}
|
||||
numItems={bucket_list?bucket_list.length:0}
|
||||
itemSize={32}
|
||||
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
|
||||
>
|
111
frontend/src/component/bucket/new.tsx
Normal file
111
frontend/src/component/bucket/new.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import {
|
||||
DialogTrigger,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
DialogBody,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Button, Field, Input, makeStyles, tokens, Checkbox,
|
||||
} from "@fluentui/react-components";
|
||||
import {useState} from "react";
|
||||
import {useToast} from "../../message";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
|
||||
const useStyle = makeStyles({
|
||||
container: {
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridColumnStart: 0,
|
||||
},
|
||||
content: {},
|
||||
input: {
|
||||
margin: '1rem',
|
||||
},
|
||||
checks: {
|
||||
margin: '1rem',
|
||||
display: "flex",
|
||||
},
|
||||
});
|
||||
|
||||
export interface BucketCreateProps {
|
||||
openFn: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function BucketCreate(props: BucketCreateProps) {
|
||||
const styles = useStyle();
|
||||
const {dispatchMessage} = useToast();
|
||||
|
||||
const [name, set_name] = useState<string>();
|
||||
const [public_read, set_public_read] = useState(false);
|
||||
const [public_read_write, set_public_read_write] = useState(false);
|
||||
|
||||
const {conn_active} = useStoreConnection();
|
||||
const {bucket_create, bucket_get} = useStoreBucket()
|
||||
|
||||
async function create() {
|
||||
if (!name) {
|
||||
dispatchMessage('桶名不能为空', "warning")
|
||||
return
|
||||
}
|
||||
|
||||
bucket_create(conn_active!, name, public_read, public_read_write);
|
||||
bucket_get(conn_active!, true)
|
||||
props.openFn(false)
|
||||
}
|
||||
|
||||
return <>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>在 {conn_active?.name} 新建桶</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className={styles.content}>
|
||||
<Field className={styles.input} label="桶名">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
set_name(e.target.value)
|
||||
}}></Input>
|
||||
</Field>
|
||||
<div className={styles.checks}>
|
||||
<Field>
|
||||
<Checkbox
|
||||
checked={public_read}
|
||||
onChange={(e) => {
|
||||
if (public_read_write) {
|
||||
return
|
||||
}
|
||||
|
||||
set_public_read(e.target.checked);
|
||||
}}
|
||||
label={"公共读"}></Checkbox>
|
||||
</Field>
|
||||
<Field>
|
||||
<Checkbox
|
||||
checked={public_read_write}
|
||||
onChange={(e) => {
|
||||
set_public_read_write(e.target.checked)
|
||||
if (e.target.checked) {
|
||||
set_public_read(e.target.checked)
|
||||
}
|
||||
}}
|
||||
label={"公共读/写"}></Checkbox>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions className={styles.container}>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="secondary">取消</Button>
|
||||
</DialogTrigger>
|
||||
<Button onClick={async () => {
|
||||
await create()
|
||||
}} appearance="primary">新建</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</>
|
||||
}
|
@ -2,16 +2,16 @@ import {
|
||||
Button,
|
||||
Input,
|
||||
makeStyles,
|
||||
Menu,
|
||||
|
||||
MenuItem,
|
||||
MenuList, MenuPopover, MenuProps,
|
||||
mergeClasses, PositioningImperativeRef,
|
||||
MenuList,
|
||||
mergeClasses,
|
||||
tokens,
|
||||
Tooltip
|
||||
} from "@fluentui/react-components"
|
||||
import {DatabaseLinkRegular, DismissRegular} from "@fluentui/react-icons";
|
||||
import React, {useState} from "react";
|
||||
import {Bucket, Connection} from "../../interfaces/connection";
|
||||
import React, { useEffect, useState} from "react";
|
||||
import { Connection} from "../../interfaces/connection";
|
||||
import {useToast} from "../../message";
|
||||
import {Dial} from "../../api";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
@ -40,6 +40,15 @@ const useStyles = makeStyles({
|
||||
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%",
|
||||
@ -78,6 +87,20 @@ export function ConnectionList() {
|
||||
const {conn_list, conn_update} = 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'});
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", (e) => {
|
||||
set_ctx_menu({x: 0, y: 0, display: 'none'});
|
||||
})
|
||||
return () => {
|
||||
document.removeEventListener("click", (e) => {})
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function handleSelect(item: Connection) {
|
||||
conn_list.map((one: Connection) => {
|
||||
@ -114,6 +137,11 @@ export function ConnectionList() {
|
||||
e.preventDefault()
|
||||
console.log('[DEBUG] right click connection =', item, 'event =', e)
|
||||
console.log(`[DEBUG] click position: [${e.pageX}, ${e.pageY}]`)
|
||||
set_ctx_menu({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
display: 'block',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@ -132,6 +160,15 @@ export function ConnectionList() {
|
||||
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>连接</MenuItem>
|
||||
<MenuItem>设置</MenuItem>
|
||||
<MenuItem>删除</MenuItem>
|
||||
</MenuList>
|
||||
</div>
|
||||
<div className={styles.items}>
|
||||
<MenuList>
|
||||
{conn_list.filter(item => item.name.includes(conn_filter)).map(item => {
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
DialogBody,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Button, Spinner, Field, Input, FieldProps, makeStyles, tokens,
|
||||
Button, Spinner, Field, Input, makeStyles, tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import {useState} from "react";
|
||||
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
|
||||
@ -84,8 +84,6 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="name"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
>
|
||||
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
|
||||
onChange={(e) => {
|
||||
@ -96,11 +94,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="endpoint"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
required
|
||||
>
|
||||
<Input placeholder='地址 (example: https://ip_or_server-name:port)'
|
||||
value={value.endpoint}
|
||||
required
|
||||
onChange={(e) => {
|
||||
setValue({...value, endpoint: e.target.value});
|
||||
}}/>
|
||||
@ -109,10 +107,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret access"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
required
|
||||
>
|
||||
<Input placeholder='' value={value.access} onChange={(e) => {
|
||||
<Input placeholder=''
|
||||
required
|
||||
value={value.access} onChange={(e) => {
|
||||
setValue({...value, access: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
@ -120,10 +119,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret key"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
required
|
||||
>
|
||||
<Input placeholder='' value={value.key} onChange={(e) => {
|
||||
<Input placeholder=''
|
||||
required
|
||||
value={value.key} onChange={(e) => {
|
||||
setValue({...value, key: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import {Path} from "./path";
|
||||
import {ListBucketComponent} from "./list_bucket";
|
||||
import {ListBucketComponent} from "../bucket/list_bucket";
|
||||
import {makeStyles} from "@fluentui/react-components";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {useStoreFile} from "../../store/file";
|
||||
import {ListFileComponent} from "./list_file";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@ -18,9 +17,7 @@ const useStyles = makeStyles({
|
||||
export function Content() {
|
||||
|
||||
const styles = useStyles()
|
||||
const {bucket_active, bucket_list} = useStoreBucket()
|
||||
const {file_list} = useStoreFile()
|
||||
|
||||
const {bucket_active } = useStoreBucket()
|
||||
return <div className={styles.content}>
|
||||
<Path/>
|
||||
{
|
||||
|
@ -1,9 +1,15 @@
|
||||
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
|
||||
import {ArchiveRegular, DocumentBulletListRegular, DocumentDismissRegular, FolderRegular} from "@fluentui/react-icons";
|
||||
import {
|
||||
|
||||
DocumentBulletListRegular, DocumentChevronDoubleRegular, DocumentCssRegular, DocumentDatabaseRegular,
|
||||
DocumentDismissRegular,
|
||||
DocumentImageRegular, DocumentJavascriptRegular, DocumentPdfRegular, DocumentYmlRegular,
|
||||
FolderRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
|
||||
import React, {useEffect} from "react";
|
||||
import React from "react";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {Bucket, S3File} from "../../interfaces/connection";
|
||||
import { S3File} from "../../interfaces/connection";
|
||||
import {useStoreFile} from "../../store/file";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
import {TrimSuffix} from "../../hook/strings";
|
||||
@ -85,7 +91,8 @@ export function ListFileComponent() {
|
||||
handleRightClick(e, files_list[idx])
|
||||
}}>
|
||||
<MenuItem className={styles.item}
|
||||
icon={files_list[idx].type ? <FolderRegular/> : <DocumentBulletListRegular/>}>
|
||||
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>
|
||||
@ -94,7 +101,7 @@ export function ListFileComponent() {
|
||||
}}
|
||||
</VirtualizerScrollView> : <div className={styles.no_data}>
|
||||
<div>
|
||||
<DocumentDismissRegular />
|
||||
<DocumentDismissRegular/>
|
||||
</div>
|
||||
<Text size={900}>
|
||||
没有文件
|
||||
@ -102,4 +109,47 @@ export function ListFileComponent() {
|
||||
</div>
|
||||
}
|
||||
</MenuList>
|
||||
}
|
||||
|
||||
type FileIconProps = {
|
||||
name: string
|
||||
}
|
||||
|
||||
function FileIcon(props: FileIconProps) {
|
||||
const strings = props.name.split(".")
|
||||
const suffix = strings[strings.length - 1]
|
||||
switch (suffix) {
|
||||
case "png":
|
||||
return <DocumentImageRegular/>
|
||||
case "jpg":
|
||||
return <DocumentImageRegular/>
|
||||
case "jpeg":
|
||||
return <DocumentImageRegular/>
|
||||
case "gif":
|
||||
return <DocumentImageRegular/>
|
||||
case "db":
|
||||
return <DocumentDatabaseRegular/>
|
||||
case "sqlite":
|
||||
return <DocumentDatabaseRegular/>
|
||||
case "sqlite3":
|
||||
return <DocumentDatabaseRegular/>
|
||||
case "pdf":
|
||||
return <DocumentPdfRegular/>
|
||||
case "css":
|
||||
return <DocumentCssRegular />
|
||||
case "js":
|
||||
return <DocumentJavascriptRegular/>
|
||||
case "yaml":
|
||||
return <DocumentYmlRegular/>
|
||||
case "yml":
|
||||
return <DocumentYmlRegular/>
|
||||
case "html":
|
||||
return <DocumentChevronDoubleRegular/>
|
||||
case "json":
|
||||
return <DocumentChevronDoubleRegular/>
|
||||
case "go":
|
||||
return <DocumentChevronDoubleRegular/>
|
||||
default:
|
||||
return <DocumentBulletListRegular/>
|
||||
}
|
||||
}
|
@ -54,11 +54,19 @@ const useStyles = makeStyles({
|
||||
export function Path() {
|
||||
const styles = useStyles()
|
||||
const {conn_active} = useStoreConnection()
|
||||
const {bucket_active} = useStoreBucket()
|
||||
const {bucket_active, bucket_get, bucket_set} = useStoreBucket()
|
||||
const {prefix, files_get} = useStoreFile()
|
||||
|
||||
async function handleClickUp() {
|
||||
const dirs = prefix.split('/').filter((item => item))
|
||||
if (dirs.length > 0) {
|
||||
dirs.pop()
|
||||
files_get(conn_active!, bucket_active!, dirs.join("/"))
|
||||
return
|
||||
}
|
||||
|
||||
bucket_get(conn_active!, false)
|
||||
bucket_set(null)
|
||||
}
|
||||
|
||||
|
||||
|
123
frontend/src/component/file/upload_files.tsx
Normal file
123
frontend/src/component/file/upload_files.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
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} 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 {prefix, files_get} = useStoreFile()
|
||||
|
||||
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,37 +1,76 @@
|
||||
import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components";
|
||||
import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components";
|
||||
import {ConnectionCreate} from "../connection/new";
|
||||
import {CloudAddFilled} from "@fluentui/react-icons";
|
||||
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",
|
||||
height: "5rem",
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
alignItems: "center",
|
||||
borderBottom: "1px solid lightgray",
|
||||
},
|
||||
button_new_connection: {
|
||||
margin: '0.5rem',
|
||||
button_new: {
|
||||
margin: '0.5rem',
|
||||
},
|
||||
})
|
||||
|
||||
export function Header() {
|
||||
const styles = useStyles();
|
||||
const [openCreate, setOpenCreate] = useState(false);
|
||||
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_connection}>
|
||||
<div className={styles.button_new}>
|
||||
<Dialog
|
||||
open={openCreate}
|
||||
onOpenChange={(event, data) => setOpenCreate(data.open)}>
|
||||
open={open_create_conn}
|
||||
onOpenChange={(event, data) => set_open_create_conn(data.open)}>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="primary" icon={<CloudAddFilled/>}>
|
||||
<Button appearance="primary" icon={<PlugConnectedAddRegular/>}>
|
||||
新建连接
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ConnectionCreate openFn={setOpenCreate}/>
|
||||
<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>
|
||||
}
|
@ -3,4 +3,8 @@ export function TrimSuffix(str: string, suffix: string) {
|
||||
return str.substring(0, str.length - suffix.length);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
export function GetBaseFileName(fullPath: string) {
|
||||
return fullPath.replace(/.*[\/\\]/, '');
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ interface StoreBucket {
|
||||
bucket_set: (Bucket: Bucket | null) => 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[] };
|
||||
@ -34,5 +35,21 @@ export const useStoreBucket = create<StoreBucket>()((set) => ({
|
||||
|
||||
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}]}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
3
frontend/wailsjs/go/controller/App.d.ts
vendored
3
frontend/wailsjs/go/controller/App.d.ts
vendored
@ -1,7 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// 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>;
|
||||
|
@ -2,10 +2,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Init(arg1) {
|
||||
return window['go']['controller']['App']['Init'](arg1);
|
||||
}
|
||||
|
||||
export function Invoke(arg1, arg2) {
|
||||
return window['go']['controller']['App']['Invoke'](arg1, arg2);
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/loveuer/nf-disk
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
|
@ -24,6 +24,7 @@ func Resolve(path string) (ndh.Handler, bool) {
|
||||
}
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
register("/runtime/dialog/open", handler.DialogOpen(ctx))
|
||||
register("/api/connection/test", handler.ConnectionTest)
|
||||
register("/api/connection/create", handler.ConnectionCreate)
|
||||
register("/api/connection/list", handler.ConnectionList)
|
||||
@ -31,6 +32,8 @@ func Init(ctx context.Context) error {
|
||||
register("/api/connection/disconnect", handler.ConnectionDisconnect)
|
||||
register("/api/connection/buckets", handler.ConnectionBuckets)
|
||||
register("/api/bucket/files", handler.BucketFiles)
|
||||
register("/api/bucket/create", handler.BucketCreate)
|
||||
register("/api/file/upload", handler.FileUpload)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -9,6 +9,11 @@ import (
|
||||
"github.com/loveuer/nf-disk/internal/tool"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
app *App
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@ -16,23 +21,24 @@ type App struct {
|
||||
handlers map[string]ndh.Handler
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
func NewApp(gctx context.Context) *App {
|
||||
app = &App{
|
||||
handlers: make(map[string]ndh.Handler),
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-gctx.Done()
|
||||
runtime.Quit(app.ctx)
|
||||
}()
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *App) Init(ctx context.Context) {
|
||||
log.Info("app init!!!")
|
||||
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
log.Info("app startup!!!")
|
||||
a.ctx = ctx
|
||||
|
||||
tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
|
||||
tool.Must(model.Init(db.Default.Session()))
|
||||
tool.Must(manager.Init(ctx))
|
||||
tool.Must(api.Init(ctx))
|
||||
}
|
||||
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
log.Info("app startup!!!")
|
||||
}
|
||||
|
@ -38,3 +38,36 @@ func BucketFiles(c *ndh.Ctx) 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})
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf-disk/internal/db"
|
||||
"github.com/loveuer/nf-disk/internal/manager"
|
||||
"github.com/loveuer/nf-disk/internal/model"
|
||||
@ -211,18 +210,11 @@ func ConnectionBuckets(c *ndh.Ctx) error {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
// todo: for frontend test
|
||||
buckets = append(buckets, &s3.ListBucketRes{
|
||||
Name: "这是一个非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长的名字",
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
})
|
||||
|
||||
// todo: for frontend test
|
||||
for i := 1; i <= 500; i++ {
|
||||
buckets = append(buckets, &s3.ListBucketRes{
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Name: fmt.Sprintf("test-bucket-%03d", i),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Send200(map[string]any{"list": buckets})
|
||||
}
|
||||
|
56
internal/handler/dialog.go
Normal file
56
internal/handler/dialog.go
Normal file
@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"github.com/samber/lo"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
88
internal/handler/file.go
Normal file
88
internal/handler/file.go
Normal file
@ -0,0 +1,88 @@
|
||||
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)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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!!!")
|
||||
}
|
24
internal/model/res.go
Normal file
24
internal/model/res.go
Normal file
@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Res struct {
|
||||
Status uint32 `json:"status"`
|
||||
Err string `json:"err"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
func NewRes(status uint32, err string, msg string, data any) *Res {
|
||||
return &Res{
|
||||
Status: status,
|
||||
Err: err,
|
||||
Msg: msg,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Res) String() string {
|
||||
bs, _ := json.Marshal(r)
|
||||
return string(bs)
|
||||
}
|
36
internal/s3/create.go
Normal file
36
internal/s3/create.go
Normal file
@ -0,0 +1,36 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
|
||||
func (c *Client) CreateBucket(ctx context.Context, bucket string, publicRead bool, publicReadWrite bool) error {
|
||||
var (
|
||||
err error
|
||||
input = &s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
ACL: types.BucketCannedACLAuthenticatedRead,
|
||||
}
|
||||
|
||||
output = &s3.CreateBucketOutput{}
|
||||
)
|
||||
|
||||
if publicRead {
|
||||
input.ACL = types.BucketCannedACLPublicRead
|
||||
}
|
||||
|
||||
if publicReadWrite {
|
||||
input.ACL = types.BucketCannedACLPublicReadWrite
|
||||
}
|
||||
|
||||
if output, err = c.client.CreateBucket(ctx, input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = output
|
||||
|
||||
return nil
|
||||
}
|
62
internal/s3/put.go
Normal file
62
internal/s3/put.go
Normal file
@ -0,0 +1,62 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PutFilesObj struct {
|
||||
Key string
|
||||
Reader io.ReadSeeker
|
||||
ContentLength int64
|
||||
ContentType string
|
||||
ExpireAt int64
|
||||
PublicRead bool
|
||||
PublicReadWrite bool
|
||||
}
|
||||
|
||||
func (c *Client) PutFile(ctx context.Context, bucket string, obj *PutFilesObj) error {
|
||||
var (
|
||||
err error
|
||||
input = &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(obj.Key),
|
||||
Body: obj.Reader,
|
||||
ACL: types.ObjectCannedACLPrivate,
|
||||
}
|
||||
output *s3.PutObjectOutput
|
||||
)
|
||||
|
||||
if obj.ExpireAt > 0 {
|
||||
t := time.UnixMilli(obj.ExpireAt)
|
||||
input.Expires = &t
|
||||
}
|
||||
|
||||
if obj.ContentLength > 0 {
|
||||
input.ContentLength = aws.Int64(obj.ContentLength)
|
||||
}
|
||||
|
||||
if obj.ContentType == "" {
|
||||
input.ContentType = aws.String(obj.ContentType)
|
||||
}
|
||||
|
||||
if obj.PublicRead {
|
||||
input.ACL = types.ObjectCannedACLPublicRead
|
||||
}
|
||||
|
||||
if obj.PublicReadWrite {
|
||||
input.ACL = types.ObjectCannedACLPublicReadWrite
|
||||
}
|
||||
|
||||
if output, err = c.client.PutObject(ctx, input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = output
|
||||
|
||||
return nil
|
||||
}
|
32
internal/tool/slice.go
Normal file
32
internal/tool/slice.go
Normal file
@ -0,0 +1,32 @@
|
||||
package tool
|
||||
|
||||
import "iter"
|
||||
|
||||
func Bulk[T any](slice []T, size int) iter.Seq2[int, []T] {
|
||||
if size <= 0 {
|
||||
panic("bulk size must be positive")
|
||||
}
|
||||
|
||||
s := make([]T, 0, size)
|
||||
idx := 0
|
||||
return func(yield func(int, []T) bool) {
|
||||
for i := range slice {
|
||||
s = append(s, (slice)[i])
|
||||
if len(s) >= size {
|
||||
|
||||
// send to handle
|
||||
ok := yield(idx, s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
idx++
|
||||
s = make([]T, 0, size)
|
||||
}
|
||||
}
|
||||
|
||||
if len(s) > 0 {
|
||||
yield(idx, s)
|
||||
}
|
||||
}
|
||||
}
|
7
main.go
7
main.go
@ -18,9 +18,6 @@ import (
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer cancel()
|
||||
@ -32,9 +29,7 @@ func main() {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
}
|
||||
|
||||
app := controller.NewApp()
|
||||
|
||||
app.Init(ctx)
|
||||
app := controller.NewApp(ctx)
|
||||
|
||||
if err := wails.Run(&options.App{
|
||||
Title: "nf-disk",
|
||||
|
10
xtest/path.js
Normal file
10
xtest/path.js
Normal 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
|
Loading…
x
Reference in New Issue
Block a user