Database dialects, proper database shutdown, pagination in configuration.
Added possibility to use different database (storage) backends. Currently supported: flatfiles and mysql. Fixes #2. Added database shutdown call. This call will properly shutdown database connections (in case of RDBMS) or flush pastes index cache on disk (in case of flatfiles). De-hardcoded pagination count. Fixes #12.
This commit is contained in:
		| @@ -11,6 +11,7 @@ whistles, no websockets and even NO JAVASCRIPT! | ||||
| * Syntax highlighting. | ||||
| * Pastes expiration. | ||||
| * Passwords for pastes. | ||||
| * Multiple storage backends. Currently: ``flatfiles`` and ``mysql``. | ||||
|  | ||||
| # Caveats. | ||||
|  | ||||
|   | ||||
| @@ -26,6 +26,8 @@ package config | ||||
|  | ||||
| // 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"` | ||||
|   | ||||
							
								
								
									
										30
									
								
								config/pastes.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								config/pastes.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| // 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 config | ||||
|  | ||||
| // ConfigPastes describes pastes subsystem configuration. | ||||
| type ConfigPastes struct { | ||||
| 	Pagination int `yaml:"pagination"` | ||||
| } | ||||
| @@ -29,4 +29,5 @@ type ConfigStruct struct { | ||||
| 	Database ConfigDatabase `yaml:"database"` | ||||
| 	Logging  ConfigLogging  `yaml:"logging"` | ||||
| 	HTTP     ConfigHTTP     `yaml:"http"` | ||||
| 	Pastes   ConfigPastes   `yaml:"pastes"` | ||||
| } | ||||
|   | ||||
| @@ -146,4 +146,6 @@ func (c *Context) RegisterEcho(e *echo.Echo) { | ||||
| // Shutdown shutdowns entire application. | ||||
| func (c *Context) Shutdown() { | ||||
| 	c.Logger.Info().Msg("Shutting down Fast Pastebin...") | ||||
|  | ||||
| 	c.Database.Shutdown() | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ package context | ||||
|  | ||||
| const ( | ||||
| 	// Version . | ||||
| 	Version = "0.1.1" | ||||
| 	Version = "0.1.2-dev" | ||||
| ) | ||||
|  | ||||
| // New creates new context. | ||||
|   | ||||
| @@ -26,47 +26,65 @@ package database | ||||
|  | ||||
| import ( | ||||
| 	// stdlib | ||||
| 	"fmt" | ||||
| 	"database/sql" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/database/dialects/flatfiles" | ||||
| 	"github.com/pztrn/fastpastebin/database/dialects/interface" | ||||
| 	"github.com/pztrn/fastpastebin/database/dialects/mysql" | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
|  | ||||
| 	// other | ||||
| 	_ "github.com/go-sql-driver/mysql" | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| ) | ||||
|  | ||||
| // Database represents control structure for database connection. | ||||
| type Database struct { | ||||
| 	db *sqlx.DB | ||||
| 	db dialectinterface.Interface | ||||
| } | ||||
|  | ||||
| // GetDatabaseConnection returns current database connection. | ||||
| func (db *Database) GetDatabaseConnection() *sqlx.DB { | ||||
| 	return db.db | ||||
| func (db *Database) GetDatabaseConnection() *sql.DB { | ||||
| 	if db.db != nil { | ||||
| 		return db.db.GetDatabaseConnection() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) { | ||||
| 	return db.db.GetPaste(pasteID) | ||||
| } | ||||
|  | ||||
| func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) { | ||||
| 	return db.db.GetPagedPastes(page) | ||||
| } | ||||
|  | ||||
| func (db *Database) GetPastesPages() int { | ||||
| 	return db.db.GetPastesPages() | ||||
| } | ||||
|  | ||||
| // Initialize initializes connection to database. | ||||
| func (db *Database) Initialize() { | ||||
| 	c.Logger.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 = "" | ||||
| 	if c.Config.Database.Password == "" { | ||||
| 		userpass = c.Config.Database.Username | ||||
| 	if c.Config.Database.Type == "mysql" { | ||||
| 		mysql.New(c) | ||||
| 	} else if c.Config.Database.Type == "flatfiles" { | ||||
| 		flatfiles.New(c) | ||||
| 	} else { | ||||
| 		userpass = c.Config.Database.Username + ":" + c.Config.Database.Password | ||||
| 		c.Logger.Fatal().Msgf("Unknown database type: %s", c.Config.Database.Type) | ||||
| 	} | ||||
|  | ||||
| 	dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, c.Config.Database.Address, c.Config.Database.Port, c.Config.Database.Database) | ||||
| 	c.Logger.Debug().Msgf("Database connection string: %s", dbConnString) | ||||
|  | ||||
| 	dbConn, err := sqlx.Connect("mysql", dbConnString) | ||||
| 	if err != nil { | ||||
| 		c.Logger.Panic().Msgf("Failed to connect to database: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// Force UTC for current connection. | ||||
| 	_ = dbConn.MustExec("SET @@session.time_zone='+00:00';") | ||||
|  | ||||
| 	c.Logger.Info().Msg("Database connection established") | ||||
| 	db.db = dbConn | ||||
| } | ||||
|  | ||||
| func (db *Database) RegisterDialect(di dialectinterface.Interface) { | ||||
| 	db.db = di | ||||
| 	db.db.Initialize() | ||||
| } | ||||
|  | ||||
| func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) { | ||||
| 	return db.db.SavePaste(p) | ||||
| } | ||||
|  | ||||
| func (db *Database) Shutdown() { | ||||
| 	db.db.Shutdown() | ||||
| } | ||||
|   | ||||
							
								
								
									
										42
									
								
								database/dialects/flatfiles/exported.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								database/dialects/flatfiles/exported.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| // 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 flatfiles | ||||
|  | ||||
| import ( | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/context" | ||||
| 	"github.com/pztrn/fastpastebin/database/dialects/interface" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	c *context.Context | ||||
| 	f *FlatFiles | ||||
| ) | ||||
|  | ||||
| func New(cc *context.Context) { | ||||
| 	c = cc | ||||
| 	f = &FlatFiles{} | ||||
| 	c.Database.RegisterDialect(dialectinterface.Interface(Handler{})) | ||||
| } | ||||
							
								
								
									
										246
									
								
								database/dialects/flatfiles/flatfiles.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								database/dialects/flatfiles/flatfiles.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,246 @@ | ||||
| // 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 flatfiles | ||||
|  | ||||
| import ( | ||||
| 	// stdlib | ||||
| 	"database/sql" | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/user" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
| ) | ||||
|  | ||||
| type FlatFiles struct { | ||||
| 	pastesIndex []*Index | ||||
| 	path        string | ||||
| 	writeMutex  sync.Mutex | ||||
| } | ||||
|  | ||||
| func (ff *FlatFiles) GetDatabaseConnection() *sql.DB { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (ff *FlatFiles) GetPaste(pasteID int) (*pastesmodel.Paste, error) { | ||||
| 	ff.writeMutex.Lock() | ||||
| 	pastePath := filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json") | ||||
| 	c.Logger.Debug().Msgf("Trying to load paste data from '%s'...", pastePath) | ||||
| 	pasteInBytes, err := ioutil.ReadFile(pastePath) | ||||
| 	if err != nil { | ||||
| 		c.Logger.Debug().Msgf("Failed to read paste from storage: %s", err.Error()) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	c.Logger.Debug().Msgf("Loaded %d bytes: %s", len(pasteInBytes), string(pasteInBytes)) | ||||
| 	ff.writeMutex.Unlock() | ||||
|  | ||||
| 	paste := &pastesmodel.Paste{} | ||||
| 	err = json.Unmarshal(pasteInBytes, paste) | ||||
| 	if err != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to parse paste: %s", err.Error()) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return paste, nil | ||||
| } | ||||
|  | ||||
| func (ff *FlatFiles) GetPagedPastes(page int) ([]pastesmodel.Paste, error) { | ||||
| 	// Pagination. | ||||
| 	var startPagination = 0 | ||||
| 	if page > 1 { | ||||
| 		startPagination = (page - 1) * c.Config.Pastes.Pagination | ||||
| 	} | ||||
|  | ||||
| 	c.Logger.Debug().Msgf("Pastes index: %+v", ff.pastesIndex) | ||||
|  | ||||
| 	// Iteration one - get only public pastes. | ||||
| 	var publicPastes []*Index | ||||
| 	for _, paste := range ff.pastesIndex { | ||||
| 		if !paste.Private { | ||||
| 			publicPastes = append(publicPastes, paste) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	c.Logger.Debug().Msgf("%+v", publicPastes) | ||||
|  | ||||
| 	// Iteration two - get paginated pastes. | ||||
| 	var pastesData []pastesmodel.Paste | ||||
| 	for idx, paste := range publicPastes { | ||||
| 		if len(pastesData) == c.Config.Pastes.Pagination { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if idx < startPagination { | ||||
| 			c.Logger.Debug().Msgf("Paste with index %d isn't in pagination query: too low index", idx) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if (idx-1 >= startPagination && page > 1 && idx > startPagination+((page-1)*c.Config.Pastes.Pagination)) || (idx-1 >= startPagination && page == 1 && idx > startPagination+(page*c.Config.Pastes.Pagination)) { | ||||
| 			c.Logger.Debug().Msgf("Paste with index %d isn't in pagination query: too high index", idx) | ||||
| 			break | ||||
| 		} | ||||
| 		c.Logger.Debug().Msgf("Getting paste data (ID: %d, index: %d)", paste.ID, idx) | ||||
|  | ||||
| 		// Get paste data. | ||||
| 		pasteData := &pastesmodel.Paste{} | ||||
| 		pasteRawData, err := ioutil.ReadFile(filepath.Join(ff.path, "pastes", strconv.Itoa(paste.ID)+".json")) | ||||
| 		if err != nil { | ||||
| 			c.Logger.Error().Msgf("Failed to read paste data: %s", err.Error()) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		err = json.Unmarshal(pasteRawData, pasteData) | ||||
| 		if err != nil { | ||||
| 			c.Logger.Error().Msgf("Failed to parse paste data: %s", err.Error()) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		pastesData = append(pastesData, (*pasteData)) | ||||
| 	} | ||||
|  | ||||
| 	return pastesData, nil | ||||
| } | ||||
|  | ||||
| func (ff *FlatFiles) GetPastesPages() int { | ||||
| 	// Get public pastes count. | ||||
| 	var publicPastes []*Index | ||||
|  | ||||
| 	ff.writeMutex.Lock() | ||||
| 	for _, paste := range ff.pastesIndex { | ||||
| 		if !paste.Private { | ||||
| 			publicPastes = append(publicPastes, paste) | ||||
| 		} | ||||
| 	} | ||||
| 	ff.writeMutex.Unlock() | ||||
|  | ||||
| 	// Calculate pages. | ||||
| 	pages := len(publicPastes) / c.Config.Pastes.Pagination | ||||
| 	// Check if we have any remainder. Add 1 to pages count if so. | ||||
| 	if len(publicPastes)%c.Config.Pastes.Pagination > 0 { | ||||
| 		pages++ | ||||
| 	} | ||||
|  | ||||
| 	return pages | ||||
| } | ||||
|  | ||||
| func (ff *FlatFiles) Initialize() { | ||||
| 	c.Logger.Info().Msg("Initializing flatfiles storage...") | ||||
|  | ||||
| 	path := c.Config.Database.Path | ||||
| 	// Get proper paste file path. | ||||
| 	if strings.Contains(c.Config.Database.Path, "~") { | ||||
| 		curUser, err := user.Current() | ||||
| 		if err != nil { | ||||
| 			c.Logger.Error().Msg("Failed to get current user. Will replace '~' for '/' in storage path!") | ||||
| 			path = strings.Replace(path, "~", "/", -1) | ||||
| 		} | ||||
| 		path = strings.Replace(path, "~", curUser.HomeDir, -1) | ||||
| 	} | ||||
|  | ||||
| 	path, _ = filepath.Abs(path) | ||||
| 	ff.path = path | ||||
| 	c.Logger.Debug().Msgf("Storage path is now: %s", ff.path) | ||||
|  | ||||
| 	// Create directory if neccessary. | ||||
| 	if _, err := os.Stat(ff.path); err != nil { | ||||
| 		c.Logger.Debug().Msgf("Directory '%s' does not exist, creating...", ff.path) | ||||
| 		os.MkdirAll(ff.path, os.ModePerm) | ||||
| 	} else { | ||||
| 		c.Logger.Debug().Msgf("Directory '%s' already exists", ff.path) | ||||
| 	} | ||||
|  | ||||
| 	// Create directory for pastes. | ||||
| 	if _, err := os.Stat(filepath.Join(ff.path, "pastes")); err != nil { | ||||
| 		c.Logger.Debug().Msgf("Directory '%s' does not exist, creating...", filepath.Join(ff.path, "pastes")) | ||||
| 		os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm) | ||||
| 	} else { | ||||
| 		c.Logger.Debug().Msgf("Directory '%s' already exists", filepath.Join(ff.path, "pastes")) | ||||
| 	} | ||||
|  | ||||
| 	// Load pastes index. | ||||
| 	ff.pastesIndex = []*Index{} | ||||
| 	if _, err := os.Stat(filepath.Join(ff.path, "pastes", "index.json")); err != nil { | ||||
| 		c.Logger.Warn().Msg("Pastes index file does not exist, will create new one") | ||||
| 	} else { | ||||
| 		indexData, err := ioutil.ReadFile(filepath.Join(ff.path, "pastes", "index.json")) | ||||
| 		if err != nil { | ||||
| 			c.Logger.Fatal().Msg("Failed to read contents of index file!") | ||||
| 		} | ||||
|  | ||||
| 		err = json.Unmarshal(indexData, &ff.pastesIndex) | ||||
| 		if err != nil { | ||||
| 			c.Logger.Error().Msgf("Failed to parse index file contents from JSON into internal structure. Will create new index file. All of your previous pastes will became unavailable. Error was: %s", err.Error()) | ||||
| 		} | ||||
|  | ||||
| 		c.Logger.Debug().Msgf("Parsed pastes index: %+v", ff.pastesIndex) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (ff *FlatFiles) SavePaste(p *pastesmodel.Paste) (int64, error) { | ||||
| 	ff.writeMutex.Lock() | ||||
| 	// Write paste data on disk. | ||||
| 	filesOnDisk, _ := ioutil.ReadDir(filepath.Join(ff.path, "pastes")) | ||||
| 	pasteID := len(filesOnDisk) + 1 | ||||
| 	c.Logger.Debug().Msgf("Writing paste to disk, ID will be " + strconv.Itoa(pasteID)) | ||||
| 	p.ID = pasteID | ||||
| 	data, err := json.Marshal(p) | ||||
| 	if err != nil { | ||||
| 		ff.writeMutex.Unlock() | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	err = ioutil.WriteFile(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json"), data, 0644) | ||||
| 	if err != nil { | ||||
| 		ff.writeMutex.Unlock() | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	// Add it to cache. | ||||
| 	indexData := &Index{} | ||||
| 	indexData.ID = pasteID | ||||
| 	indexData.Private = p.Private | ||||
| 	ff.pastesIndex = append(ff.pastesIndex, indexData) | ||||
| 	ff.writeMutex.Unlock() | ||||
| 	return int64(pasteID), nil | ||||
| } | ||||
|  | ||||
| func (ff *FlatFiles) Shutdown() { | ||||
| 	c.Logger.Info().Msg("Saving indexes...") | ||||
| 	indexData, err := json.Marshal(ff.pastesIndex) | ||||
| 	if err != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to encode index data into JSON: %s", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = ioutil.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0644) | ||||
| 	if err != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to write index data to file. Pretty sure that you've lost your pastes.") | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										63
									
								
								database/dialects/flatfiles/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								database/dialects/flatfiles/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| // 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 flatfiles | ||||
|  | ||||
| import ( | ||||
| 	// stdlib | ||||
| 	"database/sql" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
| ) | ||||
|  | ||||
| type Handler struct{} | ||||
|  | ||||
| func (dbh Handler) GetDatabaseConnection() *sql.DB { | ||||
| 	return f.GetDatabaseConnection() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) { | ||||
| 	return f.GetPaste(pasteID) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) { | ||||
| 	return f.GetPagedPastes(page) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPastesPages() int { | ||||
| 	return f.GetPastesPages() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) Initialize() { | ||||
| 	f.Initialize() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) { | ||||
| 	return f.SavePaste(p) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) Shutdown() { | ||||
| 	f.Shutdown() | ||||
| } | ||||
							
								
								
									
										31
									
								
								database/dialects/flatfiles/index.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								database/dialects/flatfiles/index.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| // 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 flatfiles | ||||
|  | ||||
| // Index describes paste index structure for flatfiles data storage. | ||||
| type Index struct { | ||||
| 	ID      int  `yaml:"id"` | ||||
| 	Private bool `json:"private"` | ||||
| } | ||||
							
								
								
									
										43
									
								
								database/dialects/interface/dialectinterface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								database/dialects/interface/dialectinterface.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| // 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 dialectinterface | ||||
|  | ||||
| import ( | ||||
| 	// stdlib | ||||
| 	"database/sql" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
| ) | ||||
|  | ||||
| type Interface interface { | ||||
| 	GetDatabaseConnection() *sql.DB | ||||
| 	GetPaste(pasteID int) (*pastesmodel.Paste, error) | ||||
| 	GetPagedPastes(page int) ([]pastesmodel.Paste, error) | ||||
| 	GetPastesPages() int | ||||
| 	Initialize() | ||||
| 	SavePaste(p *pastesmodel.Paste) (int64, error) | ||||
| 	Shutdown() | ||||
| } | ||||
							
								
								
									
										42
									
								
								database/dialects/mysql/exported.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								database/dialects/mysql/exported.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| // 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 mysql | ||||
|  | ||||
| import ( | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/context" | ||||
| 	"github.com/pztrn/fastpastebin/database/dialects/interface" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	c *context.Context | ||||
| 	d *Database | ||||
| ) | ||||
|  | ||||
| func New(cc *context.Context) { | ||||
| 	c = cc | ||||
| 	d = &Database{} | ||||
| 	c.Database.RegisterDialect(dialectinterface.Interface(Handler{})) | ||||
| } | ||||
							
								
								
									
										63
									
								
								database/dialects/mysql/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								database/dialects/mysql/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| // 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 mysql | ||||
|  | ||||
| import ( | ||||
| 	// stdlib | ||||
| 	"database/sql" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
| ) | ||||
|  | ||||
| type Handler struct{} | ||||
|  | ||||
| func (dbh Handler) GetDatabaseConnection() *sql.DB { | ||||
| 	return d.GetDatabaseConnection() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) { | ||||
| 	return d.GetPaste(pasteID) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) { | ||||
| 	return d.GetPagedPastes(page) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPastesPages() int { | ||||
| 	return d.GetPastesPages() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) Initialize() { | ||||
| 	d.Initialize() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) { | ||||
| 	return d.SavePaste(p) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) Shutdown() { | ||||
| 	d.Shutdown() | ||||
| } | ||||
							
								
								
									
										156
									
								
								database/dialects/mysql/mysqldatabase.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								database/dialects/mysql/mysqldatabase.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| // 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 mysql | ||||
|  | ||||
| import ( | ||||
| 	// stdlib | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
|  | ||||
| 	// other | ||||
| 	_ "github.com/go-sql-driver/mysql" | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| ) | ||||
|  | ||||
| // Database is a MySQL/MariaDB connection controlling structure. | ||||
| type Database struct { | ||||
| 	db *sqlx.DB | ||||
| } | ||||
|  | ||||
| func (db *Database) GetDatabaseConnection() *sql.DB { | ||||
| 	return db.db.DB | ||||
| } | ||||
|  | ||||
| // GetPaste returns a single paste by ID. | ||||
| func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) { | ||||
| 	p := &pastesmodel.Paste{} | ||||
| 	err := db.db.Get(p, db.db.Rebind("SELECT * FROM `pastes` WHERE id=?"), pasteID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return p, nil | ||||
| } | ||||
|  | ||||
| func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) { | ||||
| 	var pastesRaw []pastesmodel.Paste | ||||
| 	var pastes []pastesmodel.Paste | ||||
|  | ||||
| 	// Pagination. | ||||
| 	var startPagination = 0 | ||||
| 	if page > 1 { | ||||
| 		startPagination = (page - 1) * c.Config.Pastes.Pagination | ||||
| 	} | ||||
|  | ||||
| 	err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM `pastes` WHERE private != true ORDER BY id DESC LIMIT ? OFFSET ?"), c.Config.Pastes.Pagination, startPagination) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for i := range pastesRaw { | ||||
| 		if !pastesRaw[i].IsExpired() { | ||||
| 			pastes = append(pastes, pastesRaw[i]) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return pastes, nil | ||||
| } | ||||
|  | ||||
| func (db *Database) GetPastesPages() int { | ||||
| 	var pastesRaw []pastesmodel.Paste | ||||
| 	var pastes []pastesmodel.Paste | ||||
| 	err := db.db.Get(&pastesRaw, "SELECT * FROM `pastes` WHERE private != true") | ||||
| 	if err != nil { | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	// Check if pastes isn't expired. | ||||
| 	for i := range pastesRaw { | ||||
| 		if !pastesRaw[i].IsExpired() { | ||||
| 			pastes = append(pastes, pastesRaw[i]) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Calculate pages. | ||||
| 	pages := len(pastes) / c.Config.Pastes.Pagination | ||||
| 	// Check if we have any remainder. Add 1 to pages count if so. | ||||
| 	if len(pastes)%c.Config.Pastes.Pagination > 0 { | ||||
| 		pages++ | ||||
| 	} | ||||
|  | ||||
| 	return pages | ||||
| } | ||||
|  | ||||
| // Initialize initializes MySQL/MariaDB connection. | ||||
| func (db *Database) Initialize() { | ||||
| 	c.Logger.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 = "" | ||||
| 	if c.Config.Database.Password == "" { | ||||
| 		userpass = c.Config.Database.Username | ||||
| 	} else { | ||||
| 		userpass = c.Config.Database.Username + ":" + c.Config.Database.Password | ||||
| 	} | ||||
|  | ||||
| 	dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, c.Config.Database.Address, c.Config.Database.Port, c.Config.Database.Database) | ||||
| 	c.Logger.Debug().Msgf("Database connection string: %s", dbConnString) | ||||
|  | ||||
| 	dbConn, err := sqlx.Connect("mysql", dbConnString) | ||||
| 	if err != nil { | ||||
| 		c.Logger.Panic().Msgf("Failed to connect to database: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// Force UTC for current connection. | ||||
| 	_ = dbConn.MustExec("SET @@session.time_zone='+00:00';") | ||||
|  | ||||
| 	c.Logger.Info().Msg("Database connection established") | ||||
| 	db.db = dbConn | ||||
| } | ||||
|  | ||||
| func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) { | ||||
| 	result, err := db.db.NamedExec("INSERT INTO `pastes` (title, data, created_at, keep_for, keep_for_unit_type, language, private, password, password_salt) VALUES (:title, :data, :created_at, :keep_for, :keep_for_unit_type, :language, :private, :password, :password_salt)", p) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	ID, err1 := result.LastInsertId() | ||||
| 	if err1 != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	return ID, nil | ||||
| } | ||||
|  | ||||
| func (db *Database) Shutdown() { | ||||
| 	err := db.db.Close() | ||||
| 	if err != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to close database connection: %s", err.Error()) | ||||
| 	} | ||||
| } | ||||
| @@ -25,20 +25,47 @@ | ||||
| package database | ||||
|  | ||||
| import ( | ||||
| 	// other | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	// stdlib | ||||
| 	"database/sql" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/database/dialects/interface" | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
| ) | ||||
|  | ||||
| // Handler is an interfaceable structure that proxifies calls from anyone | ||||
| // to Database structure. | ||||
| type Handler struct{} | ||||
|  | ||||
| // GetDatabaseConnection returns current database connection. | ||||
| func (dbh Handler) GetDatabaseConnection() *sqlx.DB { | ||||
| func (dbh Handler) GetDatabaseConnection() *sql.DB { | ||||
| 	return d.GetDatabaseConnection() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) { | ||||
| 	return d.GetPaste(pasteID) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) { | ||||
| 	return d.GetPagedPastes(page) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) GetPastesPages() int { | ||||
| 	return d.GetPastesPages() | ||||
| } | ||||
|  | ||||
| // Initialize initializes connection to database. | ||||
| func (dbh Handler) Initialize() { | ||||
| 	d.Initialize() | ||||
| } | ||||
|  | ||||
| func (dbh Handler) RegisterDialect(di dialectinterface.Interface) { | ||||
| 	d.RegisterDialect(di) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) { | ||||
| 	return d.SavePaste(p) | ||||
| } | ||||
|  | ||||
| func (dbh Handler) Shutdown() { | ||||
| 	d.Shutdown() | ||||
| } | ||||
|   | ||||
| @@ -25,13 +25,23 @@ | ||||
| package databaseinterface | ||||
|  | ||||
| import ( | ||||
| 	// other | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	// stdlib | ||||
| 	"database/sql" | ||||
|  | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/database/dialects/interface" | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
| ) | ||||
|  | ||||
| // Interface represents database interface which is available to all | ||||
| // parts of application and registers with context.Context. | ||||
| type Interface interface { | ||||
| 	GetDatabaseConnection() *sqlx.DB | ||||
| 	GetDatabaseConnection() *sql.DB | ||||
| 	GetPaste(pasteID int) (*pastesmodel.Paste, error) | ||||
| 	GetPagedPastes(page int) ([]pastesmodel.Paste, error) | ||||
| 	GetPastesPages() int | ||||
| 	Initialize() | ||||
| 	RegisterDialect(dialectinterface.Interface) | ||||
| 	SavePaste(p *pastesmodel.Paste) (int64, error) | ||||
| 	Shutdown() | ||||
| } | ||||
|   | ||||
| @@ -54,8 +54,12 @@ func Migrate() { | ||||
| 	// Add new migrations BEFORE this message. | ||||
|  | ||||
| 	dbConn := c.Database.GetDatabaseConnection() | ||||
| 	err := goose.Up(dbConn.DB, ".") | ||||
| 	if err != nil { | ||||
| 		c.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error()) | ||||
| 	if dbConn != nil { | ||||
| 		err := goose.Up(dbConn, ".") | ||||
| 		if err != nil { | ||||
| 			c.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error()) | ||||
| 		} | ||||
| 	} else { | ||||
| 		c.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,13 @@ | ||||
| # Database configuration. | ||||
| # Only MySQL database is supported for now. | ||||
| # Only MySQL database and flatfiles are supported for now. | ||||
| database: | ||||
|   # Database type. The only supported ATM is "mysql" and "flatfiles". | ||||
|   type: "mysql" | ||||
|   # Path for data stored with "flatfiles" database adapter. | ||||
|   # Will be comletely ignored for MySQL/MariaDB. | ||||
|   path: "./data" | ||||
|   # Next parameters are strictly for MySQL/MariaDB connections and | ||||
|   # will be ignored by "flatfiles" adapter. | ||||
|   address: "localhost" | ||||
|   port: "3306" | ||||
|   username: "fastpastebin" | ||||
| @@ -18,9 +25,14 @@ logging: | ||||
|  | ||||
| # HTTP server configuration. | ||||
| http: | ||||
|   address: "localhost" | ||||
|   address: "192.168.0.14" | ||||
|   port: "25544" | ||||
|   # By default we're allowing only HTTPS requests. Setting this to true | ||||
|   # will allow HTTP requests. Useful for developing or if you're | ||||
|   # running Fast Pastebin behind reverse proxy that does SSL termination. | ||||
|   allow_insecure: false | ||||
|   allow_insecure: true | ||||
|  | ||||
| # Pastes configuration. | ||||
| pastes: | ||||
|   # Pastes per page. | ||||
|   pagination: 10 | ||||
| @@ -36,6 +36,7 @@ import ( | ||||
| 	// local | ||||
| 	"github.com/pztrn/fastpastebin/captcha" | ||||
| 	"github.com/pztrn/fastpastebin/pagination" | ||||
| 	"github.com/pztrn/fastpastebin/pastes/model" | ||||
| 	"github.com/pztrn/fastpastebin/templater" | ||||
|  | ||||
| 	// other | ||||
| @@ -61,7 +62,7 @@ func pasteGET(ec echo.Context) error { | ||||
| 	c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID) | ||||
|  | ||||
| 	// Get paste. | ||||
| 	paste, err1 := GetByID(pasteID) | ||||
| 	paste, err1 := c.Database.GetPaste(pasteID) | ||||
| 	if err1 != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error()) | ||||
| 		errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found") | ||||
| @@ -172,7 +173,7 @@ func pastePasswordedVerifyGet(ec echo.Context) error { | ||||
| 	pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) | ||||
|  | ||||
| 	// Get paste. | ||||
| 	paste, err1 := GetByID(pasteID) | ||||
| 	paste, err1 := c.Database.GetPaste(pasteID) | ||||
| 	if err1 != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error()) | ||||
| 		errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found") | ||||
| @@ -216,7 +217,7 @@ func pastePasswordedVerifyPost(ec echo.Context) error { | ||||
| 	c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID) | ||||
|  | ||||
| 	// Get paste. | ||||
| 	paste, err1 := GetByID(pasteID) | ||||
| 	paste, err1 := c.Database.GetPaste(pasteID) | ||||
| 	if err1 != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error()) | ||||
| 		errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found") | ||||
| @@ -276,7 +277,7 @@ func pastePOST(ec echo.Context) error { | ||||
| 		return ec.HTML(http.StatusBadRequest, errtpl) | ||||
| 	} | ||||
|  | ||||
| 	paste := &Paste{ | ||||
| 	paste := &pastesmodel.Paste{ | ||||
| 		Title:    params["paste-title"][0], | ||||
| 		Data:     params["paste-contents"][0], | ||||
| 		Language: params["paste-language"][0], | ||||
| @@ -301,7 +302,7 @@ func pastePOST(ec echo.Context) error { | ||||
| 	paste.KeepFor = keepFor | ||||
|  | ||||
| 	keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0] | ||||
| 	keepForUnit := PASTE_KEEPS_CORELLATION[keepForUnitRaw] | ||||
| 	keepForUnit := pastesmodel.PASTE_KEEPS_CORELLATION[keepForUnitRaw] | ||||
| 	paste.KeepForUnitType = keepForUnit | ||||
|  | ||||
| 	// Try to autodetect if it was selected. | ||||
| @@ -326,7 +327,7 @@ func pastePOST(ec echo.Context) error { | ||||
| 		paste.CreatePassword(pastePassword[0]) | ||||
| 	} | ||||
|  | ||||
| 	id, err2 := Save(paste) | ||||
| 	id, err2 := c.Database.SavePaste(paste) | ||||
| 	if err2 != nil { | ||||
| 		c.Logger.Debug().Msgf("Failed to save paste: %s", err2.Error()) | ||||
| 		errtpl := templater.GetErrorTemplate(ec, "Failed to save paste. Please, try again later.") | ||||
| @@ -353,7 +354,7 @@ func pasteRawGET(ec echo.Context) error { | ||||
| 	c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID) | ||||
|  | ||||
| 	// Get paste. | ||||
| 	paste, err1 := GetByID(pasteID) | ||||
| 	paste, err1 := c.Database.GetPaste(pasteID) | ||||
| 	if err1 != nil { | ||||
| 		c.Logger.Error().Msgf("Failed to get paste #%d from database: %s", pasteID, err1.Error()) | ||||
| 		return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.") | ||||
| @@ -401,7 +402,7 @@ func pastesGET(ec echo.Context) error { | ||||
| 	c.Logger.Debug().Msgf("Requested page #%d", page) | ||||
|  | ||||
| 	// Get pastes IDs. | ||||
| 	pastes, err3 := GetPagedPastes(page) | ||||
| 	pastes, err3 := c.Database.GetPagedPastes(page) | ||||
| 	c.Logger.Debug().Msgf("Got %d pastes", len(pastes)) | ||||
|  | ||||
| 	var pastesString = "No pastes to show." | ||||
| @@ -438,7 +439,7 @@ func pastesGET(ec echo.Context) error { | ||||
| 	} | ||||
|  | ||||
| 	// Pagination. | ||||
| 	pages := GetPastesPages() | ||||
| 	pages := c.Database.GetPastesPages() | ||||
| 	c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page) | ||||
| 	paginationHTML := pagination.CreateHTML(page, pages, "/pastes/") | ||||
|  | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
| // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE | ||||
| // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
| 
 | ||||
| package pastes | ||||
| package pastesmodel | ||||
| 
 | ||||
| import ( | ||||
| 	// stdlib | ||||
| @@ -55,16 +55,16 @@ var ( | ||||
| 
 | ||||
| // Paste represents paste itself. | ||||
| type Paste struct { | ||||
| 	ID              int        `db:"id"` | ||||
| 	Title           string     `db:"title"` | ||||
| 	Data            string     `db:"data"` | ||||
| 	CreatedAt       *time.Time `db:"created_at"` | ||||
| 	KeepFor         int        `db:"keep_for"` | ||||
| 	KeepForUnitType int        `db:"keep_for_unit_type"` | ||||
| 	Language        string     `db:"language"` | ||||
| 	Private         bool       `db:"private"` | ||||
| 	Password        string     `db:"password"` | ||||
| 	PasswordSalt    string     `db:"password_salt"` | ||||
| 	ID              int        `db:"id" json:"id"` | ||||
| 	Title           string     `db:"title" json:"title"` | ||||
| 	Data            string     `db:"data" json:"data"` | ||||
| 	CreatedAt       *time.Time `db:"created_at" json:"created_at"` | ||||
| 	KeepFor         int        `db:"keep_for" json:"keep_for"` | ||||
| 	KeepForUnitType int        `db:"keep_for_unit_type" json:"keep_for_unit_type"` | ||||
| 	Language        string     `db:"language" json:"language"` | ||||
| 	Private         bool       `db:"private" json:"private"` | ||||
| 	Password        string     `db:"password" json:"password"` | ||||
| 	PasswordSalt    string     `db:"password_salt" json:"password_salt"` | ||||
| } | ||||
| 
 | ||||
| // CreatePassword creates password for current paste. | ||||
| @@ -1,115 +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 pastes | ||||
|  | ||||
| const ( | ||||
| 	// Pagination. Hardcoded for 10 for now. | ||||
| 	PAGINATION = 10 | ||||
| ) | ||||
|  | ||||
| // GetByID returns a single paste by ID. | ||||
| func GetByID(id int) (*Paste, error) { | ||||
| 	p := &Paste{} | ||||
| 	dbConn := c.Database.GetDatabaseConnection() | ||||
| 	err := dbConn.Get(p, dbConn.Rebind("SELECT * FROM `pastes` WHERE id=?"), id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Lets go with checking. | ||||
|  | ||||
| 	return p, nil | ||||
| } | ||||
|  | ||||
| // GetPagedPastes returns a paged slice of pastes. | ||||
| func GetPagedPastes(page int) ([]Paste, error) { | ||||
| 	var pastesRaw []Paste | ||||
| 	var pastes []Paste | ||||
| 	dbConn := c.Database.GetDatabaseConnection() | ||||
|  | ||||
| 	// Pagination - 10 pastes on page. | ||||
| 	var startPagination = 0 | ||||
| 	if page > 1 { | ||||
| 		startPagination = (page - 1) * PAGINATION | ||||
| 	} | ||||
|  | ||||
| 	err := dbConn.Select(&pastesRaw, dbConn.Rebind("SELECT * FROM `pastes` WHERE private != true ORDER BY id DESC LIMIT ? OFFSET ?"), PAGINATION, startPagination) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for i := range pastesRaw { | ||||
| 		if !pastesRaw[i].IsExpired() { | ||||
| 			pastes = append(pastes, pastesRaw[i]) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return pastes, nil | ||||
|  | ||||
| } | ||||
|  | ||||
| // GetPastesPages returns an integer that represents quantity of pages | ||||
| // that can be requested (or drawn in paginator). | ||||
| func GetPastesPages() int { | ||||
| 	var pastesRaw []Paste | ||||
| 	var pastes []Paste | ||||
| 	dbConn := c.Database.GetDatabaseConnection() | ||||
| 	err := dbConn.Get(&pastesRaw, "SELECT * FROM `pastes` WHERE private != true") | ||||
| 	if err != nil { | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	// Check if pastes isn't expired. | ||||
| 	for i := range pastesRaw { | ||||
| 		if !pastesRaw[i].IsExpired() { | ||||
| 			pastes = append(pastes, pastesRaw[i]) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Calculate pages. | ||||
| 	pages := len(pastes) / PAGINATION | ||||
| 	// Check if we have any remainder. Add 1 to pages count if so. | ||||
| 	if len(pastes)%PAGINATION > 0 { | ||||
| 		pages++ | ||||
| 	} | ||||
|  | ||||
| 	return pages | ||||
| } | ||||
|  | ||||
| // Save saves paste to database and returns it's ID. | ||||
| func Save(p *Paste) (int64, error) { | ||||
| 	dbConn := c.Database.GetDatabaseConnection() | ||||
| 	result, err := dbConn.NamedExec("INSERT INTO `pastes` (title, data, created_at, keep_for, keep_for_unit_type, language, private, password, password_salt) VALUES (:title, :data, :created_at, :keep_for, :keep_for_unit_type, :language, :private, :password, :password_salt)", p) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	ID, err1 := result.LastInsertId() | ||||
| 	if err1 != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	return ID, nil | ||||
| } | ||||
		Reference in New Issue
	
	Block a user