feat: 完成了 新建桶; 上传文件(基本功能)

todo: 上传 rename, 上传 public 权限选择
bug: 首次加载 conns list; 上传的时候前缀过滤失败
This commit is contained in:
zhaoyupeng 2024-10-12 17:35:59 +08:00
parent 1c818daf16
commit 777253063b
28 changed files with 791 additions and 96 deletions

View File

@ -1 +1 @@
b20ef5a27687e07e09878451f9a2e1aa
674272f31b23a798d77a17321c6a8785

View File

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

View 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>
</>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>
</>
}

View File

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

View File

@ -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(/.*[\/\\]/, '');
}

View File

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

View File

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

View File

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

@ -1,6 +1,6 @@
module github.com/loveuer/nf-disk
go 1.21
go 1.22
toolchain go1.23.0

View File

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

View File

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

View File

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

View File

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

View 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
View 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)
}

View File

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

View File

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