Compare commits

...

3 Commits

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

Also fixed golangci-lint configuration.

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

View File

@ -15,8 +15,14 @@ linters:
# This linter MIGHT BE good, but who decided that I want keepFor in
# 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

View File

@ -6,7 +6,7 @@ Easy-to-use-and-install pastebin software written in Go. No bells or whistles, n
**Please, use [my gitea](https://code.pztrn.name/apps/fastpastebin) for bug reporting. All other places are mirrors!**
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package context
package application
import (
"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":

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,22 +28,22 @@ import (
"github.com/dchest/captcha"
"github.com/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.

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ type Database struct {
// a subject of change in future.
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)