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