diff --git a/frontend/src/component/connection/list.tsx b/frontend/src/component/connection/list.tsx index ac147da..5660283 100644 --- a/frontend/src/component/connection/list.tsx +++ b/frontend/src/component/connection/list.tsx @@ -9,7 +9,13 @@ import { tokens, Tooltip } from "@fluentui/react-components" -import {DatabaseLinkRegular, DismissRegular} from "@fluentui/react-icons"; +import { + DatabaseLinkRegular, + DeleteRegular, + DismissRegular, + PlugConnectedRegular, + SettingsRegular +} from "@fluentui/react-icons"; import React, { useEffect, useState} from "react"; import { Connection} from "../../interfaces/connection"; import {useToast} from "../../message"; @@ -92,6 +98,7 @@ export function ConnectionList() { y: number, display: 'none' | 'block' }>({x: 0, y: 0, display: 'none'}); + const [menu_conn, set_menu_conn] = useState(null); useEffect(() => { document.addEventListener("click", (e) => { @@ -112,7 +119,8 @@ export function ConnectionList() { }) } - async function handleConnect(item: Connection) { + 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") @@ -137,6 +145,7 @@ export function ConnectionList() { 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, @@ -164,9 +173,11 @@ export function ConnectionList() { style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}} > - 连接 - 设置 - 删除 + {await handleConnect(menu_conn)}} + icon={}>连接 + }>设置 + }>删除
diff --git a/frontend/src/component/preview/preview.tsx b/frontend/src/component/preview/preview.tsx new file mode 100644 index 0000000..f8a75f6 --- /dev/null +++ b/frontend/src/component/preview/preview.tsx @@ -0,0 +1,3 @@ +export function PreviewFile() { + +} \ No newline at end of file diff --git a/frontend/src/store/file.tsx b/frontend/src/store/file.tsx index 1c47074..d7f6a9b 100644 --- a/frontend/src/store/file.tsx +++ b/frontend/src/store/file.tsx @@ -4,12 +4,14 @@ import {Dial} from "../api"; interface StoreFile { prefix: string; + filter:string; files_list: S3File[]; files_get: (conn: Connection, bucket: Bucket, prefix?: string, filter?: string) => void; } export const useStoreFile = create()((set) => ({ prefix: "", + filter: "", files_list: [], files_get: async (conn: Connection, bucket: Bucket, prefix = '', filter = '') => { const res = await Dial<{ list: S3File[] }>('/api/bucket/files', { @@ -23,7 +25,7 @@ export const useStoreFile = create()((set) => ({ } set((state) => { - return {files_list: res.data.list, prefix: prefix} + return {files_list: res.data.list, prefix: prefix, filter: filter} }) } })) diff --git a/frontend/src/store/preview.tsx b/frontend/src/store/preview.tsx new file mode 100644 index 0000000..5324dbe --- /dev/null +++ b/frontend/src/store/preview.tsx @@ -0,0 +1,18 @@ + +import {create} from 'zustand' +import {Bucket, Connection} from "../interfaces/connection"; + +interface StorePreview { + preview_key: string; + preview_url: string; + preview_content_type: string; + preview_set: (conn:Connection,bucket: Bucket,key: string) => void; +} +export const useStoreFile = create()((set) => ({ + preview_key: '', + preview_url: '', + preview_content_type: '', + preview_set: (conn:Connection,bucket: Bucket,key: string) => set(state => { + return {preview_key: key} + }), +})) diff --git a/internal/api/api.go b/internal/api/api.go index dcdce5e..ed6bec6 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -34,6 +34,8 @@ func Init(ctx context.Context) error { register("/api/bucket/files", handler.BucketFiles) register("/api/bucket/create", handler.BucketCreate) register("/api/file/upload", handler.FileUpload) + register("/api/file/info", handler.FileInfo) + register("/api/file/get", handler.FileGet) return nil } diff --git a/internal/handler/file.go b/internal/handler/file.go index 95c5d85..0f1bdb0 100644 --- a/internal/handler/file.go +++ b/internal/handler/file.go @@ -86,3 +86,61 @@ func FileUpload(c *ndh.Ctx) error { return c.Send200(req) } + +func FileInfo(c *ndh.Ctx) error { + type Req struct { + ConnId uint64 `json:"conn_id"` + Bucket string `json:"bucket"` + Key string `json:"key"` + } + + var ( + err error + req = new(Req) + client *s3.Client + info *s3.ObjectInfo + ) + + if err = c.ReqParse(req); err != nil { + return c.Send400(err.Error()) + } + + if _, client, err = manager.Manager.Use(req.ConnId); err != nil { + return c.Send500(err.Error()) + } + + if info, err = client.GetObjectInfo(c.Context(), req.Bucket, req.Key); err != nil { + return c.Send500(err.Error()) + } + + return c.Send200(info) +} + +func FileGet(c *ndh.Ctx) error { + type Req struct { + ConnId uint64 `json:"conn_id"` + Bucket string `json:"bucket"` + Key string `json:"key"` + } + + var ( + err error + req = new(Req) + client *s3.Client + link *s3.ObjectEntry + ) + + if err = c.ReqParse(req); err != nil { + return c.Send400(err.Error()) + } + + if _, client, err = manager.Manager.Use(req.ConnId); err != nil { + return c.Send500(err.Error()) + } + + if link, err = client.GetObject(c.Context(), req.Bucket, req.Key); err != nil { + return c.Send500(err.Error()) + } + + return c.Send200(link) +} diff --git a/internal/s3/get.go b/internal/s3/get.go new file mode 100644 index 0000000..52ede09 --- /dev/null +++ b/internal/s3/get.go @@ -0,0 +1,90 @@ +package s3 + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/labstack/gommon/log" + "net/http" + "time" +) + +type ObjectInfo struct { + Bucket string + Key string + ContentType string + Expire int64 +} + +func (c *Client) GetObjectInfo(ctx context.Context, bucket string, key string) (*ObjectInfo, error) { + var ( + err error + input = &s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + } + output *s3.HeadObjectOutput + ) + + if output, err = c.client.HeadObject(ctx, input); err != nil { + return nil, err + } + + return &ObjectInfo{ + Bucket: bucket, + Key: key, + ContentType: aws.ToString(output.ContentType), + Expire: aws.ToTime(output.Expires).UnixMilli(), + }, nil +} + +// GetObject +// https://docs.aws.amazon.com/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html + +type Presigner struct { + PresignClient *s3.PresignClient +} + +func (presigner *Presigner) GetObject(ctx context.Context, bucketName string, objectKey string, lifetimeSecs int64) (*v4.PresignedHTTPRequest, error) { + request, err := presigner.PresignClient.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + }, func(opts *s3.PresignOptions) { + opts.Expires = time.Duration(lifetimeSecs * int64(time.Second)) + }) + if err != nil { + log.Error("Presigner: couldn't get a presigned request to get %v:%v. Here's why: %v\n", + bucketName, objectKey, err) + } + return request, err +} + +type ObjectEntry struct { + URL string + Method string + Header http.Header +} + +func (c *Client) GetObject(ctx context.Context, bucket string, key string, lifetimes ...int64) (*ObjectEntry, error) { + var ( + err error + lifetime int64 = 5 * 60 + pc = &Presigner{PresignClient: s3.NewPresignClient(c.client)} + output *v4.PresignedHTTPRequest + ) + + if len(lifetimes) > 0 && lifetimes[0] > 0 { + lifetime = lifetimes[0] + } + + if output, err = pc.GetObject(ctx, bucket, key, lifetime); err != nil { + return nil, err + } + + return &ObjectEntry{ + URL: output.URL, + Method: output.Method, + Header: output.SignedHeader, + }, nil +}