Compare commits
2 Commits
f5013e5f9e
...
8cf61c68bf
Author | SHA1 | Date | |
---|---|---|---|
8cf61c68bf | |||
abd805b9cd |
@ -9,3 +9,9 @@ simple object storage
|
||||
- Standalone
|
||||
- Head
|
||||
- StorageNode
|
||||
|
||||
## create openapi
|
||||
|
||||
``` go
|
||||
oapi-codegen -config openapi/server.cfg.yml openapi/openapi.yml
|
||||
```
|
||||
|
@ -466,12 +466,6 @@ type GetBucketsResponseObject interface {
|
||||
type GetBuckets200JSONResponse struct {
|
||||
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 *int `json:"total,omitempty"`
|
||||
}
|
||||
@ -483,14 +477,6 @@ func (response GetBuckets200JSONResponse) VisitGetBucketsResponse(w http.Respons
|
||||
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 {
|
||||
Body *PostBucketsJSONRequestBody
|
||||
}
|
||||
@ -547,14 +533,6 @@ func (response DeleteBucketsBucketName404Response) VisitDeleteBucketsBucketNameR
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteBucketsBucketName500Response struct {
|
||||
}
|
||||
|
||||
func (response DeleteBucketsBucketName500Response) VisitDeleteBucketsBucketNameResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(500)
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetBucketsBucketNameObjectsRequestObject struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
Params GetBucketsBucketNameObjectsParams
|
||||
@ -567,12 +545,6 @@ type GetBucketsBucketNameObjectsResponseObject interface {
|
||||
type GetBucketsBucketNameObjects200JSONResponse struct {
|
||||
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 *int `json:"total,omitempty"`
|
||||
}
|
||||
@ -617,14 +589,6 @@ func (response DeleteBucketsBucketNameObjectsObjectKey404Response) VisitDeleteBu
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteBucketsBucketNameObjectsObjectKey500Response struct {
|
||||
}
|
||||
|
||||
func (response DeleteBucketsBucketNameObjectsObjectKey500Response) VisitDeleteBucketsBucketNameObjectsObjectKeyResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(500)
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetBucketsBucketNameObjectsObjectKeyRequestObject struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
ObjectKey string `json:"objectKey"`
|
||||
@ -687,14 +651,6 @@ func (response PutBucketsBucketNameObjectsObjectKey400Response) VisitPutBucketsB
|
||||
return nil
|
||||
}
|
||||
|
||||
type PutBucketsBucketNameObjectsObjectKey500Response struct {
|
||||
}
|
||||
|
||||
func (response PutBucketsBucketNameObjectsObjectKey500Response) VisitPutBucketsBucketNameObjectsObjectKeyResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(500)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrictServerInterface represents all server handlers.
|
||||
type StrictServerInterface interface {
|
||||
// List all buckets
|
||||
@ -945,26 +901,25 @@ func (sh *strictHandler) PutBucketsBucketNameObjectsObjectKey(w http.ResponseWri
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
42
config/env.go
Normal file
42
config/env.go
Normal 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
134
config/env_test.go
Normal 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
13
config/flag.go
Normal 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
39
config/resource.go
Normal 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
26
config/toml.go
Normal 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
22
config/toml_test.go
Normal 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
93
controller/controller.go
Normal 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
27
controller/resource.go
Normal 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
56
database/controller.go
Normal 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
20
database/resource.go
Normal 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
9
go.mod
@ -5,18 +5,27 @@ go 1.23.2
|
||||
require (
|
||||
github.com/getkin/kin-openapi v0.128.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/pelletier/go-toml/v2 v2.2.3
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
git.schreifuchs.ch/schreifuchs/logger v0.1.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/swag v0.23.0 // indirect
|
||||
github.com/google/uuid v1.5.0 // 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/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/perimeterx/marshmallow v1.1.5 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
18
go.sum
18
go.sum
@ -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/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=
|
||||
@ -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/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
|
||||
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/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
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/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
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/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/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
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/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
52
main.go
Normal 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
16
model/resource.go
Normal 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
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
|
||||
openapi: 3.0.3
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Object Storage API
|
||||
description: API for managing objects in an object storage service.
|
||||
@ -21,7 +21,7 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
default: 100
|
||||
description: The maximum number of items to return
|
||||
- name: offset
|
||||
in: query
|
||||
@ -40,18 +40,10 @@ paths:
|
||||
total:
|
||||
type: integer
|
||||
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:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Bucket'
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
post:
|
||||
summary: Create a new bucket
|
||||
@ -91,8 +83,6 @@ paths:
|
||||
description: Bucket deleted successfully
|
||||
'404':
|
||||
description: Bucket not found
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/buckets/{bucketName}/objects:
|
||||
get:
|
||||
@ -128,12 +118,6 @@ paths:
|
||||
total:
|
||||
type: integer
|
||||
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:
|
||||
type: array
|
||||
items:
|
||||
@ -197,8 +181,6 @@ paths:
|
||||
description: Object uploaded successfully
|
||||
'400':
|
||||
description: Invalid object data
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
delete:
|
||||
summary: Delete an object
|
||||
@ -221,8 +203,6 @@ paths:
|
||||
description: Object deleted successfully
|
||||
'404':
|
||||
description: Bucket or object not found
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
components:
|
||||
schemas:
|
||||
|
14
utils/pagination.go
Normal file
14
utils/pagination.go
Normal 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
37
utils/pagination_test.go
Normal 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
BIN
warehouse.db
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user