🎉 开始项目
feat: 完成基础界面; 列表展示 todo: uplevel button function todo: download/upload
This commit is contained in:
36
internal/api/api.go
Normal file
36
internal/api/api.go
Normal file
@ -0,0 +1,36 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/internal/handler"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
apis = make(map[string]ndh.Handler)
|
||||
)
|
||||
|
||||
func register(path string, h ndh.Handler) {
|
||||
name := reflect.ValueOf(h).String()
|
||||
log.Info("app register: path = %s, name = %s", path, name)
|
||||
apis[path] = h
|
||||
}
|
||||
|
||||
func Resolve(path string) (ndh.Handler, bool) {
|
||||
h, ok := apis[path]
|
||||
return h, ok
|
||||
}
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
register("/api/connection/test", handler.ConnectionTest)
|
||||
register("/api/connection/create", handler.ConnectionCreate)
|
||||
register("/api/connection/list", handler.ConnectionList)
|
||||
register("/api/connection/connect", handler.ConnectionConnect)
|
||||
register("/api/connection/disconnect", handler.ConnectionDisconnect)
|
||||
register("/api/connection/buckets", handler.ConnectionBuckets)
|
||||
register("/api/bucket/files", handler.BucketFiles)
|
||||
|
||||
return nil
|
||||
}
|
38
internal/controller/app.go
Normal file
38
internal/controller/app.go
Normal file
@ -0,0 +1,38 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/internal/api"
|
||||
"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/tool"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
handlers map[string]ndh.Handler
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
handlers: make(map[string]ndh.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Init(ctx context.Context) {
|
||||
log.Info("app init!!!")
|
||||
|
||||
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!!!")
|
||||
}
|
51
internal/controller/invoke.go
Normal file
51
internal/controller/invoke.go
Normal file
@ -0,0 +1,51 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf-disk/internal/api"
|
||||
"github.com/loveuer/nf-disk/internal/opt"
|
||||
"github.com/loveuer/nf-disk/internal/tool"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func handleError(err error) string {
|
||||
bs, _ := json.Marshal(map[string]any{
|
||||
"err": err.Error(),
|
||||
"msg": opt.Msg500,
|
||||
"status": 500,
|
||||
})
|
||||
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func handleNotFound(path string) string {
|
||||
bs, _ := json.Marshal(map[string]any{
|
||||
"err": fmt.Sprintf("path not found, path: %s", path),
|
||||
"msg": opt.Msg500,
|
||||
"status": 404,
|
||||
})
|
||||
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func (a *App) Invoke(path string, req string) (res string) {
|
||||
log.Info("app invoke: path = %s, req = %s", path, req)
|
||||
handler, ok := api.Resolve(path)
|
||||
if !ok {
|
||||
log.Warn("app invoke: path not found, path = %s", path)
|
||||
return handleNotFound(path)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
ctx := ndh.NewCtx(tool.TimeoutCtx(a.ctx), strings.NewReader(req), &buf)
|
||||
|
||||
if err := handler(ctx); err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
61
internal/db/client.go
Normal file
61
internal/db/client.go
Normal file
@ -0,0 +1,61 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/internal/opt"
|
||||
"github.com/loveuer/nf-disk/internal/tool"
|
||||
"io"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
Default *Client
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
cli *gorm.DB
|
||||
ttype string
|
||||
cfgSqlite *cfgSqlite
|
||||
}
|
||||
|
||||
func (c *Client) Type() string {
|
||||
return c.ttype
|
||||
}
|
||||
|
||||
func (c *Client) Session(ctxs ...context.Context) *gorm.DB {
|
||||
var ctx context.Context
|
||||
if len(ctxs) > 0 && ctxs[0] != nil {
|
||||
ctx = ctxs[0]
|
||||
} else {
|
||||
ctx = tool.Timeout(30)
|
||||
}
|
||||
|
||||
session := c.cli.Session(&gorm.Session{Context: ctx})
|
||||
|
||||
if opt.Debug {
|
||||
session = session.Debug()
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
d, _ := c.cli.DB()
|
||||
d.Close()
|
||||
}
|
||||
|
||||
// Dump
|
||||
// Only for sqlite with mem mode to dump data to bytes(io.Reader)
|
||||
func (c *Client) Dump() (reader io.ReadSeekCloser, ok bool) {
|
||||
if c.ttype != "sqlite" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if c.cfgSqlite.fsType != "mem" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return c.cfgSqlite.memDump.Dump(), true
|
||||
}
|
44
internal/db/db_test.go
Normal file
44
internal/db/db_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
myClient, err := New(context.TODO(), "sqlite::", OptSqliteByMem())
|
||||
if err != nil {
|
||||
t.Fatalf("TestOpen: New err = %v", err)
|
||||
}
|
||||
|
||||
type Start struct {
|
||||
Id int `json:"id" gorm:"column:id;primaryKey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Dis float64 `json:"dis" gorm:"column:dis"`
|
||||
}
|
||||
|
||||
if err = myClient.Session().AutoMigrate(&Start{}); err != nil {
|
||||
t.Fatalf("TestOpen: AutoMigrate err = %v", err)
|
||||
}
|
||||
|
||||
if err = myClient.Session().Create(&Start{Name: "sun", Dis: 6631.76}).Error; err != nil {
|
||||
t.Fatalf("TestOpen: Create err = %v", err)
|
||||
}
|
||||
|
||||
if err = myClient.Session().Create(&Start{Name: "mar", Dis: 786.35}).Error; err != nil {
|
||||
t.Fatalf("TestOpen: Create err = %v", err)
|
||||
}
|
||||
|
||||
if reader, ok := myClient.Dump(); ok {
|
||||
bs, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("TestOpen: ReadAll err = %v", err)
|
||||
}
|
||||
|
||||
os.WriteFile("dump.db", bs, 0644)
|
||||
}
|
||||
|
||||
myClient.Close()
|
||||
}
|
54
internal/db/init.go
Normal file
54
internal/db/init.go
Normal file
@ -0,0 +1,54 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, uri string, opts ...Option) (*Client, error) {
|
||||
strs := strings.Split(uri, "::")
|
||||
|
||||
if len(strs) != 2 {
|
||||
return nil, fmt.Errorf("db.Init: opt db uri invalid: %s", uri)
|
||||
}
|
||||
|
||||
c := &Client{ttype: strs[0], cfgSqlite: &cfgSqlite{fsType: "file"}}
|
||||
for _, f := range opts {
|
||||
f(c)
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
dsn = strs[1]
|
||||
)
|
||||
|
||||
switch strs[0] {
|
||||
case "sqlite":
|
||||
err = openSqlite(c, dsn)
|
||||
case "mysql":
|
||||
c.cli, err = gorm.Open(mysql.Open(dsn))
|
||||
case "postgres":
|
||||
c.cli, err = gorm.Open(postgres.Open(dsn))
|
||||
default:
|
||||
return nil, fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", strs[0])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", strs[0], dsn, err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Init(ctx context.Context, uri string, opts ...Option) (err error) {
|
||||
if Default, err = New(ctx, uri, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
27
internal/db/option.go
Normal file
27
internal/db/option.go
Normal file
@ -0,0 +1,27 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
_ "github.com/loveuer/go-sqlite3/embed"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Option func(c *Client)
|
||||
|
||||
func OptSqliteByUrl(address string) Option {
|
||||
return func(c *Client) {
|
||||
c.cfgSqlite.fsType = "url"
|
||||
}
|
||||
}
|
||||
|
||||
type SqliteMemDumper interface {
|
||||
Dump() io.ReadSeekCloser
|
||||
}
|
||||
|
||||
// 如果传 nil 则表示新生成一个 mem 的 sqlite
|
||||
// 如果传了一个合法的 reader 则会从这个 reader 初始化 database
|
||||
func OptSqliteByMem(reader io.ReadCloser) Option {
|
||||
return func(c *Client) {
|
||||
c.cfgSqlite.memReader = reader
|
||||
c.cfgSqlite.fsType = "mem"
|
||||
}
|
||||
}
|
63
internal/db/sqlite.go
Normal file
63
internal/db/sqlite.go
Normal file
@ -0,0 +1,63 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
_ "github.com/loveuer/go-sqlite3/embed"
|
||||
"github.com/loveuer/go-sqlite3/vfs/memdb"
|
||||
"github.com/loveuer/go-sqlite3/vfs/readervfs"
|
||||
"github.com/ncruces/go-sqlite3/gormlite"
|
||||
"github.com/psanford/httpreadat"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type cfgSqlite struct {
|
||||
fsType string // file, mem(bytes), url
|
||||
memDump *memdb.MemDB
|
||||
memReader io.ReadCloser
|
||||
}
|
||||
|
||||
func openSqlite(c *Client, dsn string) error {
|
||||
var (
|
||||
db gorm.Dialector
|
||||
err error
|
||||
)
|
||||
|
||||
switch c.cfgSqlite.fsType {
|
||||
case "file":
|
||||
db = gormlite.Open("file:" + dsn)
|
||||
case "url":
|
||||
name := fmt.Sprintf("%d.db", time.Now().UnixNano())
|
||||
readervfs.Create(name, httpreadat.New(dsn))
|
||||
uri := fmt.Sprintf("file:%s?vfs=reader", name)
|
||||
db = gormlite.Open(uri)
|
||||
case "mem":
|
||||
var (
|
||||
bs []byte
|
||||
name = fmt.Sprintf("%d.db", time.Now().UnixNano())
|
||||
)
|
||||
|
||||
if c.cfgSqlite.memReader == nil {
|
||||
bs = make([]byte, 0)
|
||||
} else {
|
||||
if bs, err = io.ReadAll(c.cfgSqlite.memReader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
memDump := memdb.Create(name, bs)
|
||||
c.cfgSqlite.memDump = memDump
|
||||
uri := fmt.Sprintf("file:/%s?vfs=memdb", name)
|
||||
db = gormlite.Open(uri)
|
||||
default:
|
||||
return fmt.Errorf("unsupported sqlite fs type: %s", c.cfgSqlite.fsType)
|
||||
}
|
||||
|
||||
if c.cli, err = gorm.Open(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
40
internal/handler/bucket.go
Normal file
40
internal/handler/bucket.go
Normal file
@ -0,0 +1,40 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf-disk/internal/manager"
|
||||
"github.com/loveuer/nf-disk/internal/s3"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
)
|
||||
|
||||
func BucketFiles(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
ConnId uint64 `json:"conn_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
list []*s3.ListFileRes
|
||||
)
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
if list, err = client.ListFile(c.Context(), req.Bucket, req.Prefix); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
return c.Send200(map[string]any{"list": list})
|
||||
}
|
228
internal/handler/connection.go
Normal file
228
internal/handler/connection.go
Normal file
@ -0,0 +1,228 @@
|
||||
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 {
|
||||
type Req struct {
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Access string `json:"access"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Endpoint == "" || req.Access == "" || req.Key == "" {
|
||||
return c.Send400(nil, "endpoint, secret_access, secret_key 是必填项")
|
||||
}
|
||||
|
||||
if _, err = s3.New(c.Context(), req.Endpoint, req.Access, req.Key); err != nil {
|
||||
return c.Send500(err.Error(), "连接失败")
|
||||
}
|
||||
|
||||
return c.Send200("连接测试成功")
|
||||
}
|
||||
|
||||
func ConnectionCreate(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Access string `json:"access"`
|
||||
Key string `json:"key"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Endpoint == "" || req.Access == "" || req.Key == "" {
|
||||
return c.Send400(nil, "endpoint, secret_access, secret_key 是必填项")
|
||||
}
|
||||
|
||||
if client, err = s3.New(c.Context(), req.Endpoint, req.Access, req.Key); err != nil {
|
||||
return c.Send500(err.Error(), "连接失败")
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
req.Name = req.Endpoint
|
||||
}
|
||||
|
||||
connection := &model.Connection{
|
||||
Name: req.Name,
|
||||
Endpoint: req.Endpoint,
|
||||
Access: req.Access,
|
||||
Key: req.Key,
|
||||
}
|
||||
|
||||
if err = connection.Create(db.Default.Session()); err != nil {
|
||||
return c.Send500(err.Error(), "创建连接失败(1)")
|
||||
}
|
||||
|
||||
if err = manager.Manager.Register(connection, client); err != nil {
|
||||
return c.Send500(err.Error(), "创建连接失败(2)")
|
||||
}
|
||||
|
||||
return c.Send200(connection, "创建连接成功")
|
||||
}
|
||||
|
||||
func ConnectionList(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
list = make([]*model.Connection, 0)
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(nil, "参数错误")
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Model(&model.Connection{}).
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listMap := lo.SliceToMap(list, func(item *model.Connection) (uint64, *model.Connection) {
|
||||
return item.Id, item
|
||||
})
|
||||
|
||||
manager.Manager.Map(func(c *model.Connection, s *s3.Client) error {
|
||||
if item, ok := listMap[c.Id]; ok {
|
||||
item.Active = true
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return c.Send200(map[string]any{"list": list})
|
||||
}
|
||||
|
||||
func ConnectionConnect(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(req)
|
||||
}
|
||||
|
||||
conn := &model.Connection{Id: req.Id}
|
||||
if err = conn.Get(db.Default.Session(), c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if client, err = s3.New(c.Context(), conn.Endpoint, conn.Access, conn.Key); err != nil {
|
||||
return c.Send500(err.Error(), "连接失败")
|
||||
}
|
||||
|
||||
if err = manager.Manager.Register(conn, client); err != nil {
|
||||
return c.Send500(err.Error(), "连接失败")
|
||||
}
|
||||
|
||||
return c.Send200(conn, "连接成功")
|
||||
}
|
||||
|
||||
func ConnectionDisconnect(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(req)
|
||||
}
|
||||
|
||||
conn := &model.Connection{Id: req.Id}
|
||||
if err = conn.Get(db.Default.Session(), c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = manager.Manager.UnRegister(conn.Id); err != nil {
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
return c.Send200(conn)
|
||||
}
|
||||
|
||||
func ConnectionBuckets(c *ndh.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client *s3.Client
|
||||
buckets []*s3.ListBucketRes
|
||||
)
|
||||
|
||||
if err = c.ReqParse(req); err != nil {
|
||||
return c.Send400(nil, "参数错误")
|
||||
}
|
||||
|
||||
if _, client, err = manager.Manager.Use(req.Id); err != nil {
|
||||
if errors.Is(err, manager.ErrNotFound) {
|
||||
return c.Send400(nil, "所选连接未激活")
|
||||
}
|
||||
|
||||
return c.Send500(err.Error())
|
||||
}
|
||||
|
||||
if buckets, err = client.ListBucket(c.Context()); err != nil {
|
||||
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})
|
||||
}
|
21
internal/handler/item.go
Normal file
21
internal/handler/item.go
Normal file
@ -0,0 +1,21 @@
|
||||
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!!!")
|
||||
}
|
7
internal/manager/error.go
Normal file
7
internal/manager/error.go
Normal file
@ -0,0 +1,7 @@
|
||||
package manager
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
75
internal/manager/manager.go
Normal file
75
internal/manager/manager.go
Normal file
@ -0,0 +1,75 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf-disk/internal/model"
|
||||
"github.com/loveuer/nf-disk/internal/s3"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type client struct {
|
||||
conn *model.Connection
|
||||
client *s3.Client
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
sync.Mutex
|
||||
clients map[uint64]*client
|
||||
}
|
||||
|
||||
var (
|
||||
Manager *manager
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
Manager = &manager{
|
||||
clients: make(map[uint64]*client),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) Register(c *model.Connection, s *s3.Client) error {
|
||||
log.Debug("manager: register connection-client: id = %d, name = %s", c.Id, c.Name)
|
||||
|
||||
Manager.Lock()
|
||||
defer Manager.Unlock()
|
||||
Manager.clients[c.Id] = &client{conn: c, client: s}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) UnRegister(id uint64) error {
|
||||
Manager.Lock()
|
||||
defer Manager.Unlock()
|
||||
c, ok := m.clients[id]
|
||||
if !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
log.Debug("manager: register connection-client: id = %d, name = %s", c.conn, c.conn.Name)
|
||||
|
||||
delete(m.clients, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) Map(fn func(*model.Connection, *s3.Client) error) error {
|
||||
for _, item := range m.clients {
|
||||
if err := fn(item.conn, item.client); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) Use(id uint64) (*model.Connection, *s3.Client, error) {
|
||||
c, ok := m.clients[id]
|
||||
if !ok {
|
||||
return nil, nil, ErrNotFound
|
||||
}
|
||||
|
||||
return c.conn, c.client, nil
|
||||
}
|
34
internal/model/init.go
Normal file
34
internal/model/init.go
Normal file
@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf-disk/internal/opt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func Init(tx *gorm.DB) (err error) {
|
||||
err = tx.AutoMigrate(
|
||||
&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
|
||||
}
|
36
internal/model/s3.go
Normal file
36
internal/model/s3.go
Normal file
@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/loveuer/nf-disk/ndh"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Connection struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
Name string `json:"name" gorm:"unique;column:name"`
|
||||
Endpoint string `json:"endpoint" gorm:"column:endpoint"`
|
||||
Access string `json:"access" gorm:"column:access"`
|
||||
Key string `json:"key" gorm:"column:key"`
|
||||
|
||||
Active bool `json:"active" gorm:"-"`
|
||||
}
|
||||
|
||||
func (c *Connection) Create(tx *gorm.DB) error {
|
||||
return tx.Create(c).Error
|
||||
}
|
||||
|
||||
func (c *Connection) Get(tx *gorm.DB, ctx *ndh.Ctx) error {
|
||||
if err := tx.Take(c, c.Id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ctx.Send400(err.Error())
|
||||
}
|
||||
|
||||
return ctx.Send500(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
11
internal/opt/var.go
Normal file
11
internal/opt/var.go
Normal file
@ -0,0 +1,11 @@
|
||||
package opt
|
||||
|
||||
const (
|
||||
Msg200 = "操作成功"
|
||||
Msg400 = "输入不正确"
|
||||
Msg500 = "发生错误"
|
||||
)
|
||||
|
||||
var (
|
||||
Debug bool = false
|
||||
)
|
101
internal/s3/list.go
Normal file
101
internal/s3/list.go
Normal file
@ -0,0 +1,101 @@
|
||||
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"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ListBucketRes struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ListFileType int64
|
||||
|
||||
const (
|
||||
ListFileTypeFile ListFileType = iota
|
||||
ListFileTypeDir
|
||||
)
|
||||
|
||||
type ListFileRes struct {
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
LastModified time.Time `json:"last_modified"`
|
||||
Size int64 `json:"size"`
|
||||
Type ListFileType `json:"type"`
|
||||
}
|
||||
|
||||
func (c *Client) ListBucket(ctx context.Context) ([]*ListBucketRes, error) {
|
||||
var (
|
||||
err error
|
||||
input = &s3.ListBucketsInput{
|
||||
MaxBuckets: aws.Int32(100),
|
||||
}
|
||||
output *s3.ListBucketsOutput
|
||||
)
|
||||
|
||||
if output, err = c.client.ListBuckets(ctx, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := lo.Map(
|
||||
output.Buckets,
|
||||
func(item types.Bucket, index int) *ListBucketRes {
|
||||
return &ListBucketRes{CreatedAt: item.CreationDate.UnixMilli(), Name: *item.Name}
|
||||
},
|
||||
)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListFile(ctx context.Context, bucket string, prefix 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, prefix)
|
||||
return &ListFileRes{
|
||||
Name: name,
|
||||
Key: *item.Prefix,
|
||||
Type: ListFileTypeDir,
|
||||
}, name != ""
|
||||
},
|
||||
)
|
||||
|
||||
list := lo.Map(
|
||||
output.Contents,
|
||||
func(item types.Object, index int) *ListFileRes {
|
||||
return &ListFileRes{
|
||||
Key: strings.Clone(*item.Key),
|
||||
Name: strings.TrimPrefix(*item.Key, prefix),
|
||||
LastModified: *item.LastModified,
|
||||
Size: *item.Size,
|
||||
Type: ListFileTypeFile,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return append(folder, list...), nil
|
||||
}
|
72
internal/s3/s3.go
Normal file
72
internal/s3/s3.go
Normal file
@ -0,0 +1,72 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
smithyendpoints "github.com/aws/smithy-go/endpoints"
|
||||
"github.com/loveuer/nf-disk/internal/tool"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type resolverV2 struct{}
|
||||
|
||||
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{
|
||||
URI: *u,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
client *s3.Client
|
||||
}
|
||||
|
||||
func New(ctx context.Context, endpoint string, access string, key string) (*Client, error) {
|
||||
var (
|
||||
err error
|
||||
sdkConfig aws.Config
|
||||
output *s3.ListBucketsOutput
|
||||
)
|
||||
|
||||
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.Credentials = aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(access, key, ""))
|
||||
o.UsePathStyle = true
|
||||
o.Region = "auto"
|
||||
})
|
||||
|
||||
if output, err = s3Client.ListBuckets(tool.Timeout(5), &s3.ListBucketsInput{
|
||||
MaxBuckets: aws.Int32(2),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range output.Buckets {
|
||||
log.Debug("s3.New: list bucket name = %s", *item.Name)
|
||||
}
|
||||
|
||||
return &Client{client: s3Client}, nil
|
||||
}
|
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), "topic-audit", "")
|
||||
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: %d] file = %s, size = %d", item.Type, item.Name, item.Size)
|
||||
}
|
||||
}
|
38
internal/tool/ctx.go
Normal file
38
internal/tool/ctx.go
Normal file
@ -0,0 +1,38 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Timeout(seconds ...int) (ctx context.Context) {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
ctx, _ = context.WithTimeout(context.Background(), duration)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
nctx, _ := context.WithTimeout(ctx, duration)
|
||||
|
||||
return nctx
|
||||
}
|
11
internal/tool/must.go
Normal file
11
internal/tool/must.go
Normal file
@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "github.com/loveuer/nf/nft/log"
|
||||
|
||||
func Must(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.Panic(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user