From 1792ef1c384dbc821b6e7fcba2022f9c2544a0d5 Mon Sep 17 00:00:00 2001 From: Lukas Schulte Pelkum Date: Thu, 15 Apr 2021 20:15:42 +0200 Subject: [PATCH] Rework project structure --- cmd/pasty/main.go | 12 +- cmd/transfer/main.go | 6 +- go.mod | 3 - go.sum | 9 -- internal/config/config.go | 102 +++++++++++++ internal/env/env.go | 22 ++- internal/pastes/deletion_token.go | 21 --- internal/pastes/paste.go | 56 ------- internal/shared/paste.go | 30 ++++ internal/shared/storage_type.go | 11 ++ internal/storage/driver.go | 32 ++-- internal/storage/file_driver.go | 18 +-- internal/storage/id_generation.go | 14 +- internal/storage/mongodb_driver.go | 20 +-- internal/storage/s3_driver.go | 22 +-- internal/storage/sql_driver.go | 144 ------------------ .../web/controllers/v1/hastebin_support.go | 18 ++- internal/web/controllers/v1/pastes.go | 16 +- internal/web/logger.go | 1 - internal/web/web.go | 9 +- 20 files changed, 238 insertions(+), 328 deletions(-) create mode 100644 internal/config/config.go delete mode 100644 internal/pastes/deletion_token.go delete mode 100644 internal/pastes/paste.go create mode 100644 internal/shared/paste.go create mode 100644 internal/shared/storage_type.go delete mode 100644 internal/storage/sql_driver.go diff --git a/cmd/pasty/main.go b/cmd/pasty/main.go index ca59a2a..2379afd 100644 --- a/cmd/pasty/main.go +++ b/cmd/pasty/main.go @@ -4,15 +4,15 @@ import ( "log" "time" - "github.com/lus/pasty/internal/env" + "github.com/lus/pasty/internal/config" "github.com/lus/pasty/internal/storage" "github.com/lus/pasty/internal/web" ) func main() { - // Load the optional .env file - log.Println("Loading the optional .env file...") - env.Load() + // Load the configuration + log.Println("Loading the application configuration...") + config.Load() // Load the configured storage driver log.Println("Loading the configured storage driver...") @@ -29,7 +29,7 @@ func main() { }() // Schedule the AutoDelete task - if env.Bool("AUTODELETE", false) { + if config.Current.AutoDelete.Enabled { log.Println("Scheduling the AutoDelete task...") go func() { for { @@ -41,7 +41,7 @@ func main() { log.Printf("AutoDelete: Deleted %d expired pastes", deleted) // Wait until the process should repeat - time.Sleep(env.Duration("AUTODELETE_TASK_INTERVAL", 5*time.Minute)) + time.Sleep(config.Current.AutoDelete.TaskInterval) } }() } diff --git a/cmd/transfer/main.go b/cmd/transfer/main.go index ce7b39a..ea286ce 100644 --- a/cmd/transfer/main.go +++ b/cmd/transfer/main.go @@ -5,6 +5,7 @@ import ( "os" "github.com/lus/pasty/internal/env" + "github.com/lus/pasty/internal/shared" "github.com/lus/pasty/internal/storage" ) @@ -12,7 +13,6 @@ func main() { // Validate the command line arguments if len(os.Args) != 3 { panic("Invalid command line arguments") - return } // Load the optional .env file @@ -20,7 +20,7 @@ func main() { env.Load() // Create and initialize the first (from) driver - from, err := storage.GetDriver(os.Args[1]) + from, err := storage.GetDriver(shared.StorageType(os.Args[1])) if err != nil { panic(err) } @@ -30,7 +30,7 @@ func main() { } // Create and initialize the second (to) driver - to, err := storage.GetDriver(os.Args[2]) + to, err := storage.GetDriver(shared.StorageType(os.Args[2])) if err != nil { panic(err) } diff --git a/go.mod b/go.mod index ce9646b..9cc3996 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,8 @@ go 1.16 require ( github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b github.com/fasthttp/router v1.2.4 - github.com/go-sql-driver/mysql v1.5.0 github.com/joho/godotenv v1.3.0 github.com/klauspost/compress v1.10.11 // indirect - github.com/lib/pq v1.8.0 - github.com/mattn/go-sqlite3 v1.14.2 github.com/minio/minio-go/v7 v7.0.5 github.com/ulule/limiter/v3 v3.5.0 github.com/valyala/fasthttp v1.16.0 diff --git a/go.sum b/go.sum index 72ebfa0..3244d47 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b h1:rcCpjI1OMGtBY8nnBvExeM1pXNoaM35zqmXBGpgJR2o= github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b/go.mod h1:GFtu6vaWaRJV5EvSFaVqgq/3Iq95xyYElBV/aupGzUo= github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/aws/aws-sdk-go v1.29.15 h1:0ms/213murpsujhsnxnNKNeVouW60aJqSd992Ks3mxs= github.com/aws/aws-sdk-go v1.29.15/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -20,7 +18,6 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -89,13 +86,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-sqlite3 v1.14.2 h1:A2EQLwjYf/hfYaM20FVjs1UewCTTFR7RmjEHkLjldIA= -github.com/mattn/go-sqlite3 v1.14.2/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= github.com/minio/minio-go/v7 v7.0.5 h1:I2NIJ2ojwJqD/YByemC1M59e1b4FW9kS7NlOar7HPV4= @@ -171,14 +164,12 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..686e05c --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,102 @@ +package config + +import ( + "strings" + "time" + + "github.com/joho/godotenv" + "github.com/lus/pasty/internal/env" + "github.com/lus/pasty/internal/shared" +) + +// Config represents the general application configuration structure +type Config struct { + WebAddress string + StorageType shared.StorageType + HastebinSupport bool + IDLength int + DeletionTokenLength int + RateLimit string + AutoDelete *AutoDeleteConfig + File *FileConfig + Postgres *PostgresConfig + MongoDB *MongoDBConfig + S3 *S3Config +} + +// AutoDeleteConfig represents the configuration specific for the AutoDelete behaviour +type AutoDeleteConfig struct { + Enabled bool + Lifetime time.Duration + TaskInterval time.Duration +} + +// FileConfig represents the configuration specific for the file storage driver +type FileConfig struct { + Path string +} + +// PostgresConfig represents the configuration specific for the Postgres storage driver +type PostgresConfig struct { + DSN string +} + +// MongoDBConfig represents the configuration specific for the MongoDB storage driver +type MongoDBConfig struct { + DSN string + Database string + Collection string +} + +// S3Config represents the configuration specific for the S3 storage driver +type S3Config struct { + Endpoint string + AccessKeyID string + SecretAccessKey string + SecretToken string + Secure bool + Region string + Bucket string +} + +// Current holds the currently loaded config +var Current *Config + +// Load loads the current config from environment variables and an optional .env file +func Load() { + godotenv.Load() + + Current = &Config{ + WebAddress: env.MustString("PASTY_WEB_ADDRESS", ":8080"), + StorageType: shared.StorageType(strings.ToLower(env.MustString("PASTY_STORAGE_TYPE", "file"))), + HastebinSupport: env.MustBool("PASTY_HASTEBIN_SUPPORT", false), + IDLength: env.MustInt("PASTY_ID_LENGTH", 6), + DeletionTokenLength: env.MustInt("PASTY_DELETION_TOKEN_LENGTH", 12), + RateLimit: env.MustString("PASTY_RATE_LIMIT", "30-M"), + AutoDelete: &AutoDeleteConfig{ + Enabled: env.MustBool("PASTY_AUTODELETE", false), + Lifetime: env.MustDuration("PASTY_AUTODELETE_LIFETIME", 720*time.Hour), + TaskInterval: env.MustDuration("PASTY_AUTODELETE_TASK_INTERVAL", 5*time.Minute), + }, + File: &FileConfig{ + Path: env.MustString("PASTY_STORAGE_FILE_PATH", "./data"), + }, + Postgres: &PostgresConfig{ + DSN: env.MustString("PASTY_STORAGE_POSTGRES_DSN", "postgres://pasty:pasty@localhost/pasty"), + }, + MongoDB: &MongoDBConfig{ + DSN: env.MustString("PASTY_STORAGE_MONGODB_CONNECTION_STRING", "mongodb://pasty:pasty@localhost/pasty"), + Database: env.MustString("PASTY_STORAGE_MONGODB_DATABASE", "pasty"), + Collection: env.MustString("PASTY_STORAGE_MONGODB_COLLECTION", "pastes"), + }, + S3: &S3Config{ + Endpoint: env.MustString("PASTY_STORAGE_S3_ENDPOINT", ""), + AccessKeyID: env.MustString("PASTY_STORAGE_S3_ACCESS_KEY_ID", ""), + SecretAccessKey: env.MustString("PASTY_STORAGE_S3_SECRET_ACCESS_KEY", ""), + SecretToken: env.MustString("PASTY_STORAGE_S3_SECRET_TOKEN", ""), + Secure: env.MustBool("PASTY_STORAGE_S3_SECURE", true), + Region: env.MustString("PASTY_STORAGE_S3_REGION", ""), + Bucket: env.MustString("PASTY_STORAGE_S3_BUCKET", "pasty"), + }, + } +} diff --git a/internal/env/env.go b/internal/env/env.go index c03f8bd..4fc7e59 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -14,8 +14,8 @@ func Load() { godotenv.Load() } -// Get returns the content of the environment variable with the given key or the given fallback -func Get(key, fallback string) string { +// MustString returns the content of the environment variable with the given key or the given fallback +func MustString(key, fallback string) string { found := os.Getenv(static.EnvironmentVariablePrefix + key) if found == "" { return fallback @@ -23,14 +23,20 @@ func Get(key, fallback string) string { return found } -// Bool uses Get and parses it into a boolean -func Bool(key string, fallback bool) bool { - parsed, _ := strconv.ParseBool(Get(key, strconv.FormatBool(fallback))) +// MustBool uses MustString and parses it into a boolean +func MustBool(key string, fallback bool) bool { + parsed, _ := strconv.ParseBool(MustString(key, strconv.FormatBool(fallback))) return parsed } -// Duration uses Get and parses it into a duration -func Duration(key string, fallback time.Duration) time.Duration { - parsed, _ := time.ParseDuration(Get(key, fallback.String())) +// MustInt uses MustString and parses it into an integer +func MustInt(key string, fallback int) int { + parsed, _ := strconv.Atoi(MustString(key, strconv.Itoa(fallback))) + return parsed +} + +// MustDuration uses MustString and parses it into a duration +func MustDuration(key string, fallback time.Duration) time.Duration { + parsed, _ := time.ParseDuration(MustString(key, fallback.String())) return parsed } diff --git a/internal/pastes/deletion_token.go b/internal/pastes/deletion_token.go deleted file mode 100644 index d34e3c3..0000000 --- a/internal/pastes/deletion_token.go +++ /dev/null @@ -1,21 +0,0 @@ -package pastes - -import ( - "strconv" - - "github.com/lus/pasty/internal/env" - "github.com/lus/pasty/internal/utils" -) - -// generateDeletionToken generates a new deletion token -func generateDeletionToken() (string, error) { - // Read the deletion token length - rawLength := env.Get("DELETION_TOKEN_LENGTH", "12") - length, err := strconv.Atoi(rawLength) - if err != nil { - return "", err - } - - // Generate the deletion token - return utils.RandomString(length), nil -} diff --git a/internal/pastes/paste.go b/internal/pastes/paste.go deleted file mode 100644 index f5e3129..0000000 --- a/internal/pastes/paste.go +++ /dev/null @@ -1,56 +0,0 @@ -package pastes - -import ( - "time" - - "github.com/alexedwards/argon2id" - "github.com/lus/pasty/internal/env" -) - -// Paste represents a saved paste -type Paste struct { - ID string `json:"id" bson:"_id"` - Content string `json:"content" bson:"content"` - SuggestedSyntaxType string `json:"suggestedSyntaxType" bson:"suggestedSyntaxType"` - DeletionToken string `json:"deletionToken" bson:"deletionToken,omitempty"` - Created int64 `json:"created" bson:"created"` - AutoDelete bool `json:"autoDelete" bson:"autoDelete"` -} - -// Create creates a new paste object using the given content -func Create(id, content string) (*Paste, error) { - // TODO: Generate the suggested syntax type - suggestedSyntaxType := "" - - // Generate the deletion token - deletionToken, err := generateDeletionToken() - if err != nil { - return nil, err - } - - // Return the paste object - return &Paste{ - ID: id, - Content: content, - SuggestedSyntaxType: suggestedSyntaxType, - DeletionToken: deletionToken, - Created: time.Now().Unix(), - AutoDelete: env.Bool("AUTODELETE", false), - }, nil -} - -// HashDeletionToken hashes the current deletion token of a paste -func (paste *Paste) HashDeletionToken() error { - hash, err := argon2id.CreateHash(paste.DeletionToken, argon2id.DefaultParams) - if err != nil { - return err - } - paste.DeletionToken = hash - return nil -} - -// CheckDeletionToken checks whether or not the given deletion token is correct -func (paste *Paste) CheckDeletionToken(deletionToken string) bool { - match, err := argon2id.ComparePasswordAndHash(deletionToken, paste.DeletionToken) - return err == nil && match -} diff --git a/internal/shared/paste.go b/internal/shared/paste.go new file mode 100644 index 0000000..374d68e --- /dev/null +++ b/internal/shared/paste.go @@ -0,0 +1,30 @@ +package shared + +import ( + "github.com/alexedwards/argon2id" +) + +// Paste represents a saved paste +type Paste struct { + ID string `json:"id" bson:"_id"` + Content string `json:"content" bson:"content"` + DeletionToken string `json:"deletionToken,omitempty" bson:"deletionToken"` + Created int64 `json:"created" bson:"created"` + AutoDelete bool `json:"autoDelete" bson:"autoDelete"` +} + +// HashDeletionToken hashes the current deletion token of a paste +func (paste *Paste) HashDeletionToken() error { + hash, err := argon2id.CreateHash(paste.DeletionToken, argon2id.DefaultParams) + if err != nil { + return err + } + paste.DeletionToken = hash + return nil +} + +// CheckDeletionToken checks whether or not the given deletion token is correct +func (paste *Paste) CheckDeletionToken(deletionToken string) bool { + match, err := argon2id.ComparePasswordAndHash(deletionToken, paste.DeletionToken) + return err == nil && match +} diff --git a/internal/shared/storage_type.go b/internal/shared/storage_type.go new file mode 100644 index 0000000..e2124fb --- /dev/null +++ b/internal/shared/storage_type.go @@ -0,0 +1,11 @@ +package shared + +// StorageType represents a type of storage a paste can be stored with +type StorageType string + +const ( + StorageTypeFile = StorageType("file") + StorageTypePostgres = StorageType("postgres") + StorageTypeMongoDB = StorageType("mongodb") + StorageTypeS3 = StorageType("s3") +) diff --git a/internal/storage/driver.go b/internal/storage/driver.go index 26117fc..98c9831 100644 --- a/internal/storage/driver.go +++ b/internal/storage/driver.go @@ -1,11 +1,11 @@ package storage import ( + "errors" "fmt" - "strings" - "github.com/lus/pasty/internal/env" - "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/config" + "github.com/lus/pasty/internal/shared" ) // Current holds the current storage driver @@ -16,8 +16,8 @@ type Driver interface { Initialize() error Terminate() error ListIDs() ([]string, error) - Get(id string) (*pastes.Paste, error) - Save(paste *pastes.Paste) error + Get(id string) (*shared.Paste, error) + Save(paste *shared.Paste) error Delete(id string) error Cleanup() (int, error) } @@ -25,8 +25,7 @@ type Driver interface { // Load loads the current storage driver func Load() error { // Define the driver to use - storageType := env.Get("STORAGE_TYPE", "file") - driver, err := GetDriver(storageType) + driver, err := GetDriver(config.Current.StorageType) if err != nil { return err } @@ -40,17 +39,18 @@ func Load() error { return nil } -// GetDriver returns the driver with the given type string if it exists -func GetDriver(storageType string) (Driver, error) { - switch strings.ToLower(storageType) { - case "file": +// GetDriver returns the driver with the given type if it exists +func GetDriver(storageType shared.StorageType) (Driver, error) { + switch storageType { + case shared.StorageTypeFile: return new(FileDriver), nil - case "s3": - return new(S3Driver), nil - case "mongodb": + case shared.StorageTypePostgres: + // TODO: Implement Postgres driver + return nil, errors.New("TODO") + case shared.StorageTypeMongoDB: return new(MongoDBDriver), nil - case "sql": - return new(SQLDriver), nil + case shared.StorageTypeS3: + return new(S3Driver), nil default: return nil, fmt.Errorf("invalid storage type '%s'", storageType) } diff --git a/internal/storage/file_driver.go b/internal/storage/file_driver.go index bd00742..055f1fd 100644 --- a/internal/storage/file_driver.go +++ b/internal/storage/file_driver.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/lus/pasty/internal/env" - "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/config" + "github.com/lus/pasty/internal/shared" ) // FileDriver represents the file storage driver @@ -20,7 +20,7 @@ type FileDriver struct { // Initialize initializes the file storage driver func (driver *FileDriver) Initialize() error { - driver.filePath = env.Get("STORAGE_FILE_PATH", "./data") + driver.filePath = config.Current.File.Path return os.MkdirAll(driver.filePath, os.ModePerm) } @@ -60,7 +60,7 @@ func (driver *FileDriver) ListIDs() ([]string, error) { } // Get loads a paste -func (driver *FileDriver) Get(id string) (*pastes.Paste, error) { +func (driver *FileDriver) Get(id string) (*shared.Paste, error) { // Read the file id = base64.StdEncoding.EncodeToString([]byte(id)) data, err := ioutil.ReadFile(filepath.Join(driver.filePath, id+".json")) @@ -72,7 +72,7 @@ func (driver *FileDriver) Get(id string) (*pastes.Paste, error) { } // Unmarshal the file into a paste - paste := new(pastes.Paste) + paste := new(shared.Paste) err = json.Unmarshal(data, &paste) if err != nil { return nil, err @@ -81,7 +81,7 @@ func (driver *FileDriver) Get(id string) (*pastes.Paste, error) { } // Save saves a paste -func (driver *FileDriver) Save(paste *pastes.Paste) error { +func (driver *FileDriver) Save(paste *shared.Paste) error { // Marshal the paste jsonBytes, err := json.Marshal(paste) if err != nil { @@ -123,15 +123,15 @@ func (driver *FileDriver) Cleanup() (int, error) { // Retrieve the paste object paste, err := driver.Get(id) if err != nil { - return 0, err + return deleted, err } // Delete the paste if it is expired - lifetime := env.Duration("AUTODELETE_LIFETIME", 30*24*time.Hour) + lifetime := config.Current.AutoDelete.Lifetime if paste.AutoDelete && paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() { err = driver.Delete(id) if err != nil { - return 0, err + return deleted, err } deleted++ } diff --git a/internal/storage/id_generation.go b/internal/storage/id_generation.go index 72f61d7..a3b6c58 100644 --- a/internal/storage/id_generation.go +++ b/internal/storage/id_generation.go @@ -1,24 +1,14 @@ package storage import ( - "strconv" - - "github.com/lus/pasty/internal/env" + "github.com/lus/pasty/internal/config" "github.com/lus/pasty/internal/utils" ) // AcquireID generates a new unique ID func AcquireID() (string, error) { - // Read the ID length - rawLength := env.Get("ID_LENGTH", "6") - length, err := strconv.Atoi(rawLength) - if err != nil { - return "", err - } - - // Generate the unique ID for { - id := utils.RandomString(length) + id := utils.RandomString(config.Current.IDLength) paste, err := Current.Get(id) if err != nil { return "", err diff --git a/internal/storage/mongodb_driver.go b/internal/storage/mongodb_driver.go index 66c90d1..66937ab 100644 --- a/internal/storage/mongodb_driver.go +++ b/internal/storage/mongodb_driver.go @@ -4,8 +4,8 @@ import ( "context" "time" - "github.com/lus/pasty/internal/env" - "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/config" + "github.com/lus/pasty/internal/shared" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -26,7 +26,7 @@ func (driver *MongoDBDriver) Initialize() error { defer cancel() // Connect to the MongoDB host - client, err := mongo.Connect(ctx, options.Client().ApplyURI(env.Get("STORAGE_MONGODB_CONNECTION_STRING", "mongodb://pasty:pasty@example.host/pasty"))) + client, err := mongo.Connect(ctx, options.Client().ApplyURI(config.Current.MongoDB.DSN)) if err != nil { return err } @@ -39,8 +39,8 @@ func (driver *MongoDBDriver) Initialize() error { // Set the driver attributes driver.client = client - driver.database = env.Get("STORAGE_MONGODB_DATABASE", "pasty") - driver.collection = env.Get("STORAGE_MONGODB_COLLECTION", "pastes") + driver.database = config.Current.MongoDB.Database + driver.collection = config.Current.MongoDB.Collection return nil } @@ -65,7 +65,7 @@ func (driver *MongoDBDriver) ListIDs() ([]string, error) { } // Decode all paste documents - var pasteSlice []pastes.Paste + var pasteSlice []shared.Paste err = result.All(ctx, &pasteSlice) if err != nil { return nil, err @@ -80,7 +80,7 @@ func (driver *MongoDBDriver) ListIDs() ([]string, error) { } // Get loads a paste -func (driver *MongoDBDriver) Get(id string) (*pastes.Paste, error) { +func (driver *MongoDBDriver) Get(id string) (*shared.Paste, error) { // Define the collection to use for this database operation collection := driver.client.Database(driver.database).Collection(driver.collection) @@ -100,7 +100,7 @@ func (driver *MongoDBDriver) Get(id string) (*pastes.Paste, error) { } // Return the retrieved paste object - paste := new(pastes.Paste) + paste := new(shared.Paste) err = result.Decode(paste) if err != nil { return nil, err @@ -109,7 +109,7 @@ func (driver *MongoDBDriver) Get(id string) (*pastes.Paste, error) { } // Save saves a paste -func (driver *MongoDBDriver) Save(paste *pastes.Paste) error { +func (driver *MongoDBDriver) Save(paste *shared.Paste) error { // Define the collection to use for this database operation collection := driver.client.Database(driver.database).Collection(driver.collection) @@ -157,7 +157,7 @@ func (driver *MongoDBDriver) Cleanup() (int, error) { } // Delete the paste if it is expired - lifetime := env.Duration("AUTODELETE_LIFETIME", 30*24*time.Hour) + lifetime := config.Current.AutoDelete.Lifetime if paste.AutoDelete && paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() { err = driver.Delete(id) if err != nil { diff --git a/internal/storage/s3_driver.go b/internal/storage/s3_driver.go index 3c95ad1..e79b40c 100644 --- a/internal/storage/s3_driver.go +++ b/internal/storage/s3_driver.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/lus/pasty/internal/env" - "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/config" + "github.com/lus/pasty/internal/shared" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) @@ -22,16 +22,16 @@ type S3Driver struct { // Initialize initializes the AWS S3 storage driver func (driver *S3Driver) Initialize() error { - client, err := minio.New(env.Get("STORAGE_S3_ENDPOINT", ""), &minio.Options{ - Creds: credentials.NewStaticV4(env.Get("STORAGE_S3_ACCESS_KEY_ID", ""), env.Get("STORAGE_S3_SECRET_ACCESS_KEY", ""), env.Get("STORAGE_S3_SECRET_TOKEN", "")), - Secure: env.Bool("STORAGE_S3_SECURE", true), - Region: env.Get("STORAGE_S3_REGION", ""), + client, err := minio.New(config.Current.S3.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(config.Current.S3.AccessKeyID, config.Current.S3.SecretAccessKey, config.Current.S3.SecretToken), + Secure: config.Current.S3.Secure, + Region: config.Current.S3.Region, }) if err != nil { return err } driver.client = client - driver.bucket = env.Get("STORAGE_S3_BUCKET", "pasty") + driver.bucket = config.Current.S3.Bucket return nil } @@ -59,7 +59,7 @@ func (driver *S3Driver) ListIDs() ([]string, error) { } // Get loads a paste -func (driver *S3Driver) Get(id string) (*pastes.Paste, error) { +func (driver *S3Driver) Get(id string) (*shared.Paste, error) { // Read the object object, err := driver.client.GetObject(context.Background(), driver.bucket, id+".json", minio.GetObjectOptions{}) if err != nil { @@ -74,7 +74,7 @@ func (driver *S3Driver) Get(id string) (*pastes.Paste, error) { } // Unmarshal the object into a paste - paste := new(pastes.Paste) + paste := new(shared.Paste) err = json.Unmarshal(data, &paste) if err != nil { return nil, err @@ -83,7 +83,7 @@ func (driver *S3Driver) Get(id string) (*pastes.Paste, error) { } // Save saves a paste -func (driver *S3Driver) Save(paste *pastes.Paste) error { +func (driver *S3Driver) Save(paste *shared.Paste) error { // Marshal the paste jsonBytes, err := json.Marshal(paste) if err != nil { @@ -123,7 +123,7 @@ func (driver *S3Driver) Cleanup() (int, error) { } // Delete the paste if it is expired - lifetime := env.Duration("AUTODELETE_LIFETIME", 30*24*time.Hour) + lifetime := config.Current.AutoDelete.Lifetime if paste.AutoDelete && paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() { err = driver.Delete(id) if err != nil { diff --git a/internal/storage/sql_driver.go b/internal/storage/sql_driver.go deleted file mode 100644 index 87e816d..0000000 --- a/internal/storage/sql_driver.go +++ /dev/null @@ -1,144 +0,0 @@ -package storage - -import ( - "database/sql" - "time" - - _ "github.com/go-sql-driver/mysql" - _ "github.com/lib/pq" - "github.com/lus/pasty/internal/env" - "github.com/lus/pasty/internal/pastes" - _ "github.com/mattn/go-sqlite3" -) - -// SQLDriver represents the SQL storage driver -type SQLDriver struct { - database *sql.DB - table string -} - -// Initialize initializes the SQL storage driver -func (driver *SQLDriver) Initialize() error { - // Parse the DSN and create a database object - db, err := sql.Open(env.Get("STORAGE_SQL_DRIVER", "sqlite3"), env.Get("STORAGE_SQL_DSN", "./db")) - if err != nil { - return err - } - - // Ping the database - err = db.Ping() - if err != nil { - return err - } - - // Migrate the database - table := env.Get("STORAGE_SQL_TABLE", "pasty") - _, err = db.Exec(` - CREATE TABLE IF NOT EXISTS ? ( - id varchar NOT NULL PRIMARY KEY, - content varchar NOT NULL, - suggestedSyntaxType varchar NOT NULL, - deletionToken varchar NOT NULL, - created bigint NOT NULL, - autoDelete bool NOT NULL - ); - `, table) - if err != nil { - return err - } - - // Set the database object and table name of the SQL driver - driver.database = db - driver.table = table - return nil -} - -// Terminate terminates the SQL storage driver -func (driver *SQLDriver) Terminate() error { - return driver.database.Close() -} - -// ListIDs returns a list of all existing paste IDs -func (driver *SQLDriver) ListIDs() ([]string, error) { - // Execute a SELECT query to retrieve all the paste IDs - rows, err := driver.database.Query("SELECT id FROM ?", driver.table) - if err != nil { - return nil, err - } - defer rows.Close() - - // Scan the rows into a slice of IDs and return it - var ids []string - err = rows.Scan(&ids) - if err != nil { - return nil, err - } - return ids, nil -} - -// Get loads a paste -func (driver *SQLDriver) Get(id string) (*pastes.Paste, error) { - // Execute a SELECT query to retrieve the paste - row := driver.database.QueryRow("SELECT * FROM ? WHERE id = ?", driver.table, id) - err := row.Err() - if err != nil { - if err == sql.ErrNoRows { - return nil, nil - } - return nil, err - } - - // Scan the row into a paste and return it - paste := new(pastes.Paste) - err = row.Scan(&paste) - if err != nil { - return nil, err - } - return paste, nil -} - -// Save saves a paste -func (driver *SQLDriver) Save(paste *pastes.Paste) error { - // Execute an INSERT statement to create the paste - _, err := driver.database.Exec("INSERT INTO ? (?, ?, ?, ?, ?, ?)", driver.table, paste.ID, paste.Content, paste.SuggestedSyntaxType, paste.DeletionToken, paste.Created, paste.AutoDelete) - return err -} - -// Delete deletes a paste -func (driver *SQLDriver) Delete(id string) error { - // Execute a DELETE statement to delete the paste - _, err := driver.database.Exec("DELETE FROM ? WHERE id = ?", driver.table, id) - return err -} - -// Cleanup cleans up the expired pastes -func (driver *SQLDriver) Cleanup() (int, error) { - // Retrieve all paste IDs - ids, err := driver.ListIDs() - if err != nil { - return 0, err - } - - // Define the amount of deleted items - deleted := 0 - - // Loop through all pastes - for _, id := range ids { - // Retrieve the paste object - paste, err := driver.Get(id) - if err != nil { - return 0, err - } - - // Delete the paste if it is expired - lifetime := env.Duration("AUTODELETE_LIFETIME", 30*24*time.Hour) - if paste.AutoDelete && paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() { - err = driver.Delete(id) - if err != nil { - return 0, err - } - deleted++ - } - } - return deleted, nil -} diff --git a/internal/web/controllers/v1/hastebin_support.go b/internal/web/controllers/v1/hastebin_support.go index 1b00acd..c8f1991 100644 --- a/internal/web/controllers/v1/hastebin_support.go +++ b/internal/web/controllers/v1/hastebin_support.go @@ -2,9 +2,12 @@ package v1 import ( "encoding/json" + "time" - "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/config" + "github.com/lus/pasty/internal/shared" "github.com/lus/pasty/internal/storage" + "github.com/lus/pasty/internal/utils" "github.com/valyala/fasthttp" ) @@ -15,10 +18,8 @@ func HastebinSupportHandler(ctx *fasthttp.RequestCtx) { switch string(ctx.Request.Header.ContentType()) { case "text/plain": content = string(ctx.PostBody()) - break case "multipart/form-data": content = string(ctx.FormValue("data")) - break default: ctx.SetStatusCode(fasthttp.StatusBadRequest) ctx.SetBodyString("invalid content type") @@ -34,11 +35,12 @@ func HastebinSupportHandler(ctx *fasthttp.RequestCtx) { } // Create the paste object - paste, err := pastes.Create(id, content) - if err != nil { - ctx.SetStatusCode(fasthttp.StatusInternalServerError) - ctx.SetBodyString(err.Error()) - return + paste := &shared.Paste{ + ID: id, + Content: content, + DeletionToken: utils.RandomString(config.Current.DeletionTokenLength), + Created: time.Now().Unix(), + AutoDelete: config.Current.AutoDelete.Enabled, } // Hash the deletion token diff --git a/internal/web/controllers/v1/pastes.go b/internal/web/controllers/v1/pastes.go index b463040..70756df 100644 --- a/internal/web/controllers/v1/pastes.go +++ b/internal/web/controllers/v1/pastes.go @@ -2,10 +2,13 @@ package v1 import ( "encoding/json" + "time" "github.com/fasthttp/router" - "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/config" + "github.com/lus/pasty/internal/shared" "github.com/lus/pasty/internal/storage" + "github.com/lus/pasty/internal/utils" limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp" "github.com/valyala/fasthttp" ) @@ -73,11 +76,12 @@ func v1PostPaste(ctx *fasthttp.RequestCtx) { } // Create the paste object - paste, err := pastes.Create(id, values["content"]) - if err != nil { - ctx.SetStatusCode(fasthttp.StatusInternalServerError) - ctx.SetBodyString(err.Error()) - return + paste := &shared.Paste{ + ID: id, + Content: values["content"], + DeletionToken: utils.RandomString(config.Current.DeletionTokenLength), + Created: time.Now().Unix(), + AutoDelete: config.Current.AutoDelete.Enabled, } // Hash the deletion token diff --git a/internal/web/logger.go b/internal/web/logger.go index 202d1af..035c994 100644 --- a/internal/web/logger.go +++ b/internal/web/logger.go @@ -6,5 +6,4 @@ type nilLogger struct { // Printf prints nothing func (logger *nilLogger) Printf(string, ...interface{}) { - return } diff --git a/internal/web/web.go b/internal/web/web.go index b8028ea..2349790 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -6,7 +6,7 @@ import ( "strings" routing "github.com/fasthttp/router" - "github.com/lus/pasty/internal/env" + "github.com/lus/pasty/internal/config" "github.com/lus/pasty/internal/static" v1 "github.com/lus/pasty/internal/web/controllers/v1" "github.com/ulule/limiter/v3" @@ -38,7 +38,7 @@ func Serve() error { }) // Set up the rate limiter - rate, err := limiter.NewRateFromFormatted(env.Get("RATE_LIMIT", "30-M")) + rate, err := limiter.NewRateFromFormatted(config.Current.RateLimit) if err != nil { return err } @@ -61,12 +61,11 @@ func Serve() error { } // Route the hastebin documents route if hastebin support is enabled - if env.Bool("HASTEBIN_SUPPORT", false) { + if config.Current.HastebinSupport { router.POST("/documents", rateLimiterMiddleware.Handle(v1.HastebinSupportHandler)) } // Serve the web resources - address := env.Get("WEB_ADDRESS", ":8080") return (&fasthttp.Server{ Handler: func(ctx *fasthttp.RequestCtx) { // Add the CORS headers @@ -77,7 +76,7 @@ func Serve() error { router.Handler(ctx) }, Logger: new(nilLogger), - }).ListenAndServe(address) + }).ListenAndServe(config.Current.WebAddress) } // frontendHandler handles the frontend routing