wip: list files in bucket
This commit is contained in:
parent
efe7800b59
commit
8bc2a2541d
@ -1,6 +1,16 @@
|
||||
import {Button, Input, makeStyles, MenuItem, MenuList, mergeClasses, tokens, Tooltip} from "@fluentui/react-components"
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
makeStyles,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuList, MenuPopover, MenuProps,
|
||||
mergeClasses, PositioningImperativeRef,
|
||||
tokens,
|
||||
Tooltip
|
||||
} from "@fluentui/react-components"
|
||||
import {DatabaseLinkRegular, DismissRegular} from "@fluentui/react-icons";
|
||||
import {useState} from "react";
|
||||
import React, {useState} from "react";
|
||||
import {Connection} from "../../interfaces/connection";
|
||||
import {useToast} from "../../message";
|
||||
import {Dial} from "../../api";
|
||||
@ -15,7 +25,6 @@ const useStyles = makeStyles({
|
||||
},
|
||||
content: {
|
||||
height: "100%",
|
||||
minWidth: "22rem",
|
||||
width: "25rem",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -38,7 +47,7 @@ const useStyles = makeStyles({
|
||||
items_one: {
|
||||
marginLeft: "0.5rem",
|
||||
marginRight: "0.5rem",
|
||||
"&:hover":{
|
||||
"&:hover": {
|
||||
color: tokens.colorNeutralForeground2BrandPressed,
|
||||
},
|
||||
"&.active": {
|
||||
@ -94,6 +103,11 @@ export function ConnectionList() {
|
||||
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}]`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
@ -115,14 +129,17 @@ export function ConnectionList() {
|
||||
<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}
|
||||
className={item.active ? mergeClasses(styles.items_one, "active") : styles.items_one}
|
||||
onClick={async () => {
|
||||
await handleSelect(item)
|
||||
}}
|
||||
onDoubleClick={async () => {
|
||||
await handleConnect(item)
|
||||
}}
|
||||
icon={<DatabaseLinkRegular />}
|
||||
onContextMenu={async (e) => {
|
||||
await handleRightClick(e, item)
|
||||
}}
|
||||
icon={<DatabaseLinkRegular/>}
|
||||
key={item.id}>
|
||||
{item.name}
|
||||
<Tooltip
|
||||
|
@ -1,40 +0,0 @@
|
||||
import {makeStyles, MenuItem, MenuList, tokens} from "@fluentui/react-components";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {ArchiveRegular} from "@fluentui/react-icons";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
buckets: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
},
|
||||
bucket_items: {
|
||||
width: "100%",
|
||||
},
|
||||
bucket_item: {
|
||||
width: "100%",
|
||||
"&:first-child": {
|
||||
marginTop: "0.5rem",
|
||||
},
|
||||
"&:hover": {
|
||||
color: tokens.colorNeutralForeground2BrandPressed,
|
||||
},
|
||||
"& > span:nth-child(2)": {
|
||||
maxWidth: '100% !important',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function Bucket() {
|
||||
|
||||
const styles = useStyles();
|
||||
const {bucket_list} = useStoreBucket()
|
||||
|
||||
return <div className={styles.buckets}>
|
||||
<MenuList className={styles.bucket_items}>
|
||||
{bucket_list.map(((item, idx) => {
|
||||
return <MenuItem className={styles.bucket_item} icon={<ArchiveRegular/>} style={{width: '100%'}}
|
||||
key={idx}>{item.name}</MenuItem>
|
||||
}))}
|
||||
</MenuList>
|
||||
</div>
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import {Path} from "./path";
|
||||
import {Bucket} from "./bucket";
|
||||
import {ListComponent} from "./list";
|
||||
import {makeStyles} from "@fluentui/react-components";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import {useStoreFile} from "../../store/file";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
@ -13,9 +15,17 @@ const useStyles = makeStyles({
|
||||
})
|
||||
|
||||
export function Content() {
|
||||
|
||||
const styles = useStyles()
|
||||
const {bucket_list} = useStoreBucket()
|
||||
const {bucket, file_list} = useStoreFile()
|
||||
|
||||
return <div className={styles.content}>
|
||||
<Path />
|
||||
<Bucket />
|
||||
<Path/>
|
||||
{
|
||||
bucket.name ?
|
||||
<ListComponent type={'file'} list={file_list.map(item => item.name)}/> :
|
||||
<ListComponent type={'bucket'} list={bucket_list.map(item => item.name)}/>
|
||||
}
|
||||
</div>
|
||||
}
|
73
frontend/src/component/file/list.tsx
Normal file
73
frontend/src/component/file/list.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
|
||||
import {ArchiveRegular, DocumentBulletListRegular} from "@fluentui/react-icons";
|
||||
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
marginTop: '0.5rem',
|
||||
maxWidth: 'calc(100vw - 25rem - 1px)',
|
||||
},
|
||||
row: {
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
marginLeft: '0.5rem',
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
item: {
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
"&:hover": {
|
||||
color: tokens.colorNeutralForeground2BrandPressed,
|
||||
}
|
||||
},
|
||||
text: {
|
||||
overflow: 'hidden',
|
||||
width: 'calc(100vw - 32rem)',
|
||||
display: "block",
|
||||
}
|
||||
})
|
||||
|
||||
export interface ListComponentProps {
|
||||
type: "bucket" | "file"
|
||||
list: string[],
|
||||
}
|
||||
|
||||
export function ListComponent(props: ListComponentProps) {
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
async function handleClick(item: string) {
|
||||
console.log('[DEBUG] bucket click =', item);
|
||||
}
|
||||
|
||||
function handleRightClick(e: React.MouseEvent<HTMLDivElement>, string: string) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
return <MenuList className={styles.container}>
|
||||
<VirtualizerScrollView
|
||||
numItems={props.list.length}
|
||||
itemSize={32}
|
||||
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
|
||||
>
|
||||
{(idx) => {
|
||||
return <div
|
||||
className={styles.row} key={idx}
|
||||
onClick={async () => {
|
||||
await handleClick(props.list[idx])
|
||||
}}
|
||||
onContextMenu={async (e) => {
|
||||
handleRightClick(e, props.list[idx])
|
||||
}}>
|
||||
<MenuItem className={styles.item}
|
||||
icon={props.type === 'bucket' ? <ArchiveRegular/> : <DocumentBulletListRegular/>}>
|
||||
<Text truncate wrap={false} className={styles.text}>
|
||||
{props.list[idx]}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</div>
|
||||
}}
|
||||
</VirtualizerScrollView>
|
||||
</MenuList>
|
||||
}
|
@ -11,4 +11,7 @@ export interface Connection {
|
||||
export interface Bucket {
|
||||
name: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface S3File {
|
||||
name:string;
|
||||
}
|
12
frontend/src/store/file.tsx
Normal file
12
frontend/src/store/file.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import {create} from 'zustand'
|
||||
import {Bucket, S3File} from "../interfaces/connection";
|
||||
|
||||
interface StoreFile {
|
||||
bucket: Bucket;
|
||||
file_list: S3File[];
|
||||
}
|
||||
|
||||
export const useStoreFile = create<StoreFile>()((set) => ({
|
||||
bucket: {name: '', created_at: 0},
|
||||
file_list: [],
|
||||
}))
|
@ -30,6 +30,7 @@ func Init(ctx context.Context) error {
|
||||
register("/api/connection/connect", handler.ConnectionConnect)
|
||||
register("/api/connection/disconnect", handler.ConnectionDisconnect)
|
||||
register("/api/connection/buckets", handler.ConnectionBuckets)
|
||||
register("/api/bucket/file", handler.BucketFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
35
internal/handler/bucket.go
Normal file
35
internal/handler/bucket.go
Normal file
@ -0,0 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf-disk/internal/manager"
|
||||
"github.com/loveuer/nf-disk/internal/s3"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
)
|
||||
|
||||
func BucketFile(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
ConnId uint64 `json:"conn_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(err.Error())
|
||||
}
|
||||
|
||||
if req.ConnId == 0 || req.Bucket == "" {
|
||||
return c.Send400(req, "缺少参数")
|
||||
}
|
||||
|
||||
if _, client, err = manager.Manager.Use(req.ConnId); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
client.ListFile()
|
||||
}
|
@ -2,12 +2,14 @@ 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"
|
||||
"github.com/loveuer/nf-disk/internal/s3"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"github.com/samber/lo"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ConnectionTest(c *ndh.Ctx) error {
|
||||
@ -209,5 +211,18 @@ func ConnectionBuckets(c *ndh.Ctx) error {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ListBucketRes struct {
|
||||
@ -13,6 +15,12 @@ type ListBucketRes struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ListFileRes struct {
|
||||
Name string
|
||||
LastModified time.Time
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (c *Client) ListBucket(ctx context.Context) ([]*ListBucketRes, error) {
|
||||
var (
|
||||
err error
|
||||
@ -35,3 +43,46 @@ func (c *Client) ListBucket(ctx context.Context) ([]*ListBucketRes, error) {
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListFile(ctx context.Context, bucket string, prefix string, parent string) ([]*ListFileRes, error) {
|
||||
var (
|
||||
err error
|
||||
input = &s3.ListObjectsV2Input{
|
||||
Delimiter: aws.String("/"),
|
||||
MaxKeys: aws.Int32(1000),
|
||||
Bucket: aws.String(bucket),
|
||||
}
|
||||
output *s3.ListObjectsV2Output
|
||||
)
|
||||
|
||||
if prefix != "" {
|
||||
input.Prefix = aws.String(prefix)
|
||||
}
|
||||
|
||||
if output, err = c.client.ListObjectsV2(ctx, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folder := lo.FilterMap(
|
||||
output.CommonPrefixes,
|
||||
func(item types.CommonPrefix, index int) (*ListFileRes, bool) {
|
||||
name := strings.TrimPrefix(*item.Prefix, parent)
|
||||
return &ListFileRes{
|
||||
Name: name,
|
||||
}, name != ""
|
||||
},
|
||||
)
|
||||
|
||||
list := lo.Map(
|
||||
output.Contents,
|
||||
func(item types.Object, index int) *ListFileRes {
|
||||
return &ListFileRes{
|
||||
Name: *item.Key,
|
||||
LastModified: *item.LastModified,
|
||||
Size: *item.Size,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return append(folder, list...), nil
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
_, err := New(context.TODO(), "http://10.220.10.15:9000/", "8ALV3DUZI31YG4BDRJ0Z", "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H")
|
||||
if err != nil {
|
||||
t.Fatalf("call s3.New err = %s", err.Error())
|
||||
}
|
||||
}
|
@ -14,11 +14,10 @@ import (
|
||||
|
||||
type resolverV2 struct{}
|
||||
|
||||
func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (
|
||||
smithyendpoints.Endpoint, error,
|
||||
) {
|
||||
func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (smithyendpoints.Endpoint, error) {
|
||||
u, err := url.Parse(*params.Endpoint)
|
||||
if err != nil {
|
||||
log.Warn("resolver v2: parse url = %s, err = %s", params.Endpoint, err.Error())
|
||||
return smithyendpoints.Endpoint{}, err
|
||||
}
|
||||
return smithyendpoints.Endpoint{
|
||||
@ -38,14 +37,25 @@ func New(ctx context.Context, endpoint string, access string, key string) (*Clie
|
||||
output *s3.ListBucketsOutput
|
||||
)
|
||||
|
||||
if sdkConfig, err = config.LoadDefaultConfig(ctx); err != nil {
|
||||
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
return aws.Endpoint{
|
||||
URL: endpoint,
|
||||
}, nil
|
||||
})
|
||||
|
||||
if sdkConfig, err = config.LoadDefaultConfig(
|
||||
ctx,
|
||||
config.WithEndpointResolverWithOptions(customResolver),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
o.EndpointResolverV2 = &resolverV2{}
|
||||
//o.BaseEndpoint = aws.String(endpoint)
|
||||
//o.EndpointResolverV2 = &resolverV2{}
|
||||
o.Credentials = aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(access, key, ""))
|
||||
o.UsePathStyle = true
|
||||
o.Region = "auto"
|
||||
})
|
||||
|
||||
if output, err = s3Client.ListBuckets(tool.Timeout(5), &s3.ListBucketsInput{
|
37
internal/s3/s3_test.go
Normal file
37
internal/s3/s3_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/internal/tool"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
_, err := New(context.TODO(), "http://10.220.10.15:9000/", "8ALV3DUZI31YG4BDRJ0Z", "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H")
|
||||
if err != nil {
|
||||
t.Fatalf("call s3.New err = %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFile(t *testing.T) {
|
||||
//log.SetLogLevel(log.LogLevelDebug)
|
||||
|
||||
//cli, err := New(context.TODO(), "http://10.220.10.14:19000", "5VCR05L4BSGNCTCD8DXP", "FPTMYBEiHhWLJ05C3aGXW8bjFXXNmghc8Za3Fo2u")
|
||||
cli, err := New(context.TODO(), "http://10.220.10.15:9000/", "8ALV3DUZI31YG4BDRJ0Z", "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H")
|
||||
if err != nil {
|
||||
t.Fatalf("call s3.New err = %s", err.Error())
|
||||
}
|
||||
|
||||
files, err := cli.ListFile(tool.Timeout(30), "infobox-person", "")
|
||||
if err != nil {
|
||||
t.Fatalf("call s3.ListFile err = %s", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("[x] file length = %d", len(files))
|
||||
|
||||
for _, item := range files {
|
||||
t.Logf("[x] file = %s, size = %d", item.Name, item.Size)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user