Rework project structure

This commit is contained in:
Lukas Schulte Pelkum 2021-04-15 20:15:42 +02:00
parent de2add44ec
commit 1792ef1c38
No known key found for this signature in database
GPG Key ID: 408DA7CA81DB885C
20 changed files with 238 additions and 328 deletions

View File

@ -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)
}
}()
}

View File

@ -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)
}

3
go.mod
View File

@ -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

9
go.sum
View File

@ -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=

102
internal/config/config.go Normal file
View File

@ -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"),
},
}
}

22
internal/env/env.go vendored
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

30
internal/shared/paste.go Normal file
View File

@ -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
}

View File

@ -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")
)

View File

@ -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)
}

View File

@ -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++
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -6,5 +6,4 @@ type nilLogger struct {
// Printf prints nothing
func (logger *nilLogger) Printf(string, ...interface{}) {
return
}

View File

@ -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