Application superstructure (or supersingleton, if you want).
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is failing
				
			
		
		
	
	
				
					
				
			
		
			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.
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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,39 @@ import ( | ||||
| 	"go.dev.pztrn.name/fastpastebin/domains/dbnotavailable" | ||||
| 	"go.dev.pztrn.name/fastpastebin/domains/indexpage" | ||||
| 	"go.dev.pztrn.name/fastpastebin/domains/pastes" | ||||
| 	"go.dev.pztrn.name/fastpastebin/internal/application" | ||||
| 	"go.dev.pztrn.name/fastpastebin/internal/captcha" | ||||
| 	"go.dev.pztrn.name/fastpastebin/internal/context" | ||||
| 	"go.dev.pztrn.name/fastpastebin/internal/database" | ||||
| 	"go.dev.pztrn.name/fastpastebin/internal/templater" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	appCtx := context.New() | ||||
| 	appCtx.Initialize() | ||||
|  | ||||
| 	appCtx.Logger.Info().Msg("Starting Fast Pastebin...") | ||||
|  | ||||
| 	// Here goes initial initialization for packages that want CLI flags | ||||
| 	// to be added. | ||||
|  | ||||
| 	// Parse flags. | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	// Continue loading. | ||||
| 	appCtx.LoadConfiguration() | ||||
| 	appCtx.InitializePost() | ||||
| 	database.New(appCtx) | ||||
| 	appCtx.Database.Initialize() | ||||
| 	templater.Initialize(appCtx) | ||||
|  | ||||
| 	captcha.New(appCtx) | ||||
|  | ||||
| 	dbnotavailable.New(appCtx) | ||||
| 	indexpage.New(appCtx) | ||||
| 	pastes.New(appCtx) | ||||
|  | ||||
| 	// CTRL+C handler. | ||||
| 	signalHandler := make(chan os.Signal, 1) | ||||
| 	shutdownDone := make(chan bool, 1) | ||||
|  | ||||
| 	signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM) | ||||
|  | ||||
| 	app := application.New() | ||||
| 	app.Log.Info().Msg("Starting Fast Pastebin...") | ||||
|  | ||||
| 	database.New(app) | ||||
| 	app.Database.Initialize() | ||||
| 	templater.Initialize(app) | ||||
|  | ||||
| 	captcha.New(app) | ||||
|  | ||||
| 	dbnotavailable.New(app) | ||||
| 	indexpage.New(app) | ||||
| 	pastes.New(app) | ||||
|  | ||||
| 	app.Start() | ||||
|  | ||||
| 	go func() { | ||||
| 		<-signalHandler | ||||
| 		appCtx.Shutdown() | ||||
| 		if err := app.Shutdown(); err != nil { | ||||
| 			app.Log.Error().Err(err).Msg("Fast Pastebin failed to shutdown!") | ||||
| 		} | ||||
| 		shutdownDone <- true | ||||
| 	}() | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user