Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
aeec85086e | |||
74035622f5 | |||
5fc6d3a181 |
@ -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
|
||||
|
@ -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!**
|
||||
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
|
@ -25,7 +25,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@ -33,46 +32,43 @@ 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)
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
app.Log.Fatal().Err(err).Msg("Failed to start Fast Pastebin!")
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-signalHandler
|
||||
appCtx.Shutdown()
|
||||
|
||||
if err := app.Shutdown(); err != nil {
|
||||
app.Log.Error().Err(err).Msg("Fast Pastebin failed to shutdown!")
|
||||
}
|
||||
|
||||
shutdownDone <- true
|
||||
}()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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})
|
||||
|
124
internal/application/application.go
Normal file
124
internal/application/application.go
Normal 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
|
||||
}
|
18
internal/application/application_errors.go
Normal file
18
internal/application/application_errors.go
Normal 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")
|
||||
)
|
48
internal/application/application_http_server.go
Normal file
48
internal/application/application_http_server.go
Normal 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")
|
||||
}
|
@ -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":
|
13
internal/application/application_service.go
Normal file
13
internal/application/application_service.go
Normal 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
|
||||
}
|
64
internal/application/config.go
Normal file
64
internal/application/config.go
Normal 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
|
||||
}
|
15
internal/application/config_errors.go
Normal file
15
internal/application/config_errors.go
Normal 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")
|
||||
)
|
30
internal/application/config_structs.go
Normal file
30
internal/application/config_structs.go
Normal 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"`
|
||||
}
|
6
internal/application/vars.go
Normal file
6
internal/application/vars.go
Normal file
@ -0,0 +1,6 @@
|
||||
package application
|
||||
|
||||
const (
|
||||
// Version .
|
||||
Version = "0.4.1"
|
||||
)
|
@ -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.
|
||||
|
@ -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()
|
||||
}
|
@ -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{}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||