Compare commits

..

2 Commits

Author SHA1 Message Date
8cf61c68bf bucket crud 2024-11-17 22:08:06 +01:00
abd805b9cd database & CD bucket 2024-11-13 21:16:22 +01:00
20 changed files with 645 additions and 86 deletions

View File

@ -9,3 +9,9 @@ simple object storage
- Standalone - Standalone
- Head - Head
- StorageNode - StorageNode
## create openapi
``` go
oapi-codegen -config openapi/server.cfg.yml openapi/openapi.yml
```

View File

@ -466,12 +466,6 @@ type GetBucketsResponseObject interface {
type GetBuckets200JSONResponse struct { type GetBuckets200JSONResponse struct {
Items *[]Bucket `json:"items,omitempty"` Items *[]Bucket `json:"items,omitempty"`
// Limit Maximum number of items returned in the response
Limit *int `json:"limit,omitempty"`
// Offset Number of items skipped before starting the response
Offset *int `json:"offset,omitempty"`
// Total Total number of buckets available // Total Total number of buckets available
Total *int `json:"total,omitempty"` Total *int `json:"total,omitempty"`
} }
@ -483,14 +477,6 @@ func (response GetBuckets200JSONResponse) VisitGetBucketsResponse(w http.Respons
return json.NewEncoder(w).Encode(response) return json.NewEncoder(w).Encode(response)
} }
type GetBuckets500Response struct {
}
func (response GetBuckets500Response) VisitGetBucketsResponse(w http.ResponseWriter) error {
w.WriteHeader(500)
return nil
}
type PostBucketsRequestObject struct { type PostBucketsRequestObject struct {
Body *PostBucketsJSONRequestBody Body *PostBucketsJSONRequestBody
} }
@ -547,14 +533,6 @@ func (response DeleteBucketsBucketName404Response) VisitDeleteBucketsBucketNameR
return nil return nil
} }
type DeleteBucketsBucketName500Response struct {
}
func (response DeleteBucketsBucketName500Response) VisitDeleteBucketsBucketNameResponse(w http.ResponseWriter) error {
w.WriteHeader(500)
return nil
}
type GetBucketsBucketNameObjectsRequestObject struct { type GetBucketsBucketNameObjectsRequestObject struct {
BucketName string `json:"bucketName"` BucketName string `json:"bucketName"`
Params GetBucketsBucketNameObjectsParams Params GetBucketsBucketNameObjectsParams
@ -567,12 +545,6 @@ type GetBucketsBucketNameObjectsResponseObject interface {
type GetBucketsBucketNameObjects200JSONResponse struct { type GetBucketsBucketNameObjects200JSONResponse struct {
Items *[]Object `json:"items,omitempty"` Items *[]Object `json:"items,omitempty"`
// Limit Maximum number of items returned in the response
Limit *int `json:"limit,omitempty"`
// Offset Number of items skipped before starting the response
Offset *int `json:"offset,omitempty"`
// Total Total number of objects available in the bucket // Total Total number of objects available in the bucket
Total *int `json:"total,omitempty"` Total *int `json:"total,omitempty"`
} }
@ -617,14 +589,6 @@ func (response DeleteBucketsBucketNameObjectsObjectKey404Response) VisitDeleteBu
return nil return nil
} }
type DeleteBucketsBucketNameObjectsObjectKey500Response struct {
}
func (response DeleteBucketsBucketNameObjectsObjectKey500Response) VisitDeleteBucketsBucketNameObjectsObjectKeyResponse(w http.ResponseWriter) error {
w.WriteHeader(500)
return nil
}
type GetBucketsBucketNameObjectsObjectKeyRequestObject struct { type GetBucketsBucketNameObjectsObjectKeyRequestObject struct {
BucketName string `json:"bucketName"` BucketName string `json:"bucketName"`
ObjectKey string `json:"objectKey"` ObjectKey string `json:"objectKey"`
@ -687,14 +651,6 @@ func (response PutBucketsBucketNameObjectsObjectKey400Response) VisitPutBucketsB
return nil return nil
} }
type PutBucketsBucketNameObjectsObjectKey500Response struct {
}
func (response PutBucketsBucketNameObjectsObjectKey500Response) VisitPutBucketsBucketNameObjectsObjectKeyResponse(w http.ResponseWriter) error {
w.WriteHeader(500)
return nil
}
// StrictServerInterface represents all server handlers. // StrictServerInterface represents all server handlers.
type StrictServerInterface interface { type StrictServerInterface interface {
// List all buckets // List all buckets
@ -945,26 +901,25 @@ func (sh *strictHandler) PutBucketsBucketNameObjectsObjectKey(w http.ResponseWri
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

42
config/env.go Normal file
View File

@ -0,0 +1,42 @@
package config
import (
"os"
"strconv"
"time"
)
func (c *Config) ReadEnv() (err error) {
// General Configuration
stringVar(&c.BaseUrl, "BASE_URL")
stringVar(&c.SavePath, "SAVE_PATH")
intVar(&c.Port, "PORT")
return
}
func stringVar(x *string, key string) {
if v := os.Getenv(key); v != "" {
*x = v
}
}
func float64Var(x *float64, key string) (err error) {
if v := os.Getenv(key); v != "" {
*x, err = strconv.ParseFloat(v, 64)
}
return
}
func intVar(x *int, key string) (err error) {
if v := os.Getenv(key); v != "" {
*x, err = strconv.Atoi(v)
}
return
}
func durationVar(x *time.Duration, key string) (err error) {
if v := os.Getenv(key); v != "" {
*x, err = time.ParseDuration(v)
}
return
}

134
config/env_test.go Normal file
View File

@ -0,0 +1,134 @@
package config
import (
"os"
"testing"
"time"
)
func TestReadEnv(t *testing.T) {
// Set up environment variables
os.Setenv("CONFIG_PATH", "/path/to/config")
os.Setenv("ENV", "dev")
os.Setenv("PORT", "80")
os.Setenv("BASE_URL", "https://example.com")
os.Setenv("ACCESS_ORIGIN", "lou-taylor.ch")
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")
os.Setenv("DB_USER", "testuser")
os.Setenv("DB_PASSWORD", "password")
os.Setenv("DB_NAME", "testdb")
os.Setenv("ADMIN_NAME", "admin")
os.Setenv("ADMIN_PASSWORD", "adminpass")
os.Setenv("JWT_SECRET", "supersecret")
os.Setenv("JWT_VALID_PERIOD", "24h")
os.Setenv("IMAGE_QUALITY", "85.5")
os.Setenv("IMAGE_MAX_WIDTH", "1920")
os.Setenv("IMAGE_SAVE_PATH", "/path/to/images")
// Initialize an empty Config struct
c := New()
// Call ReadEnv to populate the config from environment variables
err := c.ReadEnv()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.BaseUrl != "https://example.com" {
t.Errorf("expected BaseUrl to be 'https://example.com', got %v", c.BaseUrl)
}
if c.Port != 80 {
t.Errorf("expected Port to be 80, got %d", c.Port)
}
}
func TestHelperFunctions(t *testing.T) {
t.Run("stringVar", func(t *testing.T) {
// Set up environment variable
os.Setenv("TEST_STRING", "test_value")
var result string
stringVar(&result, "TEST_STRING")
if result != "test_value" {
t.Errorf("expected 'test_value', got '%v'", result)
}
// Test empty environment variable case
os.Setenv("TEST_STRING_EMPTY", "")
stringVar(&result, "TEST_STRING_EMPTY")
if result != "test_value" { // result shouldn't change
t.Errorf("expected 'test_value', got '%v'", result)
}
})
t.Run("float64Var", func(t *testing.T) {
// Set up valid float environment variable
os.Setenv("TEST_FLOAT", "42.42")
var result float64
err := float64Var(&result, "TEST_FLOAT")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 42.42 {
t.Errorf("expected 42.42, got '%v'", result)
}
// Test invalid float value
os.Setenv("TEST_FLOAT_INVALID", "invalid_float")
err = float64Var(&result, "TEST_FLOAT_INVALID")
if err == nil {
t.Errorf("expected error for invalid float, but got nil")
}
})
t.Run("intVar", func(t *testing.T) {
// Set up valid integer environment variable
os.Setenv("TEST_INT", "123")
var result int
err := intVar(&result, "TEST_INT")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 123 {
t.Errorf("expected 123, got '%v'", result)
}
// Test invalid int value
os.Setenv("TEST_INT_INVALID", "invalid_int")
err = intVar(&result, "TEST_INT_INVALID")
if err == nil {
t.Errorf("expected error for invalid int, but got nil")
}
})
t.Run("durationVar", func(t *testing.T) {
// Set up valid duration environment variable
os.Setenv("TEST_DURATION", "2h")
var result time.Duration
err := durationVar(&result, "TEST_DURATION")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedDuration := 2 * time.Hour
if result != expectedDuration {
t.Errorf("expected %v, got '%v'", expectedDuration, result)
}
// Test invalid duration value
os.Setenv("TEST_DURATION_INVALID", "invalid_duration")
err = durationVar(&result, "TEST_DURATION_INVALID")
if err == nil {
t.Errorf("expected error for invalid duration, but got nil")
}
})
}

13
config/flag.go Normal file
View File

@ -0,0 +1,13 @@
package config
import (
"flag"
)
func (c *Config) ReadFlags() {
flag.StringVar(&c.ConfigPath, "config", c.ConfigPath, "path of the toml configuration file")
flag.StringVar(&c.BaseUrl, "base-ulr", c.BaseUrl, "base url of the api")
flag.IntVar(&c.Port, "port", c.Port, "port of the api")
flag.Parse()
}

39
config/resource.go Normal file
View File

@ -0,0 +1,39 @@
package config
type Environment string
const (
Dev Environment = "dev"
Staging = "staging"
Production = "prod"
)
// Config holds all configuration values
type Config struct {
ConfigPath string `toml:"-"`
SavePath string `toml:"savePath"`
BaseUrl string `toml:"base_url"`
Port int `toml:"port"`
}
// New returns a pointer to a default configuration with all empty values
func New() *Config {
return &Config{
ConfigPath: "",
Port: 0,
SavePath: "",
BaseUrl: "",
}
}
// Default returns a pointer to a default configuration
func Default() *Config {
return &Config{
ConfigPath: "./config.toml",
BaseUrl: "http://localhost:8080",
Port: 8080,
SavePath: "./storage",
}
}

26
config/toml.go Normal file
View File

@ -0,0 +1,26 @@
package config
import (
"os"
"github.com/pelletier/go-toml/v2"
)
func (c *Config) ReadFile(name string) error {
file, err := os.Open(name)
if err != nil {
return err
}
err = toml.NewDecoder(file).Decode(c)
return err
}
func (c *Config) TOML() string {
b, err := toml.Marshal(c)
if err != nil {
panic(err)
}
return string(b)
}

22
config/toml_test.go Normal file
View File

@ -0,0 +1,22 @@
package config
import (
"testing"
)
func TestReadFile(t *testing.T) {
c := Default()
err := c.ReadFile("../config.example.toml")
if err != nil {
t.Fatalf("failed to read config file: %v", err)
}
// Validate the values are correctly set based on the TOML file
expectedBaseURL := "http://localhost:8080"
if c.BaseUrl != expectedBaseURL {
t.Errorf("expected BaseUrl to be '%v', got '%v'", expectedBaseURL, c.BaseUrl)
}
}

93
controller/controller.go Normal file
View File

@ -0,0 +1,93 @@
package controller
import (
"context"
"errors"
"git.schreifuchs.ch/schreifuchs/warehouse/api"
"git.schreifuchs.ch/schreifuchs/warehouse/model"
"git.schreifuchs.ch/schreifuchs/warehouse/utils"
"gorm.io/gorm"
)
func (c *Controller) GetBuckets(ctx context.Context, request api.GetBucketsRequestObject) (api.GetBucketsResponseObject, error) {
buckets, err := c.db.FindApiBuckets(*request.Params.Limit, *request.Params.Offset)
if err != nil {
return nil, err
}
t := len(buckets)
return api.GetBuckets200JSONResponse{
Items: &buckets,
Total: &t,
}, nil
}
func (c *Controller) PostBuckets(ctx context.Context, request api.PostBucketsRequestObject) (api.PostBucketsResponseObject, error) {
b := &model.Bucket{}
b.Name = *request.Body.Name
c.db.InsertBucket(b)
return api.PostBuckets409Response{}, nil
}
func (c *Controller) DeleteBucketsBucketName(ctx context.Context, request api.DeleteBucketsBucketNameRequestObject) (api.DeleteBucketsBucketNameResponseObject, error) {
if err := c.db.DeleteBucketByName(request.BucketName); errors.Is(err, gorm.ErrRecordNotFound) {
return api.DeleteBucketsBucketName404Response{}, nil
} else if err != nil {
return nil, err
}
return api.DeleteBucketsBucketName204Response{}, nil
}
func (c *Controller) GetBucketsBucketNameObjects(ctx context.Context, request api.GetBucketsBucketNameObjectsRequestObject) (response api.GetBucketsBucketNameObjectsResponseObject, err error) {
if bucket, err := c.db.FindBucketByName(request.BucketName, *request.Params.Limit, *request.Params.Offset); err == nil {
objects := make([]api.Object, 0, len(bucket.Objects))
for _, o := range utils.Paginate(bucket.Objects, *request.Params.Offset, *request.Params.Limit) {
s := int(o.Size)
objects = append(objects, api.Object{
Key: &o.Key,
LastModified: &o.UpdatedAt,
Size: &s,
})
}
total := len(bucket.Objects)
return api.GetBucketsBucketNameObjects200JSONResponse{
Items: &objects,
Total: &total,
}, nil
} else if errors.Is(err, gorm.ErrRecordNotFound) {
return api.GetBucketsBucketNameObjects404Response{}, nil
}
return
}
func (c *Controller) DeleteBucketsBucketNameObjectsObjectKey(ctx context.Context, request api.DeleteBucketsBucketNameObjectsObjectKeyRequestObject) (api.DeleteBucketsBucketNameObjectsObjectKeyResponseObject, error) {
err := c.db.DeleteObjectByKey(request.BucketName, request.ObjectKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
return api.DeleteBucketsBucketNameObjectsObjectKey404Response{}, nil
}
return api.DeleteBucketsBucketNameObjectsObjectKey204Response{}, err
}
func (c *Controller) GetBucketsBucketNameObjectsObjectKey(ctx context.Context, request api.GetBucketsBucketNameObjectsObjectKeyRequestObject) (api.GetBucketsBucketNameObjectsObjectKeyResponseObject, error) {
return api.GetBucketsBucketNameObjectsObjectKey404Response{}, nil
}
func (c *Controller) PutBucketsBucketNameObjectsObjectKey(ctx context.Context, request api.PutBucketsBucketNameObjectsObjectKeyRequestObject) (api.PutBucketsBucketNameObjectsObjectKeyResponseObject, error) {
return api.PutBucketsBucketNameObjectsObjectKey400Response{}, nil
}

27
controller/resource.go Normal file
View File

@ -0,0 +1,27 @@
package controller
import (
"git.schreifuchs.ch/schreifuchs/warehouse/api"
"git.schreifuchs.ch/schreifuchs/warehouse/model"
)
// Implement the interface
type Controller struct {
db database
}
func New(db database) *Controller {
return &Controller{
db: db,
}
}
type database interface {
InsertBucket(bucket *model.Bucket) error
FindApiBuckets(limit, offset int) (buckets []api.Bucket, err error)
FindBucketByName(name string, limit, offset int) (buckets model.Bucket, err error)
FindBucketIdByName(name string) (id uint, err error)
DeleteBucketByName(name string) error
FindObjectByKey(bucketName, key string) (object *model.Object, err error)
DeleteObjectByKey(bucketName, key string) error
}

56
database/controller.go Normal file
View File

@ -0,0 +1,56 @@
package database
import (
"git.schreifuchs.ch/schreifuchs/warehouse/api"
"git.schreifuchs.ch/schreifuchs/warehouse/model"
)
func (db *DB) InsertBucket(bucket *model.Bucket) error {
return db.conn.Create(bucket).Error
}
func (db *DB) FindApiBuckets(limit, offset int) (buckets []api.Bucket, err error) {
err = db.conn.Model(&model.Bucket{}).Limit(limit).Offset(offset).Find(&buckets).Error
return
}
func (db *DB) FindBucketByName(name string) (bucket model.Bucket, err error) {
err = db.conn.First(&bucket).Where("name = ?", name).Error
return
}
func (db *DB) FindBucketIdByName(name string) (id uint, err error) {
b := &model.Bucket{}
err = db.conn.Select("id").First(b).Error
return b.ID, err
}
func (db *DB) DeleteBucket(id uint) error {
return db.conn.Delete(&model.Bucket{}, id).Error
}
func (db *DB) DeleteBucketByName(name string) error {
return db.conn.Delete(&model.Bucket{}).Where("name = ?", name).Error
}
func (db *DB) FindObjectByKey(bucketName, key string) (object *model.Object, err error) {
bid, err := db.FindBucketIdByName(bucketName)
if err != nil {
return
}
err = db.conn.Find(object).Where("key = ? AND bucket_id = ?", key, bid).Error
return
}
func (db *DB) DeleteObjectByKey(bucketName, key string) (err error) {
bid, err := db.FindBucketIdByName(bucketName)
if err != nil {
return
}
err = db.conn.Delete(&model.Object{}).Where("key = ? AND bucket_id = ?", key, bid).Error
return
}

20
database/resource.go Normal file
View File

@ -0,0 +1,20 @@
package database
import (
"git.schreifuchs.ch/schreifuchs/warehouse/model"
"gorm.io/driver/sqlite" // Sqlite driver based on CGO
"gorm.io/gorm"
)
type DB struct {
conn *gorm.DB
}
func Init(file string) (*DB, error) {
if db, err := gorm.Open(sqlite.Open(file), &gorm.Config{}); err != nil {
return nil, err
} else {
db.AutoMigrate(&model.Bucket{}, &model.Object{})
return &DB{conn: db}, nil
}
}

9
go.mod
View File

@ -5,18 +5,27 @@ go 1.23.2
require ( require (
github.com/getkin/kin-openapi v0.128.0 github.com/getkin/kin-openapi v0.128.0
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/joho/godotenv v1.5.1
github.com/oapi-codegen/runtime v1.1.1 github.com/oapi-codegen/runtime v1.1.1
github.com/pelletier/go-toml/v2 v2.2.3
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.12
) )
require ( require (
git.schreifuchs.ch/schreifuchs/logger v0.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/uuid v1.5.0 // indirect github.com/google/uuid v1.5.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect github.com/invopop/yaml v0.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

18
go.sum
View File

@ -1,3 +1,5 @@
git.schreifuchs.ch/schreifuchs/logger v0.1.0 h1:ChBvtZpVNkYxoQ52jbCyhyLG5ZS+s0863OovBaWQgS4=
git.schreifuchs.ch/schreifuchs/logger v0.1.0/go.mod h1:VRX/HF+FeI/xTk9Guoq+vBzH2WK20zASZEUhMSL3Tws=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
@ -19,6 +21,12 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
@ -28,10 +36,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -45,8 +57,14 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

52
main.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"fmt"
"net/http"
"os"
"git.schreifuchs.ch/schreifuchs/warehouse/api"
"git.schreifuchs.ch/schreifuchs/warehouse/config"
"git.schreifuchs.ch/schreifuchs/warehouse/controller"
"git.schreifuchs.ch/schreifuchs/warehouse/database"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
func readCfg() *config.Config {
cfg := config.Default()
if err := godotenv.Overload(); err != nil {
fmt.Println("no .env file loaded")
} else {
fmt.Println(".env file loaded")
}
cfg.ReadEnv()
return cfg
}
func main() {
cfg := readCfg()
fmt.Println(cfg.TOML())
db, err := database.Init("./warehouse.db")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
controller := controller.New(db)
apiHandler := api.Handler(api.NewStrictHandler(controller, nil))
r := mux.NewRouter()
r.PathPrefix("/").Handler(apiHandler)
if err := http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), r); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

16
model/resource.go Normal file
View File

@ -0,0 +1,16 @@
package model
import "gorm.io/gorm"
type Bucket struct {
gorm.Model
Name string `gorm:"uniqueIndex"`
Objects []Object
}
type Object struct {
gorm.Model
Key string `gorm:"uniqueIndex"`
Size uint
BucketId uint
}

View File

@ -1,5 +1,5 @@
openapi: 3.0.3 openapi: 3.0.0
info: info:
title: Object Storage API title: Object Storage API
description: API for managing objects in an object storage service. description: API for managing objects in an object storage service.
@ -21,7 +21,7 @@ paths:
in: query in: query
schema: schema:
type: integer type: integer
default: 10 default: 100
description: The maximum number of items to return description: The maximum number of items to return
- name: offset - name: offset
in: query in: query
@ -40,18 +40,10 @@ paths:
total: total:
type: integer type: integer
description: Total number of buckets available description: Total number of buckets available
limit:
type: integer
description: Maximum number of items returned in the response
offset:
type: integer
description: Number of items skipped before starting the response
items: items:
type: array type: array
items: items:
$ref: '#/components/schemas/Bucket' $ref: '#/components/schemas/Bucket'
'500':
description: Server error
post: post:
summary: Create a new bucket summary: Create a new bucket
@ -91,8 +83,6 @@ paths:
description: Bucket deleted successfully description: Bucket deleted successfully
'404': '404':
description: Bucket not found description: Bucket not found
'500':
description: Server error
/buckets/{bucketName}/objects: /buckets/{bucketName}/objects:
get: get:
@ -128,12 +118,6 @@ paths:
total: total:
type: integer type: integer
description: Total number of objects available in the bucket description: Total number of objects available in the bucket
limit:
type: integer
description: Maximum number of items returned in the response
offset:
type: integer
description: Number of items skipped before starting the response
items: items:
type: array type: array
items: items:
@ -197,8 +181,6 @@ paths:
description: Object uploaded successfully description: Object uploaded successfully
'400': '400':
description: Invalid object data description: Invalid object data
'500':
description: Server error
delete: delete:
summary: Delete an object summary: Delete an object
@ -221,8 +203,6 @@ paths:
description: Object deleted successfully description: Object deleted successfully
'404': '404':
description: Bucket or object not found description: Bucket or object not found
'500':
description: Server error
components: components:
schemas: schemas:

14
utils/pagination.go Normal file
View File

@ -0,0 +1,14 @@
package utils
// Paginate returns a slicle with offset & limit
func Paginate[T any](items []T, offset, limit int) []T {
if len(items) <= offset {
return []T{}
}
if limit > len(items)-offset {
limit = len(items) - offset
}
return items[offset : offset+limit]
}

37
utils/pagination_test.go Normal file
View File

@ -0,0 +1,37 @@
package utils
import (
"fmt"
"reflect"
"testing"
)
func TestPagination(t *testing.T) {
cases := []struct {
Slice []int
Offset int
Limit int
Expected []int
}{
{
Slice: []int{1, 2, 3, 4, 5},
Offset: 1,
Limit: 4,
Expected: []int{2, 3, 4, 5},
},
}
for _, tc := range cases {
t.Run(
fmt.Sprintf("%v %d %d %v", tc.Slice, tc.Offset, tc.Limit, tc.Expected),
func(t *testing.T) {
got := Paginate(tc.Slice, tc.Offset, tc.Limit)
if !reflect.DeepEqual(got, tc.Expected) {
t.Errorf("Expected %v, but got %v", tc.Expected, got)
}
})
}
}

BIN
warehouse.db Normal file

Binary file not shown.