From 5fc6d3a181db2ea55c12c2b244de74e1eedcbd4b Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Fri, 19 Aug 2022 21:52:49 +0500 Subject: [PATCH] Application superstructure (or supersingleton, if you want). Got rid of context thing which misleads due to existance of stdlib's context package. Also fixed golangci-lint configuration. Fixes #20. --- .golangci.yml | 6 + README.md | 2 +- cmd/fastpastebin/fastpastebin.go | 46 +++---- domains/dbnotavailable/exported.go | 12 +- domains/indexpage/exported.go | 10 +- domains/indexpage/indexpage.go | 4 +- domains/pastes/exported.go | 26 ++-- domains/pastes/paste_get.go | 58 ++++---- domains/pastes/paste_post.go | 24 ++-- domains/pastes/pastes_get.go | 16 +-- internal/application/application.go | 124 +++++++++++++++++ internal/application/application_errors.go | 18 +++ .../application/application_http_server.go | 48 +++++++ .../application_logger.go} | 16 +-- internal/application/application_service.go | 13 ++ internal/application/config.go | 64 +++++++++ internal/application/config_errors.go | 15 +++ internal/application/config_structs.go | 30 +++++ internal/application/vars.go | 6 + internal/captcha/exported.go | 12 +- internal/context/context.go | 126 ------------------ internal/context/exported.go | 36 ----- internal/context/http_server.go | 46 ------- internal/database/database.go | 24 ++-- .../database/dialects/flatfiles/exported.go | 10 +- .../database/dialects/flatfiles/flatfiles.go | 64 ++++----- internal/database/dialects/mysql/exported.go | 10 +- .../dialects/mysql/migrations/exported.go | 16 +-- .../database/dialects/mysql/mysqldatabase.go | 28 ++-- .../database/dialects/postgresql/exported.go | 10 +- .../postgresql/migrations/exported.go | 18 +-- .../dialects/postgresql/postgresqldatabase.go | 28 ++-- internal/database/exported.go | 10 +- internal/helpers/path.go | 41 ++++++ internal/templater/exported.go | 12 +- 35 files changed, 589 insertions(+), 440 deletions(-) create mode 100644 internal/application/application.go create mode 100644 internal/application/application_errors.go create mode 100644 internal/application/application_http_server.go rename internal/{context/logger.go => application/application_logger.go} (80%) create mode 100644 internal/application/application_service.go create mode 100644 internal/application/config.go create mode 100644 internal/application/config_errors.go create mode 100644 internal/application/config_structs.go create mode 100644 internal/application/vars.go delete mode 100644 internal/context/context.go delete mode 100644 internal/context/exported.go delete mode 100644 internal/context/http_server.go create mode 100644 internal/helpers/path.go diff --git a/.golangci.yml b/.golangci.yml index 1b3523a..6255ca1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,8 +15,14 @@ linters: # This linter MIGHT BE good, but who decided that I want keepFor in # JSON instead of keep_for for KeepFor field? - 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. - exhaustivestruct + - golint + - wrapcheck linters-settings: lll: line-length: 420 diff --git a/README.md b/README.md index f0d25c8..def3ab5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ services: 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 diff --git a/cmd/fastpastebin/fastpastebin.go b/cmd/fastpastebin/fastpastebin.go index 59499d6..b65ccea 100644 --- a/cmd/fastpastebin/fastpastebin.go +++ b/cmd/fastpastebin/fastpastebin.go @@ -25,7 +25,6 @@ package main import ( - "flag" "os" "os/signal" "syscall" @@ -33,46 +32,39 @@ import ( "go.dev.pztrn.name/fastpastebin/domains/dbnotavailable" "go.dev.pztrn.name/fastpastebin/domains/indexpage" "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/context" "go.dev.pztrn.name/fastpastebin/internal/database" "go.dev.pztrn.name/fastpastebin/internal/templater" ) 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. signalHandler := make(chan os.Signal, 1) shutdownDone := make(chan bool, 1) 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) + + app.Start() + go func() { <-signalHandler - appCtx.Shutdown() + if err := app.Shutdown(); err != nil { + app.Log.Error().Err(err).Msg("Fast Pastebin failed to shutdown!") + } shutdownDone <- true }() diff --git a/domains/dbnotavailable/exported.go b/domains/dbnotavailable/exported.go index 673d9cc..27e1a48 100644 --- a/domains/dbnotavailable/exported.go +++ b/domains/dbnotavailable/exported.go @@ -25,16 +25,16 @@ package dbnotavailable 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 // endpoints. -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc - ctx.Echo.GET("/database_not_available", dbNotAvailableGet) - ctx.Echo.GET("/database_not_available/raw", dbNotAvailableRawGet) + app.Echo.GET("/database_not_available", dbNotAvailableGet) + app.Echo.GET("/database_not_available/raw", dbNotAvailableRawGet) } diff --git a/domains/indexpage/exported.go b/domains/indexpage/exported.go index 31a8ba6..8d9183c 100644 --- a/domains/indexpage/exported.go +++ b/domains/indexpage/exported.go @@ -25,15 +25,15 @@ package indexpage 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 // endpoints. -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc - ctx.Echo.GET("/", indexGet) + app.Echo.GET("/", indexGet) } diff --git a/domains/indexpage/indexpage.go b/domains/indexpage/indexpage.go index 0fef9ce..503be8b 100644 --- a/domains/indexpage/indexpage.go +++ b/domains/indexpage/indexpage.go @@ -37,8 +37,8 @@ import ( // Index of this site. func indexGet(ectx echo.Context) error { // We should check if database connection available. - dbConn := ctx.Database.GetDatabaseConnection() - if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { + dbConn := app.Database.GetDatabaseConnection() + if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { //nolint:wrapcheck return ectx.Redirect(http.StatusFound, "/database_not_available") } diff --git a/domains/pastes/exported.go b/domains/pastes/exported.go index 0b4ac4b..63386dd 100644 --- a/domains/pastes/exported.go +++ b/domains/pastes/exported.go @@ -27,35 +27,35 @@ package pastes import ( "regexp" - "go.dev.pztrn.name/fastpastebin/internal/context" + "go.dev.pztrn.name/fastpastebin/internal/application" ) var regexInts = regexp.MustCompile("[0-9]+") -var ctx *context.Context +var app *application.Application // New initializes pastes package and adds necessary HTTP and API // endpoints. -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc //////////////////////////////////////////////////////////// // HTTP endpoints. //////////////////////////////////////////////////////////// // New paste. - ctx.Echo.POST("/paste/", pastePOSTWebInterface) + app.Echo.POST("/paste/", pastePOSTWebInterface) // Show public paste. - ctx.Echo.GET("/paste/:id", pasteGETWebInterface) + app.Echo.GET("/paste/:id", pasteGETWebInterface) // Show RAW representation of public paste. - ctx.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface) + app.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface) // Show private paste. - ctx.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface) + app.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface) // 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. - ctx.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet) - ctx.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost) + app.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet) + app.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost) // Pastes list. - ctx.Echo.GET("/pastes/", pastesGET) - ctx.Echo.GET("/pastes/:page", pastesGET) + app.Echo.GET("/pastes/", pastesGET) + app.Echo.GET("/pastes/:page", pastesGET) } diff --git a/domains/pastes/paste_get.go b/domains/pastes/paste_get.go index d10baa1..8f29422 100644 --- a/domains/pastes/paste_get.go +++ b/domains/pastes/paste_get.go @@ -30,16 +30,16 @@ const ( // value (they both will be ignored), but private will. func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Paste, string) { // Get paste. - paste, err1 := ctx.Database.GetPaste(pasteID) + paste, err1 := app.Database.GetPaste(pasteID) 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 } // Check if paste is expired. 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 } @@ -48,7 +48,7 @@ func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Pa if paste.Private { pasteTS := paste.CreatedAt.Unix() 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 } @@ -77,7 +77,7 @@ func pasteGETWebInterface(ectx echo.Context) error { // error. pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) 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. // If passed timestamp is invalid (isn't a real UNIX timestamp) we @@ -88,7 +88,7 @@ func pasteGETWebInterface(ectx echo.Context) error { if tsProvidedStr != "" { tsProvided, err := strconv.ParseInt(tsProvidedStr, 10, 64) 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") @@ -121,7 +121,7 @@ func pasteGETWebInterface(ectx echo.Context) error { // If passed cookie value was invalid - go to paste authorization // page. 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 return ectx.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDStr+"/"+ectx.Param("timestamp")+"/verify") @@ -158,7 +158,7 @@ func pasteGETWebInterface(ectx echo.Context) error { // Tokenize paste data. lexered, err3 := lexer.Tokenise(nil, paste.Data) 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. style := styles.Get("monokai") @@ -173,7 +173,7 @@ func pasteGETWebInterface(ectx echo.Context) error { err4 := formatter.Format(buf, style, lexered) 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() @@ -194,9 +194,9 @@ func pastePasswordedVerifyGet(ectx echo.Context) error { pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) // Get paste. - paste, err1 := ctx.Database.GetPaste(pasteID) + paste, err1 := app.Database.GetPaste(pasteID) 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") @@ -208,19 +208,19 @@ func pastePasswordedVerifyGet(ectx echo.Context) error { cookie, err := ectx.Cookie("PASTE-" + strconv.Itoa(pasteID)) if err == nil { // 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. cookieValue := paste.GenerateCryptedCookieValue() 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 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. @@ -237,9 +237,9 @@ func pastePasswordedVerifyGet(ectx echo.Context) error { // POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page. func pastePasswordedVerifyPost(ectx echo.Context) error { // 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 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() // error. 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. - paste, err1 := ctx.Database.GetPaste(pasteID) + paste, err1 := app.Database.GetPaste(pasteID) 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") //nolint:wrapcheck @@ -263,7 +263,7 @@ func pastePasswordedVerifyPost(ectx echo.Context) error { params, err2 := ectx.FormParams() 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") @@ -294,8 +294,8 @@ func pastePasswordedVerifyPost(ectx echo.Context) error { // Web interface version. func pasteRawGETWebInterface(ectx echo.Context) error { // We should check if database connection available. - dbConn := ctx.Database.GetDatabaseConnection() - if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { + dbConn := app.Database.GetDatabaseConnection() + if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { //nolint:wrapcheck 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() // error. 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. - paste, err1 := ctx.Database.GetPaste(pasteID) + paste, err1 := app.Database.GetPaste(pasteID) 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 return ectx.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.") } 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 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) 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 return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found") @@ -336,7 +336,7 @@ func pasteRawGETWebInterface(ectx echo.Context) error { pasteTS := paste.CreatedAt.Unix() 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 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. // Return error for now. 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") } diff --git a/domains/pastes/paste_post.go b/domains/pastes/paste_post.go index 106a080..c864c17 100644 --- a/domains/pastes/paste_post.go +++ b/domains/pastes/paste_post.go @@ -22,15 +22,15 @@ const KeepPastesForever = "forever" // requests comes from browsers via web interface. func pastePOSTWebInterface(ectx echo.Context) error { // We should check if database connection available. - dbConn := ctx.Database.GetDatabaseConnection() - if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { + dbConn := app.Database.GetDatabaseConnection() + if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { //nolint:wrapcheck return ectx.Redirect(http.StatusFound, "/database_not_available") } params, err := ectx.FormParams() 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") @@ -38,11 +38,11 @@ func pastePOSTWebInterface(ectx echo.Context) error { 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. 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.") @@ -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 { - 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 ;).") @@ -61,7 +61,7 @@ func pastePOSTWebInterface(ectx echo.Context) error { // Verify captcha. 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.") @@ -95,11 +95,11 @@ func pastePOSTWebInterface(ectx echo.Context) error { keepFor, err = strconv.Atoi(keepForRaw) if err != nil { if params["paste-keep-for"][0] == KeepPastesForever { - ctx.Logger.Debug().Msg("Keeping paste forever!") + app.Log.Debug().Msg("Keeping paste forever!") keepFor = 0 } 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 ;).") @@ -138,9 +138,9 @@ func pastePOSTWebInterface(ectx echo.Context) error { _ = paste.CreatePassword(pastePassword[0]) } - pasteID, err2 := ctx.Database.SavePaste(paste) + pasteID, err2 := app.Database.SavePaste(paste) 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.") @@ -149,7 +149,7 @@ func pastePOSTWebInterface(ectx echo.Context) error { } 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. if paste.Private { diff --git a/domains/pastes/pastes_get.go b/domains/pastes/pastes_get.go index 6fd575c..e0398a2 100644 --- a/domains/pastes/pastes_get.go +++ b/domains/pastes/pastes_get.go @@ -39,8 +39,8 @@ import ( // Web interface version. func pastesGET(ectx echo.Context) error { // We should check if database connection available. - dbConn := ctx.Database.GetDatabaseConnection() - if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { + dbConn := app.Database.GetDatabaseConnection() + if app.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil { //nolint:wrapcheck return ectx.Redirect(http.StatusFound, "/database_not_available") } @@ -54,17 +54,17 @@ func pastesGET(ectx echo.Context) error { 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. - pastes, err3 := ctx.Database.GetPagedPastes(page) - ctx.Logger.Debug().Int("count", len(pastes)).Msg("Got pastes") + pastes, err3 := app.Database.GetPagedPastes(page) + app.Log.Debug().Int("count", len(pastes)).Msg("Got pastes") pastesString := "No pastes to show." // Show "No pastes to show" on any error for now. 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.") @@ -100,8 +100,8 @@ func pastesGET(ectx echo.Context) error { } // Pagination. - pages := ctx.Database.GetPastesPages() - ctx.Logger.Debug().Int("total pages", pages).Int("current page", page).Msg("Paging data") + pages := app.Database.GetPastesPages() + app.Log.Debug().Int("total pages", pages).Int("current page", page).Msg("Paging data") paginationHTML := pagination.CreateHTML(page, pages, "/pastes/") pasteListTpl := templater.GetTemplate(ectx, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML}) diff --git a/internal/application/application.go b/internal/application/application.go new file mode 100644 index 0000000..70a6331 --- /dev/null +++ b/internal/application/application.go @@ -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 +} diff --git a/internal/application/application_errors.go b/internal/application/application_errors.go new file mode 100644 index 0000000..ff4d371 --- /dev/null +++ b/internal/application/application_errors.go @@ -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") +) diff --git a/internal/application/application_http_server.go b/internal/application/application_http_server.go new file mode 100644 index 0000000..d33aa4a --- /dev/null +++ b/internal/application/application_http_server.go @@ -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") +} diff --git a/internal/context/logger.go b/internal/application/application_logger.go similarity index 80% rename from internal/context/logger.go rename to internal/application/application_logger.go index e0266e6..fe17b72 100644 --- a/internal/context/logger.go +++ b/internal/application/application_logger.go @@ -1,4 +1,4 @@ -package context +package application import ( "fmt" @@ -11,7 +11,7 @@ import ( ) // 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 runtime.ReadMemStats(&memstats) @@ -22,7 +22,7 @@ func (c *Context) getMemoryUsage(event *zerolog.Event, level zerolog.Level, mess } // Initializes logger. -func (c *Context) initializeLogger() { +func (a *Application) initializeLogger() { // Устанавливаем форматирование логгера. //nolint:exhaustruct output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false, TimeFormat: time.RFC3339} @@ -52,17 +52,17 @@ func (c *Context) initializeLogger() { 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. -func (c *Context) initializeLoggerPost() { +func (a *Application) initializeLoggerPost() { // 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": zerolog.SetGlobalLevel(zerolog.DebugLevel) case "INFO": diff --git a/internal/application/application_service.go b/internal/application/application_service.go new file mode 100644 index 0000000..09a599a --- /dev/null +++ b/internal/application/application_service.go @@ -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 +} diff --git a/internal/application/config.go b/internal/application/config.go new file mode 100644 index 0000000..5ebb36d --- /dev/null +++ b/internal/application/config.go @@ -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 +} diff --git a/internal/application/config_errors.go b/internal/application/config_errors.go new file mode 100644 index 0000000..d894353 --- /dev/null +++ b/internal/application/config_errors.go @@ -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") +) diff --git a/internal/application/config_structs.go b/internal/application/config_structs.go new file mode 100644 index 0000000..8b07675 --- /dev/null +++ b/internal/application/config_structs.go @@ -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"` +} diff --git a/internal/application/vars.go b/internal/application/vars.go new file mode 100644 index 0000000..8e0147b --- /dev/null +++ b/internal/application/vars.go @@ -0,0 +1,6 @@ +package application + +const ( + // Version . + Version = "0.4.1" +) diff --git a/internal/captcha/exported.go b/internal/captcha/exported.go index 4dc7bff..1bbf329 100644 --- a/internal/captcha/exported.go +++ b/internal/captcha/exported.go @@ -28,22 +28,22 @@ import ( "github.com/dchest/captcha" "github.com/labstack/echo" "github.com/rs/zerolog" - "go.dev.pztrn.name/fastpastebin/internal/context" + "go.dev.pztrn.name/fastpastebin/internal/application" ) var ( - ctx *context.Context + app *application.Application log zerolog.Logger ) // New initializes captcha package and adds necessary HTTP and API // endpoints. -func New(cc *context.Context) { - ctx = cc - log = ctx.Logger.With().Str("type", "internal").Str("package", "captcha").Logger() +func New(cc *application.Application) { + app = cc + log = app.Log.With().Str("type", "internal").Str("package", "captcha").Logger() // 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. diff --git a/internal/context/context.go b/internal/context/context.go deleted file mode 100644 index 44ccbee..0000000 --- a/internal/context/context.go +++ /dev/null @@ -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() -} diff --git a/internal/context/exported.go b/internal/context/exported.go deleted file mode 100644 index 60c3fba..0000000 --- a/internal/context/exported.go +++ /dev/null @@ -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{} -} diff --git a/internal/context/http_server.go b/internal/context/http_server.go deleted file mode 100644 index 42113ad..0000000 --- a/internal/context/http_server.go +++ /dev/null @@ -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) - } - } -} diff --git a/internal/database/database.go b/internal/database/database.go index 2ef484b..d39473f 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -46,7 +46,7 @@ type Database struct { // a subject of change in future. func (db *Database) cleanup() { for { - ctx.Logger.Info().Msg("Starting pastes cleanup procedure...") + app.Log.Info().Msg("Starting pastes cleanup procedure...") pages := db.db.GetPastesPages() @@ -55,7 +55,7 @@ func (db *Database) cleanup() { for i := 0; i < pages; i++ { pastes, err := db.db.GetPagedPastes(i) 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 { @@ -68,11 +68,11 @@ func (db *Database) cleanup() { for _, pasteID := range pasteIDsToRemove { err := db.DeletePaste(pasteID) 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) } @@ -107,16 +107,16 @@ func (db *Database) GetPastesPages() int { // Initialize initializes connection to database. func (db *Database) Initialize() { - ctx.Logger.Info().Msg("Initializing database connection...") + app.Log.Info().Msg("Initializing database connection...") - if ctx.Config.Database.Type == "mysql" { - mysql.New(ctx) - } else if ctx.Config.Database.Type == flatfiles.FlatFileDialect { - flatfiles.New(ctx) - } else if ctx.Config.Database.Type == "postgresql" { - postgresql.New(ctx) + if app.Config.Database.Type == "mysql" { + mysql.New(app) + } else if app.Config.Database.Type == flatfiles.FlatFileDialect { + flatfiles.New(app) + } else if app.Config.Database.Type == "postgresql" { + postgresql.New(app) } 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() diff --git a/internal/database/dialects/flatfiles/exported.go b/internal/database/dialects/flatfiles/exported.go index c4fa9d8..8788f8b 100644 --- a/internal/database/dialects/flatfiles/exported.go +++ b/internal/database/dialects/flatfiles/exported.go @@ -25,21 +25,21 @@ package flatfiles 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" ) const FlatFileDialect = "flatfiles" var ( - ctx *context.Context + app *application.Application flf *FlatFiles ) -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc //nolint:exhaustruct flf = &FlatFiles{} - ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{})) + app.Database.RegisterDialect(dialectinterface.Interface(Handler{})) } diff --git a/internal/database/dialects/flatfiles/flatfiles.go b/internal/database/dialects/flatfiles/flatfiles.go index 93ab356..cd5a352 100644 --- a/internal/database/dialects/flatfiles/flatfiles.go +++ b/internal/database/dialects/flatfiles/flatfiles.go @@ -48,7 +48,7 @@ func (ff *FlatFiles) DeletePaste(pasteID int) error { // Delete from disk. err := os.Remove(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json")) 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 return err @@ -82,17 +82,17 @@ func (ff *FlatFiles) GetDatabaseConnection() *sql.DB { func (ff *FlatFiles) GetPaste(pasteID int) (*structs.Paste, error) { ff.writeMutex.Lock() 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) 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 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() //nolint:exhaustruct @@ -100,7 +100,7 @@ func (ff *FlatFiles) GetPaste(pasteID int) (*structs.Paste, error) { err1 := json.Unmarshal(pasteInBytes, paste) 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 return nil, err1 @@ -113,7 +113,7 @@ func (ff *FlatFiles) GetPagedPastes(page int) ([]structs.Paste, error) { // Pagination. startPagination := 0 if page > 1 { - startPagination = (page - 1) * ctx.Config.Pastes.Pagination + startPagination = (page - 1) * app.Config.Pastes.Pagination } // Iteration one - get only public pastes. @@ -129,23 +129,23 @@ func (ff *FlatFiles) GetPagedPastes(page int) ([]structs.Paste, error) { pastesData := make([]structs.Paste, 0) for idx, paste := range publicPastes { - if len(pastesData) == ctx.Config.Pastes.Pagination { + if len(pastesData) == app.Config.Pastes.Pagination { break } 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 } - 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)) { - ctx.Logger.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too high index") + 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)) { + app.Log.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too high index") 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. //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")) 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 } err1 := json.Unmarshal(pasteRawData, pasteData) 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 } @@ -184,9 +184,9 @@ func (ff *FlatFiles) GetPastesPages() int { ff.writeMutex.Unlock() // 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. - if len(publicPastes)%ctx.Config.Pastes.Pagination > 0 { + if len(publicPastes)%app.Config.Pastes.Pagination > 0 { pages++ } @@ -194,14 +194,14 @@ func (ff *FlatFiles) GetPastesPages() int { } 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. - if strings.Contains(ctx.Config.Database.Path, "~") { + if strings.Contains(app.Config.Database.Path, "~") { curUser, err := user.Current() 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) } @@ -212,40 +212,40 @@ func (ff *FlatFiles) Initialize() { path, _ = filepath.Abs(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. 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) } 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. 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) } 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. ff.pastesIndex = []Index{} 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 { indexData, err := os.ReadFile(filepath.Join(ff.path, "pastes", "index.json")) 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) 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 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) if err != nil { @@ -286,18 +286,18 @@ func (ff *FlatFiles) SavePaste(paste *structs.Paste) (int64, error) { } func (ff *FlatFiles) Shutdown() { - ctx.Logger.Info().Msg("Saving indexes...") + app.Log.Info().Msg("Saving indexes...") indexData, err := json.Marshal(ff.pastesIndex) 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 } err1 := os.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0o600) 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 } diff --git a/internal/database/dialects/mysql/exported.go b/internal/database/dialects/mysql/exported.go index b00209d..0bad7e8 100644 --- a/internal/database/dialects/mysql/exported.go +++ b/internal/database/dialects/mysql/exported.go @@ -25,19 +25,19 @@ package mysql 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" ) var ( - ctx *context.Context + app *application.Application dbAdapter *Database ) -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc //nolint:exhaustruct dbAdapter = &Database{} - ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{})) + app.Database.RegisterDialect(dialectinterface.Interface(Handler{})) } diff --git a/internal/database/dialects/mysql/migrations/exported.go b/internal/database/dialects/mysql/migrations/exported.go index 7994392..2b22bf2 100644 --- a/internal/database/dialects/mysql/migrations/exported.go +++ b/internal/database/dialects/mysql/migrations/exported.go @@ -26,19 +26,19 @@ package migrations import ( "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. -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc } // Migrate launching migrations. func Migrate() { - ctx.Logger.Info().Msg("Migrating database...") + app.Log.Info().Msg("Migrating database...") _ = goose.SetDialect("mysql") goose.AddNamedMigration("1_initial.go", InitialUp, nil) @@ -47,13 +47,13 @@ func Migrate() { goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown) // Add new migrations BEFORE this message. - dbConn := ctx.Database.GetDatabaseConnection() + dbConn := app.Database.GetDatabaseConnection() if dbConn != nil { err := goose.Up(dbConn, ".") 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 { - ctx.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping") + app.Log.Warn().Msg("Current database dialect isn't supporting migrations, skipping") } } diff --git a/internal/database/dialects/mysql/mysqldatabase.go b/internal/database/dialects/mysql/mysqldatabase.go index 1be432f..4193549 100644 --- a/internal/database/dialects/mysql/mysqldatabase.go +++ b/internal/database/dialects/mysql/mysqldatabase.go @@ -103,10 +103,10 @@ func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) { // Pagination. startPagination := 0 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 { //nolint:wrapcheck return nil, err @@ -142,9 +142,9 @@ func (db *Database) GetPastesPages() int { } // 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. - if len(pastes)%ctx.Config.Pastes.Pagination > 0 { + if len(pastes)%app.Config.Pastes.Pagination > 0 { pages++ } @@ -153,23 +153,23 @@ func (db *Database) GetPastesPages() int { // Initialize initializes MySQL/MariaDB connection. 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 // in DSN wants "user" or "user:password", "user:" is invalid. var userpass string - if ctx.Config.Database.Password == "" { - userpass = ctx.Config.Database.Username + if app.Config.Database.Password == "" { + userpass = app.Config.Database.Username } 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) - ctx.Logger.Debug().Str("DSN", dbConnString).Msgf("Database connection string composed") + 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) + app.Log.Debug().Str("DSN", dbConnString).Msgf("Database connection string composed") dbConn, err := sqlx.Connect("mysql", dbConnString) 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 } @@ -177,12 +177,12 @@ func (db *Database) Initialize() { // Force UTC for current connection. _ = 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 // Perform migrations. - migrations.New(ctx) + migrations.New(app) migrations.Migrate() } @@ -208,7 +208,7 @@ func (db *Database) Shutdown() { if db.db != nil { err := db.db.Close() 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") } } } diff --git a/internal/database/dialects/postgresql/exported.go b/internal/database/dialects/postgresql/exported.go index f84d089..4cebd5e 100644 --- a/internal/database/dialects/postgresql/exported.go +++ b/internal/database/dialects/postgresql/exported.go @@ -25,19 +25,19 @@ package postgresql 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" ) var ( - ctx *context.Context + app *application.Application dbAdapter *Database ) -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc //nolint:exhaustruct dbAdapter = &Database{} - ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{})) + app.Database.RegisterDialect(dialectinterface.Interface(Handler{})) } diff --git a/internal/database/dialects/postgresql/migrations/exported.go b/internal/database/dialects/postgresql/migrations/exported.go index a5d8e34..1bd2587 100644 --- a/internal/database/dialects/postgresql/migrations/exported.go +++ b/internal/database/dialects/postgresql/migrations/exported.go @@ -26,19 +26,19 @@ package migrations import ( "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. -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc } // Migrate launching migrations. func Migrate() { - ctx.Logger.Info().Msg("Migrating database...") + app.Log.Info().Msg("Migrating database...") _ = goose.SetDialect("postgres") goose.AddNamedMigration("1_initial.go", InitialUp, nil) @@ -47,14 +47,14 @@ func Migrate() { goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown) // Add new migrations BEFORE this message. - dbConn := ctx.Database.GetDatabaseConnection() + dbConn := app.Database.GetDatabaseConnection() if dbConn != nil { err := goose.Up(dbConn, ".") if err != nil { - ctx.Logger.Info().Msgf("%+v", err) - ctx.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error()) + app.Log.Info().Msgf("%+v", err) + app.Log.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error()) } } 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") } } diff --git a/internal/database/dialects/postgresql/postgresqldatabase.go b/internal/database/dialects/postgresql/postgresqldatabase.go index 8124f5b..28fda73 100644 --- a/internal/database/dialects/postgresql/postgresqldatabase.go +++ b/internal/database/dialects/postgresql/postgresqldatabase.go @@ -114,10 +114,10 @@ func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) { // Pagination. startPagination := 0 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 { //nolint:wrapcheck return nil, err @@ -159,9 +159,9 @@ func (db *Database) GetPastesPages() int { } // 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. - if len(pastes)%ctx.Config.Pastes.Pagination > 0 { + if len(pastes)%app.Config.Pastes.Pagination > 0 { pages++ } @@ -170,31 +170,31 @@ func (db *Database) GetPastesPages() int { // Initialize initializes MySQL/MariaDB connection. func (db *Database) Initialize() { - ctx.Logger.Info().Msg("Initializing database connection...") + app.Log.Info().Msg("Initializing database connection...") var userpass string - if ctx.Config.Database.Password == "" { - userpass = ctx.Config.Database.Username + if app.Config.Database.Password == "" { + userpass = app.Config.Database.Username } 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) - ctx.Logger.Debug().Str("DSN", dbConnString).Msg("Database connection string composed") + 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) + app.Log.Debug().Str("DSN", dbConnString).Msg("Database connection string composed") dbConn, err := sqlx.Connect("postgres", dbConnString) 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 } - ctx.Logger.Info().Msg("Database connection established") + app.Log.Info().Msg("Database connection established") db.db = dbConn // Perform migrations. - migrations.New(ctx) + migrations.New(app) migrations.Migrate() } @@ -222,7 +222,7 @@ func (db *Database) Shutdown() { if db.db != nil { err := db.db.Close() 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") } } } diff --git a/internal/database/exported.go b/internal/database/exported.go index 3ed3192..93bd6e9 100644 --- a/internal/database/exported.go +++ b/internal/database/exported.go @@ -25,20 +25,20 @@ package database import ( - "go.dev.pztrn.name/fastpastebin/internal/context" + "go.dev.pztrn.name/fastpastebin/internal/application" databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface" ) var ( - ctx *context.Context + app *application.Application dbAdapter *Database ) // New initializes database structure. -func New(cc *context.Context) { - ctx = cc +func New(cc *application.Application) { + app = cc //nolint:exhaustruct dbAdapter = &Database{} - ctx.RegisterDatabaseInterface(databaseinterface.Interface(Handler{})) + app.Database = databaseinterface.Interface(Handler{}) } diff --git a/internal/helpers/path.go b/internal/helpers/path.go new file mode 100644 index 0000000..e4c7156 --- /dev/null +++ b/internal/helpers/path.go @@ -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 +} diff --git a/internal/templater/exported.go b/internal/templater/exported.go index be75ca1..19d5b37 100644 --- a/internal/templater/exported.go +++ b/internal/templater/exported.go @@ -31,11 +31,11 @@ import ( "github.com/labstack/echo" "github.com/rs/zerolog" "go.dev.pztrn.name/fastpastebin/assets" - "go.dev.pztrn.name/fastpastebin/internal/context" + "go.dev.pztrn.name/fastpastebin/internal/application" ) var ( - ctx *context.Context + app *application.Application 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(tpl, "{footer}", string(footerhtml), 1) // Version. - tpl = strings.Replace(tpl, "{version}", context.Version, 1) + tpl = strings.Replace(tpl, "{version}", application.Version, 1) // Get requested template. 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. -func Initialize(cc *context.Context) { - ctx = cc - log = ctx.Logger.With().Str("type", "internal").Str("package", "templater").Logger() +func Initialize(cc *application.Application) { + app = cc + log = app.Log.With().Str("type", "internal").Str("package", "templater").Logger() }