Compare commits
4 Commits
8c6f59a6b4
...
13f3217a38
| Author | SHA1 | Date | |
|---|---|---|---|
| 13f3217a38 | |||
| aa82cd938c | |||
| aac1ca8891 | |||
| 2268b518b1 |
@@ -13,6 +13,20 @@ services:
|
|||||||
command: server /data --console-address ":9001"
|
command: server /data --console-address ":9001"
|
||||||
volumes:
|
volumes:
|
||||||
- minio_dev:/data
|
- minio_dev:/data
|
||||||
|
db:
|
||||||
|
container_name: db
|
||||||
|
image: mariadb
|
||||||
|
environment:
|
||||||
|
- MARIADB_ROOT_PASSWORD=hunter2
|
||||||
|
- MARIADB_DATABASE=s3Browser
|
||||||
|
- MARIADB_USER=s3Browser
|
||||||
|
- MARIADB_PASSWORD=hunter2
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
volumes:
|
||||||
|
- mariadb_dev:/var/lib/mysql
|
||||||
volumes:
|
volumes:
|
||||||
minio_dev:
|
minio_dev:
|
||||||
name: minio_dev
|
name: minio_dev
|
||||||
|
mariadb_dev:
|
||||||
|
name: mariadb_dev
|
||||||
2
go.mod
2
go.mod
@@ -4,6 +4,7 @@ go 1.16
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-arg v1.4.2
|
github.com/alexflint/go-arg v1.4.2
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/graph-gophers/dataloader v5.0.0+incompatible
|
github.com/graph-gophers/dataloader v5.0.0+incompatible
|
||||||
@@ -13,4 +14,5 @@ require (
|
|||||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
|||||||
96
internal/db/db.go
Normal file
96
internal/db/db.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
_ "embed"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed setup.sql
|
||||||
|
var setupSql string
|
||||||
|
|
||||||
|
const DB_NAME = "s3Browser"
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
dbConn *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB(driver, dataSourceName string) (*DB, error) {
|
||||||
|
db, err := sql.Open(driver, dataSourceName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if driver == "mysql" {
|
||||||
|
db.SetConnMaxLifetime(time.Minute * 3)
|
||||||
|
db.SetMaxOpenConns(10)
|
||||||
|
db.SetMaxIdleConns(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DB{
|
||||||
|
dbConn: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) Setup() error {
|
||||||
|
tx, err := d.dbConn.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(setupSql)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CheckLogin(ctx context.Context, username, password string) (bool, error) {
|
||||||
|
rows, err := d.dbConn.QueryContext(ctx, "SELECT password FROM user WHERE username = ?", username)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordHash []byte
|
||||||
|
err = rows.Scan(&passwordHash)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bcrypt.CompareHashAndPassword(passwordHash, []byte(password)) != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) AddUser(ctx context.Context, username, password string) error {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.dbConn.ExecContext(ctx, "INSERT INTO user (username,password) VALUES (?,?)", username, hash)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
11
internal/db/setup.sql
Normal file
11
internal/db/setup.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE s3Browser.`user` (
|
||||||
|
id INT auto_increment NOT NULL,
|
||||||
|
username varchar(100) NOT NULL,
|
||||||
|
password varchar(60) NOT NULL,
|
||||||
|
CONSTRAINT user_PK PRIMARY KEY (id),
|
||||||
|
CONSTRAINT user_UN UNIQUE KEY (username)
|
||||||
|
)
|
||||||
|
|
||||||
|
ENGINE=InnoDB
|
||||||
|
DEFAULT CHARSET=utf8mb4
|
||||||
|
COLLATE=utf8mb4_general_ci;
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
|
|
||||||
|
"git.kapelle.org/niklas/s3browser/internal/db"
|
||||||
helper "git.kapelle.org/niklas/s3browser/internal/helper"
|
helper "git.kapelle.org/niklas/s3browser/internal/helper"
|
||||||
"git.kapelle.org/niklas/s3browser/internal/loader"
|
"git.kapelle.org/niklas/s3browser/internal/loader"
|
||||||
types "git.kapelle.org/niklas/s3browser/internal/types"
|
types "git.kapelle.org/niklas/s3browser/internal/types"
|
||||||
@@ -73,7 +74,58 @@ func copyMutation(ctx context.Context, src, dest types.ID) (*types.File, error)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveMutation(ctx context.Context, src, dest types.ID) (*types.File, error) {
|
func moveDirMutation(ctx context.Context, src, dest types.ID) ([]*types.File, error) {
|
||||||
|
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Failed to get s3Client from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dest.IsDirectory() {
|
||||||
|
return nil, fmt.Errorf("Dest must be a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
loader, ok := ctx.Value("loader").(*loader.Loader)
|
||||||
|
|
||||||
|
// "move" all file inside dir
|
||||||
|
files, err := loader.GetFilesRecursive(ctx, src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*types.File
|
||||||
|
parent := src.Parent()
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
newID := types.ID{
|
||||||
|
Bucket: dest.Bucket,
|
||||||
|
Key: strings.Replace(file.ID.Key, parent.Key, dest.Key, 1),
|
||||||
|
}
|
||||||
|
newID.Normalize()
|
||||||
|
|
||||||
|
_, err := s3Client.CopyObject(ctx, minio.CopyDestOptions{
|
||||||
|
Bucket: dest.Bucket,
|
||||||
|
Object: newID.Key,
|
||||||
|
}, minio.CopySrcOptions{
|
||||||
|
Bucket: file.ID.Bucket,
|
||||||
|
Object: file.ID.Key,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// TODO: handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMutation(ctx, file.ID)
|
||||||
|
|
||||||
|
result = append(result, &types.File{
|
||||||
|
ID: newID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveFileMutation(ctx context.Context, src, dest types.ID) (*types.File, error) {
|
||||||
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
|
s3Client, ok := ctx.Value("s3Client").(*minio.Client)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -203,8 +255,11 @@ func deleteDirectory(ctx context.Context, id types.ID) error {
|
|||||||
//login Checks for valid username password combination. Returns singed jwt string
|
//login Checks for valid username password combination. Returns singed jwt string
|
||||||
func login(ctx context.Context, username, password string) (types.LoginResult, error) {
|
func login(ctx context.Context, username, password string) (types.LoginResult, error) {
|
||||||
|
|
||||||
// TODO: replace with propper user management
|
dbStore := ctx.Value("dbStore").(*db.DB)
|
||||||
if username != "admin" && password != "hunter2" {
|
|
||||||
|
succes, err := dbStore.CheckLogin(ctx, username, password)
|
||||||
|
|
||||||
|
if !succes {
|
||||||
return types.LoginResult{
|
return types.LoginResult{
|
||||||
Successful: false,
|
Successful: false,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ func GraphqlSchema() (graphql.Schema, error) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"move": &graphql.Field{
|
"move": &graphql.Field{
|
||||||
Type: graphqlFileType,
|
Type: graphql.NewNonNull(graphqlFileType),
|
||||||
Args: graphql.FieldConfigArgument{
|
Args: graphql.FieldConfigArgument{
|
||||||
"src": &graphql.ArgumentConfig{
|
"src": &graphql.ArgumentConfig{
|
||||||
Type: graphql.NewNonNull(objIDType),
|
Type: graphql.NewNonNull(objIDType),
|
||||||
@@ -191,7 +191,36 @@ func GraphqlSchema() (graphql.Schema, error) {
|
|||||||
|
|
||||||
log.Debug("mutation 'move': ", src, "-->", dest)
|
log.Debug("mutation 'move': ", src, "-->", dest)
|
||||||
|
|
||||||
return moveMutation(p.Context, *src, *dest)
|
return moveFileMutation(p.Context, *src, *dest)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"moveDir": &graphql.Field{
|
||||||
|
Type: graphql.NewNonNull(graphql.NewList(graphqlFileType)),
|
||||||
|
Args: graphql.FieldConfigArgument{
|
||||||
|
"src": &graphql.ArgumentConfig{
|
||||||
|
Type: graphql.NewNonNull(objIDType),
|
||||||
|
},
|
||||||
|
"dest": &graphql.ArgumentConfig{
|
||||||
|
Type: graphql.NewNonNull(objIDType),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||||
|
if !helper.IsAuthenticated(p.Context) {
|
||||||
|
return nil, s3errors.ErrNotAuthenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
src, ok := p.Args["src"].(*types.ID)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Failed to parse args")
|
||||||
|
}
|
||||||
|
dest, ok := p.Args["dest"].(*types.ID)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Failed to parse args")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("mutation 'moveDir': ", src, "-->", dest)
|
||||||
|
|
||||||
|
return moveDirMutation(p.Context, *src, *dest)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"createDir": &graphql.Field{
|
"createDir": &graphql.Field{
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kapelle.org/niklas/s3browser/internal/cache"
|
|
||||||
"git.kapelle.org/niklas/s3browser/internal/helper"
|
"git.kapelle.org/niklas/s3browser/internal/helper"
|
||||||
types "git.kapelle.org/niklas/s3browser/internal/types"
|
types "git.kapelle.org/niklas/s3browser/internal/types"
|
||||||
"github.com/graph-gophers/dataloader"
|
"github.com/graph-gophers/dataloader"
|
||||||
@@ -25,19 +24,19 @@ func NewLoader(config types.AppConfig) *Loader {
|
|||||||
return &Loader{
|
return &Loader{
|
||||||
listObjectsLoader: dataloader.NewBatchedLoader(
|
listObjectsLoader: dataloader.NewBatchedLoader(
|
||||||
listObjectsBatch,
|
listObjectsBatch,
|
||||||
dataloader.WithCache(cache.NewTTLCache(config.CacheTTL, config.CacheCleanup)),
|
dataloader.WithCache(&dataloader.NoCache{}),
|
||||||
),
|
),
|
||||||
listObjectsRecursiveLoader: dataloader.NewBatchedLoader(
|
listObjectsRecursiveLoader: dataloader.NewBatchedLoader(
|
||||||
listObjectsRecursiveBatch,
|
listObjectsRecursiveBatch,
|
||||||
dataloader.WithCache(cache.NewTTLCache(config.CacheTTL, config.CacheCleanup)),
|
dataloader.WithCache(&dataloader.NoCache{}),
|
||||||
),
|
),
|
||||||
statObjectLoader: dataloader.NewBatchedLoader(
|
statObjectLoader: dataloader.NewBatchedLoader(
|
||||||
statObjectBatch,
|
statObjectBatch,
|
||||||
dataloader.WithCache(cache.NewTTLCache(config.CacheTTL, config.CacheCleanup)),
|
dataloader.WithCache(&dataloader.NoCache{}),
|
||||||
),
|
),
|
||||||
listBucketsLoader: dataloader.NewBatchedLoader(
|
listBucketsLoader: dataloader.NewBatchedLoader(
|
||||||
listBucketsBatch,
|
listBucketsBatch,
|
||||||
dataloader.WithCache(cache.NewTTLCache(config.CacheTTL, config.CacheCleanup)),
|
dataloader.WithCache(&dataloader.NoCache{}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.kapelle.org/niklas/s3browser/internal/db"
|
||||||
gql "git.kapelle.org/niklas/s3browser/internal/gql"
|
gql "git.kapelle.org/niklas/s3browser/internal/gql"
|
||||||
httpserver "git.kapelle.org/niklas/s3browser/internal/httpserver"
|
httpserver "git.kapelle.org/niklas/s3browser/internal/httpserver"
|
||||||
"git.kapelle.org/niklas/s3browser/internal/loader"
|
"git.kapelle.org/niklas/s3browser/internal/loader"
|
||||||
@@ -55,6 +56,11 @@ func Start(config types.AppConfig) {
|
|||||||
}
|
}
|
||||||
log.Info("s3 client connected")
|
log.Info("s3 client connected")
|
||||||
|
|
||||||
|
dbStore, err := db.NewDB("mysql", "s3Browser:hunter2@/s3Browser")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to connect DB: ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug("Creating dataloader")
|
log.Debug("Creating dataloader")
|
||||||
loader := loader.NewLoader(config)
|
loader := loader.NewLoader(config)
|
||||||
|
|
||||||
@@ -69,6 +75,7 @@ func Start(config types.AppConfig) {
|
|||||||
|
|
||||||
resolveContext := context.WithValue(context.Background(), "s3Client", s3Client)
|
resolveContext := context.WithValue(context.Background(), "s3Client", s3Client)
|
||||||
resolveContext = context.WithValue(resolveContext, "loader", loader)
|
resolveContext = context.WithValue(resolveContext, "loader", loader)
|
||||||
|
resolveContext = context.WithValue(resolveContext, "dbStore", dbStore)
|
||||||
|
|
||||||
log.Debug("Starting HTTP server")
|
log.Debug("Starting HTTP server")
|
||||||
err = httpserver.InitHttp(resolveContext, schema, config.Address)
|
err = httpserver.InitHttp(resolveContext, schema, config.Address)
|
||||||
|
|||||||
@@ -52,16 +52,27 @@ func (i ID) Raw() interface{} {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent returns the parent dir ID.
|
// Parent returns the parent dir ID. If its a file then return containing directory.
|
||||||
|
// If this is a directory then return the dir one up.
|
||||||
func (i ID) Parent() *ID {
|
func (i ID) Parent() *ID {
|
||||||
if i.Key == "/" {
|
if i.Key == "/" {
|
||||||
// Already at root. We dont have a parent
|
// Already at root. We dont have a parent
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
parent := &ID{
|
var parent *ID
|
||||||
Bucket: i.Bucket,
|
|
||||||
Key: filepath.Dir(i.Key) + "/",
|
if i.IsDirectory() {
|
||||||
|
parts := strings.Split(i.Key, "/")
|
||||||
|
parent = &ID{
|
||||||
|
Bucket: i.Bucket,
|
||||||
|
Key: strings.Join(parts[:len(parts)-2], "/") + "/",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent = &ID{
|
||||||
|
Bucket: i.Bucket,
|
||||||
|
Key: filepath.Dir(i.Key) + "/",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.Normalize()
|
parent.Normalize()
|
||||||
|
|||||||
Reference in New Issue
Block a user