Compare commits

..

3 Commits

Author SHA1 Message Date
aeec85086e
Add note about mailing list in README.
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-19 22:11:42 +05:00
74035622f5
Make linter happy.
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-19 21:55:42 +05:00
5fc6d3a181
Application superstructure (or supersingleton, if you want).
Some checks failed
continuous-integration/drone/push Build is failing
Got rid of context thing which misleads due to existance of stdlib's
context package.

Also fixed golangci-lint configuration.

Fixes #20.
2022-08-19 21:52:49 +05:00
35 changed files with 594 additions and 441 deletions

View File

@ -15,8 +15,14 @@ linters:
# This linter MIGHT BE good, but who decided that I want keepFor in # This linter MIGHT BE good, but who decided that I want keepFor in
# JSON instead of keep_for for KeepFor field? # JSON instead of keep_for for KeepFor field?
- tagliatelle - tagliatelle
# Why it is bad to return an interface? No one knows, except authors of this linter.
- ireturn
# Some structs will contain context, and it's cancelFunc.
- containedctx
# Deprecated. # Deprecated.
- exhaustivestruct - exhaustivestruct
- golint
- wrapcheck
linters-settings: linters-settings:
lll: lll:
line-length: 420 line-length: 420

View File

@ -6,7 +6,7 @@ Easy-to-use-and-install pastebin software written in Go. No bells or whistles, n
**Please, use [my gitea](https://code.pztrn.name/apps/fastpastebin) for bug reporting. All other places are mirrors!** **Please, use [my gitea](https://code.pztrn.name/apps/fastpastebin) for bug reporting. All other places are mirrors!**
Also, [join Matrix room](https://matrix.to/#/%23fastpastebin:pztrn.online?via=matrix.org) for near-realtime chat. Also, [join Matrix room](https://matrix.to/#/%23fastpastebin:pztrn.online?via=matrix.org) for near-realtime chat. Also there is a [mailing list](https://lists.pztrn.name/postorius/lists/fastpastebin.lists.pztrn.name/) if you prefer to ask asynchronously.
## Current functionality ## Current functionality
@ -57,7 +57,7 @@ services:
Take a look at [example configuration file](examples/fastpastebin.yaml.dist) which contains all supported options and their descriptions. Take a look at [example configuration file](examples/fastpastebin.yaml.dist) which contains all supported options and their descriptions.
Configuration file position is irrelevant, there is no hardcoded paths where Fast Paste Bin looking for it's configuration. Use ``-config`` CLI parameter or ``FASTPASTEBIN_CONFIG`` environment variable to specify path. Configuration file position is irrelevant, there is no hardcoded paths where Fast Paste Bin looking for it's configuration. Use ``FASTPASTEBIN_CONFIG`` environment variable to specify path.
## Developing ## Developing

View File

@ -25,7 +25,6 @@
package main package main
import ( import (
"flag"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -33,46 +32,43 @@ import (
"go.dev.pztrn.name/fastpastebin/domains/dbnotavailable" "go.dev.pztrn.name/fastpastebin/domains/dbnotavailable"
"go.dev.pztrn.name/fastpastebin/domains/indexpage" "go.dev.pztrn.name/fastpastebin/domains/indexpage"
"go.dev.pztrn.name/fastpastebin/domains/pastes" "go.dev.pztrn.name/fastpastebin/domains/pastes"
"go.dev.pztrn.name/fastpastebin/internal/application"
"go.dev.pztrn.name/fastpastebin/internal/captcha" "go.dev.pztrn.name/fastpastebin/internal/captcha"
"go.dev.pztrn.name/fastpastebin/internal/context"
"go.dev.pztrn.name/fastpastebin/internal/database" "go.dev.pztrn.name/fastpastebin/internal/database"
"go.dev.pztrn.name/fastpastebin/internal/templater" "go.dev.pztrn.name/fastpastebin/internal/templater"
) )
func main() { func main() {
appCtx := context.New()
appCtx.Initialize()
appCtx.Logger.Info().Msg("Starting Fast Pastebin...")
// Here goes initial initialization for packages that want CLI flags
// to be added.
// Parse flags.
flag.Parse()
// Continue loading.
appCtx.LoadConfiguration()
appCtx.InitializePost()
database.New(appCtx)
appCtx.Database.Initialize()
templater.Initialize(appCtx)
captcha.New(appCtx)
dbnotavailable.New(appCtx)
indexpage.New(appCtx)
pastes.New(appCtx)
// CTRL+C handler. // CTRL+C handler.
signalHandler := make(chan os.Signal, 1) signalHandler := make(chan os.Signal, 1)
shutdownDone := make(chan bool, 1) shutdownDone := make(chan bool, 1)
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM) signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
app := application.New()
app.Log.Info().Msg("Starting Fast Pastebin...")
database.New(app)
app.Database.Initialize()
templater.Initialize(app)
captcha.New(app)
dbnotavailable.New(app)
indexpage.New(app)
pastes.New(app)
if err := app.Start(); err != nil {
app.Log.Fatal().Err(err).Msg("Failed to start Fast Pastebin!")
}
go func() { go func() {
<-signalHandler <-signalHandler
appCtx.Shutdown()
if err := app.Shutdown(); err != nil {
app.Log.Error().Err(err).Msg("Fast Pastebin failed to shutdown!")
}
shutdownDone <- true shutdownDone <- true
}() }()

View File

@ -25,16 +25,16 @@
package dbnotavailable package dbnotavailable
import ( import (
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
) )
var ctx *context.Context var app *application.Application
// New initializes pastes package and adds necessary HTTP and API // New initializes pastes package and adds necessary HTTP and API
// endpoints. // endpoints.
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
ctx.Echo.GET("/database_not_available", dbNotAvailableGet) app.Echo.GET("/database_not_available", dbNotAvailableGet)
ctx.Echo.GET("/database_not_available/raw", dbNotAvailableRawGet) app.Echo.GET("/database_not_available/raw", dbNotAvailableRawGet)
} }

View File

@ -25,15 +25,15 @@
package indexpage package indexpage
import ( import (
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
) )
var ctx *context.Context var app *application.Application
// New initializes pastes package and adds necessary HTTP and API // New initializes pastes package and adds necessary HTTP and API
// endpoints. // endpoints.
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
ctx.Echo.GET("/", indexGet) app.Echo.GET("/", indexGet)
} }

View File

@ -37,8 +37,8 @@ import (
// Index of this site. // Index of this site.
func indexGet(ectx echo.Context) error { func indexGet(ectx echo.Context) error {
// We should check if database connection available. // We should check if database connection available.
dbConn := ctx.Database.GetDatabaseConnection() dbConn := app.Database.GetDatabaseConnection()
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
//nolint:wrapcheck //nolint:wrapcheck
return ectx.Redirect(http.StatusFound, "/database_not_available") return ectx.Redirect(http.StatusFound, "/database_not_available")
} }

View File

@ -27,35 +27,35 @@ package pastes
import ( import (
"regexp" "regexp"
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
) )
var regexInts = regexp.MustCompile("[0-9]+") var regexInts = regexp.MustCompile("[0-9]+")
var ctx *context.Context var app *application.Application
// New initializes pastes package and adds necessary HTTP and API // New initializes pastes package and adds necessary HTTP and API
// endpoints. // endpoints.
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// HTTP endpoints. // HTTP endpoints.
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// New paste. // New paste.
ctx.Echo.POST("/paste/", pastePOSTWebInterface) app.Echo.POST("/paste/", pastePOSTWebInterface)
// Show public paste. // Show public paste.
ctx.Echo.GET("/paste/:id", pasteGETWebInterface) app.Echo.GET("/paste/:id", pasteGETWebInterface)
// Show RAW representation of public paste. // Show RAW representation of public paste.
ctx.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface) app.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface)
// Show private paste. // Show private paste.
ctx.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface) app.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface)
// Show RAW representation of private paste. // Show RAW representation of private paste.
ctx.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGETWebInterface) app.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGETWebInterface)
// Verify access to passworded paste. // Verify access to passworded paste.
ctx.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet) app.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
ctx.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost) app.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
// Pastes list. // Pastes list.
ctx.Echo.GET("/pastes/", pastesGET) app.Echo.GET("/pastes/", pastesGET)
ctx.Echo.GET("/pastes/:page", pastesGET) app.Echo.GET("/pastes/:page", pastesGET)
} }

View File

@ -30,16 +30,16 @@ const (
// value (they both will be ignored), but private will. // value (they both will be ignored), but private will.
func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Paste, string) { func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Paste, string) {
// Get paste. // Get paste.
paste, err1 := ctx.Database.GetPaste(pasteID) paste, err1 := app.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste") app.Log.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste")
return nil, pasteNotFound return nil, pasteNotFound
} }
// Check if paste is expired. // Check if paste is expired.
if paste.IsExpired() { if paste.IsExpired() {
ctx.Logger.Error().Int("paste ID", pasteID).Msg("Paste is expired") app.Log.Error().Int("paste ID", pasteID).Msg("Paste is expired")
return nil, pasteExpired return nil, pasteExpired
} }
@ -48,7 +48,7 @@ func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Pa
if paste.Private { if paste.Private {
pasteTS := paste.CreatedAt.Unix() pasteTS := paste.CreatedAt.Unix()
if timestamp != pasteTS { if timestamp != pasteTS {
ctx.Logger.Error().Int("paste ID", pasteID).Int64("paste timestamp", pasteTS).Int64("provided timestamp", timestamp).Msg("Incorrect timestamp provided for private paste") app.Log.Error().Int("paste ID", pasteID).Int64("paste timestamp", pasteTS).Int64("provided timestamp", timestamp).Msg("Incorrect timestamp provided for private paste")
return nil, pasteTimestampInvalid return nil, pasteTimestampInvalid
} }
@ -77,7 +77,7 @@ func pasteGETWebInterface(ectx echo.Context) error {
// error. // error.
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
pasteIDStr := strconv.Itoa(pasteID) pasteIDStr := strconv.Itoa(pasteID)
ctx.Logger.Debug().Int("paste ID", pasteID).Msg("Trying to get paste data") app.Log.Debug().Int("paste ID", pasteID).Msg("Trying to get paste data")
// Check if we have timestamp passed. // Check if we have timestamp passed.
// If passed timestamp is invalid (isn't a real UNIX timestamp) we // If passed timestamp is invalid (isn't a real UNIX timestamp) we
@ -88,7 +88,7 @@ func pasteGETWebInterface(ectx echo.Context) error {
if tsProvidedStr != "" { if tsProvidedStr != "" {
tsProvided, err := strconv.ParseInt(tsProvidedStr, 10, 64) tsProvided, err := strconv.ParseInt(tsProvidedStr, 10, 64)
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Msg("Invalid timestamp provided for getting private paste") app.Log.Error().Err(err).Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Msg("Invalid timestamp provided for getting private paste")
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+pasteIDStr+" not found") errtpl := templater.GetErrorTemplate(ectx, "Paste #"+pasteIDStr+" not found")
@ -121,7 +121,7 @@ func pasteGETWebInterface(ectx echo.Context) error {
// If passed cookie value was invalid - go to paste authorization // If passed cookie value was invalid - go to paste authorization
// page. // page.
if err == pasteCookieInvalid { if err == pasteCookieInvalid {
ctx.Logger.Info().Int("paste ID", pasteID).Msg("Invalid cookie, redirecting to auth page") app.Log.Info().Int("paste ID", pasteID).Msg("Invalid cookie, redirecting to auth page")
//nolint:wrapcheck //nolint:wrapcheck
return ectx.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDStr+"/"+ectx.Param("timestamp")+"/verify") return ectx.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDStr+"/"+ectx.Param("timestamp")+"/verify")
@ -158,7 +158,7 @@ func pasteGETWebInterface(ectx echo.Context) error {
// Tokenize paste data. // Tokenize paste data.
lexered, err3 := lexer.Tokenise(nil, paste.Data) lexered, err3 := lexer.Tokenise(nil, paste.Data)
if err3 != nil { if err3 != nil {
ctx.Logger.Error().Err(err3).Msg("Failed to tokenize paste data") app.Log.Error().Err(err3).Msg("Failed to tokenize paste data")
} }
// Get style for HTML output. // Get style for HTML output.
style := styles.Get("monokai") style := styles.Get("monokai")
@ -173,7 +173,7 @@ func pasteGETWebInterface(ectx echo.Context) error {
err4 := formatter.Format(buf, style, lexered) err4 := formatter.Format(buf, style, lexered)
if err4 != nil { if err4 != nil {
ctx.Logger.Error().Err(err4).Msg("Failed to format paste data") app.Log.Error().Err(err4).Msg("Failed to format paste data")
} }
pasteData["pastedata"] = buf.String() pasteData["pastedata"] = buf.String()
@ -194,9 +194,9 @@ func pastePasswordedVerifyGet(ectx echo.Context) error {
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
// Get paste. // Get paste.
paste, err1 := ctx.Database.GetPaste(pasteID) paste, err1 := app.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste data") app.Log.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste data")
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+pasteIDRaw+" not found") errtpl := templater.GetErrorTemplate(ectx, "Paste #"+pasteIDRaw+" not found")
@ -208,19 +208,19 @@ func pastePasswordedVerifyGet(ectx echo.Context) error {
cookie, err := ectx.Cookie("PASTE-" + strconv.Itoa(pasteID)) cookie, err := ectx.Cookie("PASTE-" + strconv.Itoa(pasteID))
if err == nil { if err == nil {
// No cookie, redirect to auth page. // No cookie, redirect to auth page.
ctx.Logger.Debug().Msg("Paste cookie found, checking it...") app.Log.Debug().Msg("Paste cookie found, checking it...")
// Generate cookie value to check. // Generate cookie value to check.
cookieValue := paste.GenerateCryptedCookieValue() cookieValue := paste.GenerateCryptedCookieValue()
if cookieValue == cookie.Value { if cookieValue == cookie.Value {
ctx.Logger.Info().Msg("Valid cookie, redirecting to paste page...") app.Log.Info().Msg("Valid cookie, redirecting to paste page...")
//nolint:wrapcheck //nolint:wrapcheck
return ectx.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ectx.Param("timestamp")) return ectx.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ectx.Param("timestamp"))
} }
ctx.Logger.Debug().Msg("Invalid cookie, showing auth page") app.Log.Debug().Msg("Invalid cookie, showing auth page")
} }
// HTML data. // HTML data.
@ -237,9 +237,9 @@ func pastePasswordedVerifyGet(ectx echo.Context) error {
// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page. // POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
func pastePasswordedVerifyPost(ectx echo.Context) error { func pastePasswordedVerifyPost(ectx echo.Context) error {
// We should check if database connection available. // We should check if database connection available.
dbConn := ctx.Database.GetDatabaseConnection() dbConn := app.Database.GetDatabaseConnection()
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
//nolint:wrapcheck //nolint:wrapcheck
return ectx.Redirect(http.StatusFound, "/database_not_available") return ectx.Redirect(http.StatusFound, "/database_not_available")
} }
@ -249,12 +249,12 @@ func pastePasswordedVerifyPost(ectx echo.Context) error {
// We already get numbers from string, so we will not check strconv.Atoi() // We already get numbers from string, so we will not check strconv.Atoi()
// error. // error.
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
ctx.Logger.Debug().Int("paste ID", pasteID).Msg("Requesting paste") app.Log.Debug().Int("paste ID", pasteID).Msg("Requesting paste")
// Get paste. // Get paste.
paste, err1 := ctx.Database.GetPaste(pasteID) paste, err1 := app.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste") app.Log.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste")
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+strconv.Itoa(pasteID)+" not found") errtpl := templater.GetErrorTemplate(ectx, "Paste #"+strconv.Itoa(pasteID)+" not found")
//nolint:wrapcheck //nolint:wrapcheck
@ -263,7 +263,7 @@ func pastePasswordedVerifyPost(ectx echo.Context) error {
params, err2 := ectx.FormParams() params, err2 := ectx.FormParams()
if err2 != nil { if err2 != nil {
ctx.Logger.Debug().Msg("No form parameters passed") app.Log.Debug().Msg("No form parameters passed")
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+strconv.Itoa(pasteID)+" not found") errtpl := templater.GetErrorTemplate(ectx, "Paste #"+strconv.Itoa(pasteID)+" not found")
@ -294,8 +294,8 @@ func pastePasswordedVerifyPost(ectx echo.Context) error {
// Web interface version. // Web interface version.
func pasteRawGETWebInterface(ectx echo.Context) error { func pasteRawGETWebInterface(ectx echo.Context) error {
// We should check if database connection available. // We should check if database connection available.
dbConn := ctx.Database.GetDatabaseConnection() dbConn := app.Database.GetDatabaseConnection()
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
//nolint:wrapcheck //nolint:wrapcheck
return ectx.Redirect(http.StatusFound, "/database_not_available/raw") return ectx.Redirect(http.StatusFound, "/database_not_available/raw")
} }
@ -304,19 +304,19 @@ func pasteRawGETWebInterface(ectx echo.Context) error {
// We already get numbers from string, so we will not check strconv.Atoi() // We already get numbers from string, so we will not check strconv.Atoi()
// error. // error.
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
ctx.Logger.Debug().Int("paste ID", pasteID).Msg("Requesting paste data") app.Log.Debug().Int("paste ID", pasteID).Msg("Requesting paste data")
// Get paste. // Get paste.
paste, err1 := ctx.Database.GetPaste(pasteID) paste, err1 := app.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste from database") app.Log.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste from database")
//nolint:wrapcheck //nolint:wrapcheck
return ectx.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.") return ectx.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
} }
if paste.IsExpired() { if paste.IsExpired() {
ctx.Logger.Error().Int("paste ID", pasteID).Msg("Paste is expired") app.Log.Error().Int("paste ID", pasteID).Msg("Paste is expired")
//nolint:wrapcheck //nolint:wrapcheck
return ectx.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.") return ectx.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
@ -328,7 +328,7 @@ func pasteRawGETWebInterface(ectx echo.Context) error {
tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64) tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
if err2 != nil { if err2 != nil {
ctx.Logger.Error().Err(err2).Int("paste ID", pasteID).Str("provided timestamp", tsProvidedStr).Msg("Invalid timestamp provided for getting private paste") app.Log.Error().Err(err2).Int("paste ID", pasteID).Str("provided timestamp", tsProvidedStr).Msg("Invalid timestamp provided for getting private paste")
//nolint:wrapcheck //nolint:wrapcheck
return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found") return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
@ -336,7 +336,7 @@ func pasteRawGETWebInterface(ectx echo.Context) error {
pasteTS := paste.CreatedAt.Unix() pasteTS := paste.CreatedAt.Unix()
if tsProvided != pasteTS { if tsProvided != pasteTS {
ctx.Logger.Error().Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Int64("paste timestamp", pasteTS).Msg("Incorrect timestamp provided for private paste") app.Log.Error().Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Int64("paste timestamp", pasteTS).Msg("Incorrect timestamp provided for private paste")
//nolint:wrapcheck //nolint:wrapcheck
return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found") return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
@ -347,7 +347,7 @@ func pasteRawGETWebInterface(ectx echo.Context) error {
// ToDo: figure out how to handle passworded pastes here. // ToDo: figure out how to handle passworded pastes here.
// Return error for now. // Return error for now.
if paste.Password != "" { if paste.Password != "" {
ctx.Logger.Error().Int("paste ID", pasteID).Msg("Cannot render paste as raw: passworded paste. Patches welcome!") app.Log.Error().Int("paste ID", pasteID).Msg("Cannot render paste as raw: passworded paste. Patches welcome!")
return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found") return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
} }

View File

@ -22,15 +22,15 @@ const KeepPastesForever = "forever"
// requests comes from browsers via web interface. // requests comes from browsers via web interface.
func pastePOSTWebInterface(ectx echo.Context) error { func pastePOSTWebInterface(ectx echo.Context) error {
// We should check if database connection available. // We should check if database connection available.
dbConn := ctx.Database.GetDatabaseConnection() dbConn := app.Database.GetDatabaseConnection()
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
//nolint:wrapcheck //nolint:wrapcheck
return ectx.Redirect(http.StatusFound, "/database_not_available") return ectx.Redirect(http.StatusFound, "/database_not_available")
} }
params, err := ectx.FormParams() params, err := ectx.FormParams()
if err != nil { if err != nil {
ctx.Logger.Error().Msg("Passed paste form is empty") app.Log.Error().Msg("Passed paste form is empty")
errtpl := templater.GetErrorTemplate(ectx, "Cannot create empty paste") errtpl := templater.GetErrorTemplate(ectx, "Cannot create empty paste")
@ -38,11 +38,11 @@ func pastePOSTWebInterface(ectx echo.Context) error {
return ectx.HTML(http.StatusBadRequest, errtpl) return ectx.HTML(http.StatusBadRequest, errtpl)
} }
ctx.Logger.Debug().Msgf("Received parameters: %+v", params) app.Log.Debug().Msgf("Received parameters: %+v", params)
// Do nothing if paste contents is empty. // Do nothing if paste contents is empty.
if len(params["paste-contents"][0]) == 0 { if len(params["paste-contents"][0]) == 0 {
ctx.Logger.Debug().Msg("Empty paste submitted, ignoring") app.Log.Debug().Msg("Empty paste submitted, ignoring")
errtpl := templater.GetErrorTemplate(ectx, "Empty pastes aren't allowed.") errtpl := templater.GetErrorTemplate(ectx, "Empty pastes aren't allowed.")
@ -51,7 +51,7 @@ func pastePOSTWebInterface(ectx echo.Context) error {
} }
if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") && params["paste-keep-for"][0] != KeepPastesForever { if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") && params["paste-keep-for"][0] != KeepPastesForever {
ctx.Logger.Debug().Str("field value", params["paste-keep-for"][0]).Msg("'Keep paste for' field have invalid value") app.Log.Debug().Str("field value", params["paste-keep-for"][0]).Msg("'Keep paste for' field have invalid value")
errtpl := templater.GetErrorTemplate(ectx, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).") errtpl := templater.GetErrorTemplate(ectx, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
@ -61,7 +61,7 @@ func pastePOSTWebInterface(ectx echo.Context) error {
// Verify captcha. // Verify captcha.
if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) { if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) {
ctx.Logger.Debug().Str("captcha ID", params["paste-captcha-id"][0]).Str("captcha solution", params["paste-captcha-solution"][0]).Msg("Invalid captcha solution") app.Log.Debug().Str("captcha ID", params["paste-captcha-id"][0]).Str("captcha solution", params["paste-captcha-solution"][0]).Msg("Invalid captcha solution")
errtpl := templater.GetErrorTemplate(ectx, "Invalid captcha solution.") errtpl := templater.GetErrorTemplate(ectx, "Invalid captcha solution.")
@ -95,11 +95,11 @@ func pastePOSTWebInterface(ectx echo.Context) error {
keepFor, err = strconv.Atoi(keepForRaw) keepFor, err = strconv.Atoi(keepForRaw)
if err != nil { if err != nil {
if params["paste-keep-for"][0] == KeepPastesForever { if params["paste-keep-for"][0] == KeepPastesForever {
ctx.Logger.Debug().Msg("Keeping paste forever!") app.Log.Debug().Msg("Keeping paste forever!")
keepFor = 0 keepFor = 0
} else { } else {
ctx.Logger.Debug().Err(err).Msg("Failed to parse 'Keep for' integer") app.Log.Debug().Err(err).Msg("Failed to parse 'Keep for' integer")
errtpl := templater.GetErrorTemplate(ectx, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).") errtpl := templater.GetErrorTemplate(ectx, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
@ -138,9 +138,9 @@ func pastePOSTWebInterface(ectx echo.Context) error {
_ = paste.CreatePassword(pastePassword[0]) _ = paste.CreatePassword(pastePassword[0])
} }
pasteID, err2 := ctx.Database.SavePaste(paste) pasteID, err2 := app.Database.SavePaste(paste)
if err2 != nil { if err2 != nil {
ctx.Logger.Error().Err(err2).Msg("Failed to save paste") app.Log.Error().Err(err2).Msg("Failed to save paste")
errtpl := templater.GetErrorTemplate(ectx, "Failed to save paste. Please, try again later.") errtpl := templater.GetErrorTemplate(ectx, "Failed to save paste. Please, try again later.")
@ -149,7 +149,7 @@ func pastePOSTWebInterface(ectx echo.Context) error {
} }
newPasteIDAsString := strconv.FormatInt(pasteID, 10) newPasteIDAsString := strconv.FormatInt(pasteID, 10)
ctx.Logger.Debug().Msg("Paste saved, URL: /paste/" + newPasteIDAsString) app.Log.Debug().Msg("Paste saved, URL: /paste/" + newPasteIDAsString)
// Private pastes have it's timestamp in URL. // Private pastes have it's timestamp in URL.
if paste.Private { if paste.Private {

View File

@ -39,8 +39,8 @@ import (
// Web interface version. // Web interface version.
func pastesGET(ectx echo.Context) error { func pastesGET(ectx echo.Context) error {
// We should check if database connection available. // We should check if database connection available.
dbConn := ctx.Database.GetDatabaseConnection() dbConn := app.Database.GetDatabaseConnection()
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
//nolint:wrapcheck //nolint:wrapcheck
return ectx.Redirect(http.StatusFound, "/database_not_available") return ectx.Redirect(http.StatusFound, "/database_not_available")
} }
@ -54,17 +54,17 @@ func pastesGET(ectx echo.Context) error {
page, _ = strconv.Atoi(pageRaw) page, _ = strconv.Atoi(pageRaw)
} }
ctx.Logger.Debug().Int("page", page).Msg("Requested page") app.Log.Debug().Int("page", page).Msg("Requested page")
// Get pastes IDs. // Get pastes IDs.
pastes, err3 := ctx.Database.GetPagedPastes(page) pastes, err3 := app.Database.GetPagedPastes(page)
ctx.Logger.Debug().Int("count", len(pastes)).Msg("Got pastes") app.Log.Debug().Int("count", len(pastes)).Msg("Got pastes")
pastesString := "No pastes to show." pastesString := "No pastes to show."
// Show "No pastes to show" on any error for now. // Show "No pastes to show" on any error for now.
if err3 != nil { if err3 != nil {
ctx.Logger.Error().Err(err3).Msg("Failed to get pastes list from database") app.Log.Error().Err(err3).Msg("Failed to get pastes list from database")
noPastesToShowTpl := templater.GetErrorTemplate(ectx, "No pastes to show.") noPastesToShowTpl := templater.GetErrorTemplate(ectx, "No pastes to show.")
@ -100,8 +100,8 @@ func pastesGET(ectx echo.Context) error {
} }
// Pagination. // Pagination.
pages := ctx.Database.GetPastesPages() pages := app.Database.GetPastesPages()
ctx.Logger.Debug().Int("total pages", pages).Int("current page", page).Msg("Paging data") app.Log.Debug().Int("total pages", pages).Int("current page", page).Msg("Paging data")
paginationHTML := pagination.CreateHTML(page, pages, "/pastes/") paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
pasteListTpl := templater.GetTemplate(ectx, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML}) pasteListTpl := templater.GetTemplate(ectx, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})

View File

@ -0,0 +1,124 @@
package application
import (
"context"
"fmt"
"os"
"sync"
"github.com/labstack/echo"
"github.com/rs/zerolog"
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
)
// Application is a main application superstructure. It passes around all parts of application
// and serves as lifecycle management thing as well as kind-of-dependency-injection thing.
type Application struct {
Config *Config
Database databaseinterface.Interface
Echo *echo.Echo
Log zerolog.Logger
services map[string]Service
servicesMutex sync.RWMutex
ctx context.Context
cancelFunc context.CancelFunc
}
// New creates new application superstructure.
func New() *Application {
//nolint:exhaustruct
appl := &Application{}
appl.initialize()
return appl
}
// GetContext returns application-wide context.
func (a *Application) GetContext() context.Context {
return a.ctx
}
// GetService returns interface{} with requested service or error if service wasn't registered.
func (a *Application) GetService(name string) (Service, error) {
a.servicesMutex.RLock()
srv, found := a.services[name]
a.servicesMutex.RUnlock()
if !found {
return nil, fmt.Errorf("%s: %w", ErrApplicationError, ErrApplicationServiceNotRegistered)
}
return srv, nil
}
// Initializes internal state.
func (a *Application) initialize() {
a.Log = zerolog.New(os.Stdout).With().Timestamp().Logger()
a.Log.Info().Msg("Initializing Application...")
a.ctx, a.cancelFunc = context.WithCancel(context.Background())
a.services = make(map[string]Service)
cfg, err := newConfig(a)
if err != nil {
a.Log.Fatal().Err(err).Msg("Failed to initialize configuration!")
}
a.Config = cfg
a.initializeLogger()
a.initializeHTTPServer()
}
// RegisterService registers service for later re-use everywhere it's needed.
func (a *Application) RegisterService(srv Service) error {
a.servicesMutex.Lock()
_, found := a.services[srv.GetName()]
a.servicesMutex.Unlock()
if found {
return fmt.Errorf("%s: %w", ErrApplicationError, ErrApplicationServiceAlreadyRegistered)
}
if err := srv.Initialize(); err != nil {
return fmt.Errorf("%s: %s: %w", ErrApplicationError, ErrApplicationServiceRegister, err)
}
a.services[srv.GetName()] = srv
return nil
}
// Shutdown shutdowns application.
func (a *Application) Shutdown() error {
a.cancelFunc()
a.servicesMutex.RLock()
defer a.servicesMutex.RUnlock()
for _, service := range a.services {
if err := service.Shutdown(); err != nil {
return err
}
}
return nil
}
// Start starts application.
func (a *Application) Start() error {
a.initializeLoggerPost()
a.startHTTPServer()
a.servicesMutex.RLock()
defer a.servicesMutex.RUnlock()
for _, service := range a.services {
if err := service.Start(); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,18 @@
package application
import "errors"
var (
// ErrApplicationError indicates that error belongs to Application.
ErrApplicationError = errors.New("application")
// ErrApplicationServiceRegister appears when trying to register (and initialize) a service
// but something went wrong.
ErrApplicationServiceRegister = errors.New("service registering and initialization")
// ErrApplicationServiceAlreadyRegistered appears when trying to register service with already used name.
ErrApplicationServiceAlreadyRegistered = errors.New("service already registered")
// ErrApplicationServiceNotRegistered appears when trying to obtain a service that wasn't previously registered.
ErrApplicationServiceNotRegistered = errors.New("service not registered")
)

View File

@ -0,0 +1,48 @@
package application
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"go.dev.pztrn.name/fastpastebin/assets"
)
// Wrapper around previous function.
func (a *Application) echoReqLogger() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ectx echo.Context) error {
a.Log.Info().
Str("IP", ectx.RealIP()).
Str("Host", ectx.Request().Host).
Str("Method", ectx.Request().Method).
Str("Path", ectx.Request().URL.Path).
Str("UA", ectx.Request().UserAgent()).
Msg("HTTP request")
return next(ectx)
}
}
}
func (a *Application) initializeHTTPServer() {
a.Echo = echo.New()
a.Echo.Use(a.echoReqLogger())
a.Echo.Use(middleware.Recover())
a.Echo.Use(middleware.BodyLimit(a.Config.HTTP.MaxBodySizeMegabytes + "M"))
a.Echo.DisableHTTP2 = true
a.Echo.HideBanner = true
a.Echo.HidePort = true
// Static files.
a.Echo.GET("/static/*", echo.WrapHandler(http.FileServer(http.FS(assets.Data))))
}
func (a *Application) startHTTPServer() {
listenAddress := a.Config.HTTP.Address + ":" + a.Config.HTTP.Port
go func() {
a.Echo.Logger.Fatal(a.Echo.Start(listenAddress))
}()
a.Log.Info().Str("address", listenAddress).Msg("Started HTTP server")
}

View File

@ -1,4 +1,4 @@
package context package application
import ( import (
"fmt" "fmt"
@ -11,7 +11,7 @@ import (
) )
// Puts memory usage into log lines. // Puts memory usage into log lines.
func (c *Context) getMemoryUsage(event *zerolog.Event, level zerolog.Level, message string) { func (a *Application) getMemoryUsage(event *zerolog.Event, level zerolog.Level, message string) {
var memstats runtime.MemStats var memstats runtime.MemStats
runtime.ReadMemStats(&memstats) runtime.ReadMemStats(&memstats)
@ -22,7 +22,7 @@ func (c *Context) getMemoryUsage(event *zerolog.Event, level zerolog.Level, mess
} }
// Initializes logger. // Initializes logger.
func (c *Context) initializeLogger() { func (a *Application) initializeLogger() {
// Устанавливаем форматирование логгера. // Устанавливаем форматирование логгера.
//nolint:exhaustruct //nolint:exhaustruct
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false, TimeFormat: time.RFC3339} output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false, TimeFormat: time.RFC3339}
@ -52,17 +52,17 @@ func (c *Context) initializeLogger() {
return fmt.Sprintf("| %s |", lvl) return fmt.Sprintf("| %s |", lvl)
} }
c.Logger = zerolog.New(output).With().Timestamp().Logger() a.Log = zerolog.New(output).With().Timestamp().Logger()
c.Logger = c.Logger.Hook(zerolog.HookFunc(c.getMemoryUsage)) a.Log = a.Log.Hook(zerolog.HookFunc(a.getMemoryUsage))
} }
// Initialize logger after configuration parse. // Initialize logger after configuration parse.
func (c *Context) initializeLoggerPost() { func (a *Application) initializeLoggerPost() {
// Set log level. // Set log level.
c.Logger.Info().Msgf("Setting logger level: %s", c.Config.Logging.LogLevel) a.Log.Info().Msgf("Setting logger level: %s", a.Config.Logging.LogLevel)
switch c.Config.Logging.LogLevel { switch a.Config.Logging.LogLevel {
case "DEBUG": case "DEBUG":
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "INFO": case "INFO":

View File

@ -0,0 +1,13 @@
package application
// Service is a generic interface for all services of application.
type Service interface {
// GetName returns service name for registering with application superstructure.
GetName() string
// Initialize initializes service.
Initialize() error
// Shutdown shuts service down if needed. Also should block is shutdown should be done in synchronous manner.
Shutdown() error
// Start starts service if needed. Should not block execution.
Start() error
}

View File

@ -0,0 +1,64 @@
package application
import (
"fmt"
"os"
"github.com/rs/zerolog"
"go.dev.pztrn.name/fastpastebin/internal/helpers"
"gopkg.in/yaml.v2"
)
// Config represents configuration structure.
type Config struct {
app *Application
log zerolog.Logger
Database ConfigDatabase `yaml:"database"`
Logging ConfigLogging `yaml:"logging"`
HTTP ConfigHTTP `yaml:"http"`
Pastes ConfigPastes `yaml:"pastes"`
}
func newConfig(app *Application) (*Config, error) {
//nolint:exhaustruct
cfg := &Config{
app: app,
log: app.Log.With().Str("type", "core").Str("name", "configuration").Logger(),
}
if err := cfg.initialize(); err != nil {
return nil, fmt.Errorf("%s: %w", ErrConfigurationError, err)
}
return cfg, nil
}
func (c *Config) initialize() error {
c.log.Info().Msg("Initializing configuration...")
configPathRaw, found := os.LookupEnv("FASTPASTEBIN_CONFIG")
if !found {
return fmt.Errorf("%s: %w", ErrConfigurationLoad, ErrConfigurationPathNotDefined)
}
configPath, err := helpers.NormalizePath(configPathRaw)
if err != nil {
return fmt.Errorf("%s: %w", ErrConfigurationLoad, err)
}
c.log.Info().Str("config path", configPath).Msg("Reading configuration file...")
fileData, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("%s: %w", ErrConfigurationLoad, err)
}
if err := yaml.Unmarshal(fileData, c); err != nil {
return fmt.Errorf("%s: %w", ErrConfigurationLoad, err)
}
c.log.Debug().Msgf("Configuration loaded: %+v", c)
return nil
}

View File

@ -0,0 +1,15 @@
package application
import "errors"
var (
// ErrConfigurationError indicates that this error is related to configuration.
ErrConfigurationError = errors.New("configuration")
// ErrConfigurationLoad indicates that error appears when trying to load
// configuration data from file.
ErrConfigurationLoad = errors.New("loading configuration")
// ErrConfigurationPathNotDefined indicates that CONFIG_PATH environment variable is empty or not defined.
ErrConfigurationPathNotDefined = errors.New("configuration path (CONFIG_PATH) is empty or not defined")
)

View File

@ -0,0 +1,30 @@
package application
// ConfigDatabase describes database configuration.
type ConfigDatabase struct {
Type string `yaml:"type"`
Path string `yaml:"path"`
Address string `yaml:"address"`
Port string `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Database string `yaml:"database"`
}
// ConfigHTTP describes HTTP server configuration.
type ConfigHTTP struct {
Address string `yaml:"address"`
Port string `yaml:"port"`
MaxBodySizeMegabytes string `yaml:"max_body_size_megabytes"`
AllowInsecure bool `yaml:"allow_insecure"`
}
// ConfigLogging describes logger configuration.
type ConfigLogging struct {
LogLevel string `yaml:"loglevel"`
}
// ConfigPastes describes pastes subsystem configuration.
type ConfigPastes struct {
Pagination int `yaml:"pagination"`
}

View File

@ -0,0 +1,6 @@
package application
const (
// Version .
Version = "0.4.1"
)

View File

@ -28,22 +28,22 @@ import (
"github.com/dchest/captcha" "github.com/dchest/captcha"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
) )
var ( var (
ctx *context.Context app *application.Application
log zerolog.Logger log zerolog.Logger
) )
// New initializes captcha package and adds necessary HTTP and API // New initializes captcha package and adds necessary HTTP and API
// endpoints. // endpoints.
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
log = ctx.Logger.With().Str("type", "internal").Str("package", "captcha").Logger() log = app.Log.With().Str("type", "internal").Str("package", "captcha").Logger()
// New paste. // New paste.
ctx.Echo.GET("/captcha/:id.png", echo.WrapHandler(captcha.Server(captcha.StdWidth, captcha.StdHeight))) app.Echo.GET("/captcha/:id.png", echo.WrapHandler(captcha.Server(captcha.StdWidth, captcha.StdHeight)))
} }
// NewCaptcha creates new captcha string. // NewCaptcha creates new captcha string.

View File

@ -1,126 +0,0 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package context
import (
"flag"
"os"
"path/filepath"
"github.com/labstack/echo"
"github.com/rs/zerolog"
"go.dev.pztrn.name/fastpastebin/internal/config"
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
"gopkg.in/yaml.v2"
)
// Context is a some sort of singleton. Basically it's a structure that
// initialized once and then passed to all parts of application. It
// contains everything every part of application need, like configuration
// access, logger, etc.
type Context struct {
Config *config.Struct
Database databaseinterface.Interface
Echo *echo.Echo
Logger zerolog.Logger
configPathFromCLI string
}
// Initialize initializes context.
func (c *Context) Initialize() {
c.initializeLogger()
flag.StringVar(&c.configPathFromCLI, "config", "NO_CONFIG", "Configuration file path. Can be overridded with FASTPASTEBIN_CONFIG environment variable.")
}
// InitializePost initializes everything that needs a configuration.
func (c *Context) InitializePost() {
c.initializeLoggerPost()
c.initializeHTTPServer()
}
// LoadConfiguration loads configuration and executes right after Flagger
// have parsed CLI flags, because it depends on "-config" defined in
// Initialize().
func (c *Context) LoadConfiguration() {
c.Logger.Info().Msg("Loading configuration...")
configPath := c.configPathFromCLI
// We're accepting configuration path from "-config" CLI parameter
// and FASTPASTEBIN_CONFIG environment variable. Later have higher
// weight and can override "-config" value.
configPathFromEnv, configPathFromEnvFound := os.LookupEnv("FASTPASTEBIN_CONFIG")
if configPathFromEnvFound {
configPath = configPathFromEnv
}
if configPath == "NO_CONFIG" {
c.Logger.Panic().Msg("Configuration file path wasn't passed via '-config' or 'FASTPASTEBIN_CONFIG' environment variable. Cannot continue.")
}
// Normalize file path.
normalizedConfigPath, err1 := filepath.Abs(configPath)
if err1 != nil {
c.Logger.Fatal().Err(err1).Msg("Failed to normalize path to configuration file")
}
c.Logger.Debug().Str("path", configPath).Msg("Configuration file path")
//nolint:exhaustruct
c.Config = &config.Struct{}
// Read configuration file.
fileData, err2 := os.ReadFile(normalizedConfigPath)
if err2 != nil {
c.Logger.Panic().Err(err2).Msg("Failed to read configuration file")
}
// Parse it into structure.
err3 := yaml.Unmarshal(fileData, c.Config)
if err3 != nil {
c.Logger.Panic().Err(err3).Msg("Failed to parse configuration file")
}
// Yay! See what it gets!
c.Logger.Debug().Msgf("Parsed configuration: %+v", c.Config)
}
// RegisterDatabaseInterface registers database interface for later use.
func (c *Context) RegisterDatabaseInterface(di databaseinterface.Interface) {
c.Database = di
}
// RegisterEcho registers Echo instance for later usage.
func (c *Context) RegisterEcho(e *echo.Echo) {
c.Echo = e
}
// Shutdown shutdowns entire application.
func (c *Context) Shutdown() {
c.Logger.Info().Msg("Shutting down Fast Pastebin...")
c.Database.Shutdown()
}

View File

@ -1,36 +0,0 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package context
const (
// Version .
Version = "0.4.1"
)
// New creates new context.
func New() *Context {
//nolint:exhaustruct
return &Context{}
}

View File

@ -1,46 +0,0 @@
package context
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"go.dev.pztrn.name/fastpastebin/assets"
)
func (c *Context) initializeHTTPServer() {
c.Echo = echo.New()
c.Echo.Use(c.echoReqLogger())
c.Echo.Use(middleware.Recover())
c.Echo.Use(middleware.BodyLimit(c.Config.HTTP.MaxBodySizeMegabytes + "M"))
c.Echo.DisableHTTP2 = true
c.Echo.HideBanner = true
c.Echo.HidePort = true
// Static files.
c.Echo.GET("/static/*", echo.WrapHandler(http.FileServer(http.FS(assets.Data))))
listenAddress := c.Config.HTTP.Address + ":" + c.Config.HTTP.Port
go func() {
c.Echo.Logger.Fatal(c.Echo.Start(listenAddress))
}()
c.Logger.Info().Str("address", listenAddress).Msg("Started HTTP server")
}
// Wrapper around previous function.
func (c *Context) echoReqLogger() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ectx echo.Context) error {
c.Logger.Info().
Str("IP", ectx.RealIP()).
Str("Host", ectx.Request().Host).
Str("Method", ectx.Request().Method).
Str("Path", ectx.Request().URL.Path).
Str("UA", ectx.Request().UserAgent()).
Msg("HTTP request")
return next(ectx)
}
}
}

View File

@ -46,7 +46,7 @@ type Database struct {
// a subject of change in future. // a subject of change in future.
func (db *Database) cleanup() { func (db *Database) cleanup() {
for { for {
ctx.Logger.Info().Msg("Starting pastes cleanup procedure...") app.Log.Info().Msg("Starting pastes cleanup procedure...")
pages := db.db.GetPastesPages() pages := db.db.GetPastesPages()
@ -55,7 +55,7 @@ func (db *Database) cleanup() {
for i := 0; i < pages; i++ { for i := 0; i < pages; i++ {
pastes, err := db.db.GetPagedPastes(i) pastes, err := db.db.GetPagedPastes(i)
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Int("page", i).Msg("Failed to perform database cleanup") app.Log.Error().Err(err).Int("page", i).Msg("Failed to perform database cleanup")
} }
for _, paste := range pastes { for _, paste := range pastes {
@ -68,11 +68,11 @@ func (db *Database) cleanup() {
for _, pasteID := range pasteIDsToRemove { for _, pasteID := range pasteIDsToRemove {
err := db.DeletePaste(pasteID) err := db.DeletePaste(pasteID)
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Int("paste", pasteID).Msg("Failed to delete paste!") app.Log.Error().Err(err).Int("paste", pasteID).Msg("Failed to delete paste!")
} }
} }
ctx.Logger.Info().Msg("Pastes cleanup done.") app.Log.Info().Msg("Pastes cleanup done.")
time.Sleep(time.Hour) time.Sleep(time.Hour)
} }
@ -107,16 +107,16 @@ func (db *Database) GetPastesPages() int {
// Initialize initializes connection to database. // Initialize initializes connection to database.
func (db *Database) Initialize() { func (db *Database) Initialize() {
ctx.Logger.Info().Msg("Initializing database connection...") app.Log.Info().Msg("Initializing database connection...")
if ctx.Config.Database.Type == "mysql" { if app.Config.Database.Type == "mysql" {
mysql.New(ctx) mysql.New(app)
} else if ctx.Config.Database.Type == flatfiles.FlatFileDialect { } else if app.Config.Database.Type == flatfiles.FlatFileDialect {
flatfiles.New(ctx) flatfiles.New(app)
} else if ctx.Config.Database.Type == "postgresql" { } else if app.Config.Database.Type == "postgresql" {
postgresql.New(ctx) postgresql.New(app)
} else { } else {
ctx.Logger.Fatal().Str("type", ctx.Config.Database.Type).Msg("Unknown database type") app.Log.Fatal().Str("type", app.Config.Database.Type).Msg("Unknown database type")
} }
go db.cleanup() go db.cleanup()

View File

@ -25,21 +25,21 @@
package flatfiles package flatfiles
import ( import (
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface" dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
) )
const FlatFileDialect = "flatfiles" const FlatFileDialect = "flatfiles"
var ( var (
ctx *context.Context app *application.Application
flf *FlatFiles flf *FlatFiles
) )
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
//nolint:exhaustruct //nolint:exhaustruct
flf = &FlatFiles{} flf = &FlatFiles{}
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{})) app.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
} }

View File

@ -48,7 +48,7 @@ func (ff *FlatFiles) DeletePaste(pasteID int) error {
// Delete from disk. // Delete from disk.
err := os.Remove(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json")) err := os.Remove(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json"))
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Msg("Failed to delete paste!") app.Log.Error().Err(err).Msg("Failed to delete paste!")
//nolint:wrapcheck //nolint:wrapcheck
return err return err
@ -82,17 +82,17 @@ func (ff *FlatFiles) GetDatabaseConnection() *sql.DB {
func (ff *FlatFiles) GetPaste(pasteID int) (*structs.Paste, error) { func (ff *FlatFiles) GetPaste(pasteID int) (*structs.Paste, error) {
ff.writeMutex.Lock() ff.writeMutex.Lock()
pastePath := filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json") pastePath := filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json")
ctx.Logger.Debug().Str("path", pastePath).Msg("Trying to load paste data") app.Log.Debug().Str("path", pastePath).Msg("Trying to load paste data")
pasteInBytes, err := os.ReadFile(pastePath) pasteInBytes, err := os.ReadFile(pastePath)
if err != nil { if err != nil {
ctx.Logger.Debug().Err(err).Msg("Failed to read paste from storage") app.Log.Debug().Err(err).Msg("Failed to read paste from storage")
//nolint:wrapcheck //nolint:wrapcheck
return nil, err return nil, err
} }
ctx.Logger.Debug().Int("paste bytes", len(pasteInBytes)).Msg("Loaded paste") app.Log.Debug().Int("paste bytes", len(pasteInBytes)).Msg("Loaded paste")
ff.writeMutex.Unlock() ff.writeMutex.Unlock()
//nolint:exhaustruct //nolint:exhaustruct
@ -100,7 +100,7 @@ func (ff *FlatFiles) GetPaste(pasteID int) (*structs.Paste, error) {
err1 := json.Unmarshal(pasteInBytes, paste) err1 := json.Unmarshal(pasteInBytes, paste)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Msgf("Failed to parse paste") app.Log.Error().Err(err1).Msgf("Failed to parse paste")
//nolint:wrapcheck //nolint:wrapcheck
return nil, err1 return nil, err1
@ -113,7 +113,7 @@ func (ff *FlatFiles) GetPagedPastes(page int) ([]structs.Paste, error) {
// Pagination. // Pagination.
startPagination := 0 startPagination := 0
if page > 1 { if page > 1 {
startPagination = (page - 1) * ctx.Config.Pastes.Pagination startPagination = (page - 1) * app.Config.Pastes.Pagination
} }
// Iteration one - get only public pastes. // Iteration one - get only public pastes.
@ -129,23 +129,23 @@ func (ff *FlatFiles) GetPagedPastes(page int) ([]structs.Paste, error) {
pastesData := make([]structs.Paste, 0) pastesData := make([]structs.Paste, 0)
for idx, paste := range publicPastes { for idx, paste := range publicPastes {
if len(pastesData) == ctx.Config.Pastes.Pagination { if len(pastesData) == app.Config.Pastes.Pagination {
break break
} }
if idx < startPagination { if idx < startPagination {
ctx.Logger.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too low index") app.Log.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too low index")
continue continue
} }
if (idx-1 >= startPagination && page > 1 && idx > startPagination+((page-1)*ctx.Config.Pastes.Pagination)) || (idx-1 >= startPagination && page == 1 && idx > startPagination+(page*ctx.Config.Pastes.Pagination)) { if (idx-1 >= startPagination && page > 1 && idx > startPagination+((page-1)*app.Config.Pastes.Pagination)) || (idx-1 >= startPagination && page == 1 && idx > startPagination+(page*app.Config.Pastes.Pagination)) {
ctx.Logger.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too high index") app.Log.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too high index")
break break
} }
ctx.Logger.Debug().Int("ID", paste.ID).Int("index", idx).Msg("Getting paste data") app.Log.Debug().Int("ID", paste.ID).Int("index", idx).Msg("Getting paste data")
// Get paste data. // Get paste data.
//nolint:exhaustruct //nolint:exhaustruct
@ -153,14 +153,14 @@ func (ff *FlatFiles) GetPagedPastes(page int) ([]structs.Paste, error) {
pasteRawData, err := os.ReadFile(filepath.Join(ff.path, "pastes", strconv.Itoa(paste.ID)+".json")) pasteRawData, err := os.ReadFile(filepath.Join(ff.path, "pastes", strconv.Itoa(paste.ID)+".json"))
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Msg("Failed to read paste data") app.Log.Error().Err(err).Msg("Failed to read paste data")
continue continue
} }
err1 := json.Unmarshal(pasteRawData, pasteData) err1 := json.Unmarshal(pasteRawData, pasteData)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Msg("Failed to parse paste data") app.Log.Error().Err(err1).Msg("Failed to parse paste data")
continue continue
} }
@ -184,9 +184,9 @@ func (ff *FlatFiles) GetPastesPages() int {
ff.writeMutex.Unlock() ff.writeMutex.Unlock()
// Calculate pages. // Calculate pages.
pages := len(publicPastes) / ctx.Config.Pastes.Pagination pages := len(publicPastes) / app.Config.Pastes.Pagination
// Check if we have any remainder. Add 1 to pages count if so. // Check if we have any remainder. Add 1 to pages count if so.
if len(publicPastes)%ctx.Config.Pastes.Pagination > 0 { if len(publicPastes)%app.Config.Pastes.Pagination > 0 {
pages++ pages++
} }
@ -194,14 +194,14 @@ func (ff *FlatFiles) GetPastesPages() int {
} }
func (ff *FlatFiles) Initialize() { func (ff *FlatFiles) Initialize() {
ctx.Logger.Info().Msg("Initializing flatfiles storage...") app.Log.Info().Msg("Initializing flatfiles storage...")
path := ctx.Config.Database.Path path := app.Config.Database.Path
// Get proper paste file path. // Get proper paste file path.
if strings.Contains(ctx.Config.Database.Path, "~") { if strings.Contains(app.Config.Database.Path, "~") {
curUser, err := user.Current() curUser, err := user.Current()
if err != nil { if err != nil {
ctx.Logger.Error().Msg("Failed to get current user. Will replace '~' for '/' in storage path!") app.Log.Error().Msg("Failed to get current user. Will replace '~' for '/' in storage path!")
path = strings.Replace(path, "~", "/", -1) path = strings.Replace(path, "~", "/", -1)
} }
@ -212,40 +212,40 @@ func (ff *FlatFiles) Initialize() {
path, _ = filepath.Abs(path) path, _ = filepath.Abs(path)
ff.path = path ff.path = path
ctx.Logger.Debug().Msgf("Storage path is now: %s", ff.path) app.Log.Debug().Msgf("Storage path is now: %s", ff.path)
// Create directory if necessary. // Create directory if necessary.
if _, err := os.Stat(ff.path); err != nil { if _, err := os.Stat(ff.path); err != nil {
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...") app.Log.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...")
_ = os.MkdirAll(ff.path, os.ModePerm) _ = os.MkdirAll(ff.path, os.ModePerm)
} else { } else {
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory already exists") app.Log.Debug().Str("directory", ff.path).Msg("Directory already exists")
} }
// Create directory for pastes. // Create directory for pastes.
if _, err := os.Stat(filepath.Join(ff.path, "pastes")); err != nil { if _, err := os.Stat(filepath.Join(ff.path, "pastes")); err != nil {
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...") app.Log.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...")
_ = os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm) _ = os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm)
} else { } else {
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory already exists") app.Log.Debug().Str("directory", ff.path).Msg("Directory already exists")
} }
// Load pastes index. // Load pastes index.
ff.pastesIndex = []Index{} ff.pastesIndex = []Index{}
if _, err := os.Stat(filepath.Join(ff.path, "pastes", "index.json")); err != nil { if _, err := os.Stat(filepath.Join(ff.path, "pastes", "index.json")); err != nil {
ctx.Logger.Warn().Msg("Pastes index file does not exist, will create new one") app.Log.Warn().Msg("Pastes index file does not exist, will create new one")
} else { } else {
indexData, err := os.ReadFile(filepath.Join(ff.path, "pastes", "index.json")) indexData, err := os.ReadFile(filepath.Join(ff.path, "pastes", "index.json"))
if err != nil { if err != nil {
ctx.Logger.Fatal().Msg("Failed to read contents of index file!") app.Log.Fatal().Msg("Failed to read contents of index file!")
} }
err1 := json.Unmarshal(indexData, &ff.pastesIndex) err1 := json.Unmarshal(indexData, &ff.pastesIndex)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Msg("Failed to parse index file contents from JSON into internal structure. Will create new index file. All of your previous pastes will became unavailable.") app.Log.Error().Err(err1).Msg("Failed to parse index file contents from JSON into internal structure. Will create new index file. All of your previous pastes will became unavailable.")
} }
ctx.Logger.Debug().Int("pastes count", len(ff.pastesIndex)).Msg("Parsed pastes index") app.Log.Debug().Int("pastes count", len(ff.pastesIndex)).Msg("Parsed pastes index")
} }
} }
@ -256,7 +256,7 @@ func (ff *FlatFiles) SavePaste(paste *structs.Paste) (int64, error) {
pasteID := len(filesOnDisk) + 1 pasteID := len(filesOnDisk) + 1
paste.ID = pasteID paste.ID = pasteID
ctx.Logger.Debug().Int("new paste ID", pasteID).Msg("Writing paste to disk") app.Log.Debug().Int("new paste ID", pasteID).Msg("Writing paste to disk")
data, err := json.Marshal(paste) data, err := json.Marshal(paste)
if err != nil { if err != nil {
@ -286,18 +286,18 @@ func (ff *FlatFiles) SavePaste(paste *structs.Paste) (int64, error) {
} }
func (ff *FlatFiles) Shutdown() { func (ff *FlatFiles) Shutdown() {
ctx.Logger.Info().Msg("Saving indexes...") app.Log.Info().Msg("Saving indexes...")
indexData, err := json.Marshal(ff.pastesIndex) indexData, err := json.Marshal(ff.pastesIndex)
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Msg("Failed to encode index data into JSON") app.Log.Error().Err(err).Msg("Failed to encode index data into JSON")
return return
} }
err1 := os.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0o600) err1 := os.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0o600)
if err1 != nil { if err1 != nil {
ctx.Logger.Error().Err(err1).Msg("Failed to write index data to file. Pretty sure that you've lost your pastes.") app.Log.Error().Err(err1).Msg("Failed to write index data to file. Pretty sure that you've lost your pastes.")
return return
} }

View File

@ -25,19 +25,19 @@
package mysql package mysql
import ( import (
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface" dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
) )
var ( var (
ctx *context.Context app *application.Application
dbAdapter *Database dbAdapter *Database
) )
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
//nolint:exhaustruct //nolint:exhaustruct
dbAdapter = &Database{} dbAdapter = &Database{}
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{})) app.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
} }

View File

@ -26,19 +26,19 @@ package migrations
import ( import (
"github.com/pressly/goose" "github.com/pressly/goose"
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
) )
var ctx *context.Context var app *application.Application
// New initializes migrations. // New initializes migrations.
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
} }
// Migrate launching migrations. // Migrate launching migrations.
func Migrate() { func Migrate() {
ctx.Logger.Info().Msg("Migrating database...") app.Log.Info().Msg("Migrating database...")
_ = goose.SetDialect("mysql") _ = goose.SetDialect("mysql")
goose.AddNamedMigration("1_initial.go", InitialUp, nil) goose.AddNamedMigration("1_initial.go", InitialUp, nil)
@ -47,13 +47,13 @@ func Migrate() {
goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown) goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown)
// Add new migrations BEFORE this message. // Add new migrations BEFORE this message.
dbConn := ctx.Database.GetDatabaseConnection() dbConn := app.Database.GetDatabaseConnection()
if dbConn != nil { if dbConn != nil {
err := goose.Up(dbConn, ".") err := goose.Up(dbConn, ".")
if err != nil { if err != nil {
ctx.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error()) app.Log.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
} }
} else { } else {
ctx.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping") app.Log.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
} }
} }

View File

@ -103,10 +103,10 @@ func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
// Pagination. // Pagination.
startPagination := 0 startPagination := 0
if page > 1 { if page > 1 {
startPagination = (page - 1) * ctx.Config.Pastes.Pagination startPagination = (page - 1) * app.Config.Pastes.Pagination
} }
err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM `pastes` WHERE private != true ORDER BY id DESC LIMIT ? OFFSET ?"), ctx.Config.Pastes.Pagination, startPagination) err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM `pastes` WHERE private != true ORDER BY id DESC LIMIT ? OFFSET ?"), app.Config.Pastes.Pagination, startPagination)
if err != nil { if err != nil {
//nolint:wrapcheck //nolint:wrapcheck
return nil, err return nil, err
@ -142,9 +142,9 @@ func (db *Database) GetPastesPages() int {
} }
// Calculate pages. // Calculate pages.
pages := len(pastes) / ctx.Config.Pastes.Pagination pages := len(pastes) / app.Config.Pastes.Pagination
// Check if we have any remainder. Add 1 to pages count if so. // Check if we have any remainder. Add 1 to pages count if so.
if len(pastes)%ctx.Config.Pastes.Pagination > 0 { if len(pastes)%app.Config.Pastes.Pagination > 0 {
pages++ pages++
} }
@ -153,23 +153,23 @@ func (db *Database) GetPastesPages() int {
// Initialize initializes MySQL/MariaDB connection. // Initialize initializes MySQL/MariaDB connection.
func (db *Database) Initialize() { func (db *Database) Initialize() {
ctx.Logger.Info().Msg("Initializing database connection...") app.Log.Info().Msg("Initializing database connection...")
// There might be only user, without password. MySQL/MariaDB driver // There might be only user, without password. MySQL/MariaDB driver
// in DSN wants "user" or "user:password", "user:" is invalid. // in DSN wants "user" or "user:password", "user:" is invalid.
var userpass string var userpass string
if ctx.Config.Database.Password == "" { if app.Config.Database.Password == "" {
userpass = ctx.Config.Database.Username userpass = app.Config.Database.Username
} else { } else {
userpass = ctx.Config.Database.Username + ":" + ctx.Config.Database.Password userpass = app.Config.Database.Username + ":" + app.Config.Database.Password
} }
dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, ctx.Config.Database.Address, ctx.Config.Database.Port, ctx.Config.Database.Database) dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, app.Config.Database.Address, app.Config.Database.Port, app.Config.Database.Database)
ctx.Logger.Debug().Str("DSN", dbConnString).Msgf("Database connection string composed") app.Log.Debug().Str("DSN", dbConnString).Msgf("Database connection string composed")
dbConn, err := sqlx.Connect("mysql", dbConnString) dbConn, err := sqlx.Connect("mysql", dbConnString)
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Msg("Failed to connect to database") app.Log.Error().Err(err).Msg("Failed to connect to database")
return return
} }
@ -177,12 +177,12 @@ func (db *Database) Initialize() {
// Force UTC for current connection. // Force UTC for current connection.
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';") _ = dbConn.MustExec("SET @@session.time_zone='+00:00';")
ctx.Logger.Info().Msg("Database connection established") app.Log.Info().Msg("Database connection established")
db.db = dbConn db.db = dbConn
// Perform migrations. // Perform migrations.
migrations.New(ctx) migrations.New(app)
migrations.Migrate() migrations.Migrate()
} }
@ -208,7 +208,7 @@ func (db *Database) Shutdown() {
if db.db != nil { if db.db != nil {
err := db.db.Close() err := db.db.Close()
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Msg("Failed to close database connection") app.Log.Error().Err(err).Msg("Failed to close database connection")
} }
} }
} }

View File

@ -25,19 +25,19 @@
package postgresql package postgresql
import ( import (
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface" dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
) )
var ( var (
ctx *context.Context app *application.Application
dbAdapter *Database dbAdapter *Database
) )
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
//nolint:exhaustruct //nolint:exhaustruct
dbAdapter = &Database{} dbAdapter = &Database{}
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{})) app.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
} }

View File

@ -26,19 +26,19 @@ package migrations
import ( import (
"github.com/pressly/goose" "github.com/pressly/goose"
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
) )
var ctx *context.Context var app *application.Application
// New initializes migrations. // New initializes migrations.
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
} }
// Migrate launching migrations. // Migrate launching migrations.
func Migrate() { func Migrate() {
ctx.Logger.Info().Msg("Migrating database...") app.Log.Info().Msg("Migrating database...")
_ = goose.SetDialect("postgres") _ = goose.SetDialect("postgres")
goose.AddNamedMigration("1_initial.go", InitialUp, nil) goose.AddNamedMigration("1_initial.go", InitialUp, nil)
@ -47,14 +47,14 @@ func Migrate() {
goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown) goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown)
// Add new migrations BEFORE this message. // Add new migrations BEFORE this message.
dbConn := ctx.Database.GetDatabaseConnection() dbConn := app.Database.GetDatabaseConnection()
if dbConn != nil { if dbConn != nil {
err := goose.Up(dbConn, ".") err := goose.Up(dbConn, ".")
if err != nil { if err != nil {
ctx.Logger.Info().Msgf("%+v", err) app.Log.Info().Msgf("%+v", err)
ctx.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error()) app.Log.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
} }
} else { } else {
ctx.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping") app.Log.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
} }
} }

View File

@ -114,10 +114,10 @@ func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
// Pagination. // Pagination.
startPagination := 0 startPagination := 0
if page > 1 { if page > 1 {
startPagination = (page - 1) * ctx.Config.Pastes.Pagination startPagination = (page - 1) * app.Config.Pastes.Pagination
} }
err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM pastes WHERE private != true ORDER BY id DESC LIMIT $1 OFFSET $2"), ctx.Config.Pastes.Pagination, startPagination) err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM pastes WHERE private != true ORDER BY id DESC LIMIT $1 OFFSET $2"), app.Config.Pastes.Pagination, startPagination)
if err != nil { if err != nil {
//nolint:wrapcheck //nolint:wrapcheck
return nil, err return nil, err
@ -159,9 +159,9 @@ func (db *Database) GetPastesPages() int {
} }
// Calculate pages. // Calculate pages.
pages := len(pastes) / ctx.Config.Pastes.Pagination pages := len(pastes) / app.Config.Pastes.Pagination
// Check if we have any remainder. Add 1 to pages count if so. // Check if we have any remainder. Add 1 to pages count if so.
if len(pastes)%ctx.Config.Pastes.Pagination > 0 { if len(pastes)%app.Config.Pastes.Pagination > 0 {
pages++ pages++
} }
@ -170,31 +170,31 @@ func (db *Database) GetPastesPages() int {
// Initialize initializes MySQL/MariaDB connection. // Initialize initializes MySQL/MariaDB connection.
func (db *Database) Initialize() { func (db *Database) Initialize() {
ctx.Logger.Info().Msg("Initializing database connection...") app.Log.Info().Msg("Initializing database connection...")
var userpass string var userpass string
if ctx.Config.Database.Password == "" { if app.Config.Database.Password == "" {
userpass = ctx.Config.Database.Username userpass = app.Config.Database.Username
} else { } else {
userpass = ctx.Config.Database.Username + ":" + ctx.Config.Database.Password userpass = app.Config.Database.Username + ":" + app.Config.Database.Password
} }
dbConnString := fmt.Sprintf("postgres://%s@%s/%s?connect_timeout=10&fallback_application_name=fastpastebin&sslmode=disable", userpass, net.JoinHostPort(ctx.Config.Database.Address, ctx.Config.Database.Port), ctx.Config.Database.Database) dbConnString := fmt.Sprintf("postgres://%s@%s/%s?connect_timeout=10&fallback_application_name=fastpastebin&sslmode=disable", userpass, net.JoinHostPort(app.Config.Database.Address, app.Config.Database.Port), app.Config.Database.Database)
ctx.Logger.Debug().Str("DSN", dbConnString).Msg("Database connection string composed") app.Log.Debug().Str("DSN", dbConnString).Msg("Database connection string composed")
dbConn, err := sqlx.Connect("postgres", dbConnString) dbConn, err := sqlx.Connect("postgres", dbConnString)
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Msg("Failed to connect to database") app.Log.Error().Err(err).Msg("Failed to connect to database")
return return
} }
ctx.Logger.Info().Msg("Database connection established") app.Log.Info().Msg("Database connection established")
db.db = dbConn db.db = dbConn
// Perform migrations. // Perform migrations.
migrations.New(ctx) migrations.New(app)
migrations.Migrate() migrations.Migrate()
} }
@ -222,7 +222,7 @@ func (db *Database) Shutdown() {
if db.db != nil { if db.db != nil {
err := db.db.Close() err := db.db.Close()
if err != nil { if err != nil {
ctx.Logger.Error().Err(err).Msg("Failed to close database connection") app.Log.Error().Err(err).Msg("Failed to close database connection")
} }
} }
} }

View File

@ -25,20 +25,20 @@
package database package database
import ( import (
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface" databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
) )
var ( var (
ctx *context.Context app *application.Application
dbAdapter *Database dbAdapter *Database
) )
// New initializes database structure. // New initializes database structure.
func New(cc *context.Context) { func New(cc *application.Application) {
ctx = cc app = cc
//nolint:exhaustruct //nolint:exhaustruct
dbAdapter = &Database{} dbAdapter = &Database{}
ctx.RegisterDatabaseInterface(databaseinterface.Interface(Handler{})) app.Database = databaseinterface.Interface(Handler{})
} }

41
internal/helpers/path.go Normal file
View File

@ -0,0 +1,41 @@
package helpers
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
var (
// ErrHelperError indicates that error came from helper.
ErrHelperError = errors.New("helper")
// ErrHelperPathNormalizationError indicates that error came from path normalization helper.
ErrHelperPathNormalizationError = errors.New("path normalization")
)
// NormalizePath normalizes passed path:
// * Path will be absolute.
// * Symlinks will be resolved.
// * Support for tilde (~) for home path.
func NormalizePath(path string) (string, error) {
// Replace possible tilde in the beginning (and only beginning!) of data path.
if strings.HasPrefix(path, "~") {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("%s: %s: %w", ErrHelperError, ErrHelperPathNormalizationError, err)
}
path = strings.Replace(path, "~", homeDir, 1)
}
// Normalize path.
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("%s: %s: %w", ErrHelperError, ErrHelperPathNormalizationError, err)
}
return absPath, nil
}

View File

@ -31,11 +31,11 @@ import (
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.dev.pztrn.name/fastpastebin/assets" "go.dev.pztrn.name/fastpastebin/assets"
"go.dev.pztrn.name/fastpastebin/internal/context" "go.dev.pztrn.name/fastpastebin/internal/application"
) )
var ( var (
ctx *context.Context app *application.Application
log zerolog.Logger log zerolog.Logger
) )
@ -100,7 +100,7 @@ func GetTemplate(ectx echo.Context, name string, data map[string]string) string
tpl := strings.Replace(string(mainhtml), "{navigation}", string(navhtml), 1) tpl := strings.Replace(string(mainhtml), "{navigation}", string(navhtml), 1)
tpl = strings.Replace(tpl, "{footer}", string(footerhtml), 1) tpl = strings.Replace(tpl, "{footer}", string(footerhtml), 1)
// Version. // Version.
tpl = strings.Replace(tpl, "{version}", context.Version, 1) tpl = strings.Replace(tpl, "{version}", application.Version, 1)
// Get requested template. // Get requested template.
reqhtml, err3 := assets.Data.ReadFile(name) reqhtml, err3 := assets.Data.ReadFile(name)
@ -122,7 +122,7 @@ func GetTemplate(ectx echo.Context, name string, data map[string]string) string
} }
// Initialize initializes package. // Initialize initializes package.
func Initialize(cc *context.Context) { func Initialize(cc *application.Application) {
ctx = cc app = cc
log = ctx.Logger.With().Str("type", "internal").Str("package", "templater").Logger() log = app.Log.With().Str("type", "internal").Str("package", "templater").Logger()
} }