From 0cca0f453fee9c0f9b2fc111fb878ec77b9ff5f4 Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Sun, 27 May 2018 12:25:01 +0500 Subject: [PATCH] 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. --- README.md | 1 + config/database.go | 2 + config/pastes.go | 30 +++ config/struct.go | 1 + context/context.go | 2 + context/exported.go | 2 +- database/database.go | 70 +++-- database/dialects/flatfiles/exported.go | 42 +++ database/dialects/flatfiles/flatfiles.go | 246 ++++++++++++++++++ database/dialects/flatfiles/handler.go | 63 +++++ database/dialects/flatfiles/index.go | 31 +++ .../dialects/interface/dialectinterface.go | 43 +++ database/dialects/mysql/exported.go | 42 +++ database/dialects/mysql/handler.go | 63 +++++ database/dialects/mysql/mysqldatabase.go | 156 +++++++++++ database/handler.go | 35 ++- database/interface/databaseinterface.go | 16 +- database/migrations/exported.go | 10 +- examples/fastpastebin.yaml.dist | 18 +- pastes/api_http.go | 19 +- pastes/{ => model}/model.go | 22 +- pastes/queries.go | 115 -------- 22 files changed, 854 insertions(+), 175 deletions(-) create mode 100644 config/pastes.go create mode 100644 database/dialects/flatfiles/exported.go create mode 100644 database/dialects/flatfiles/flatfiles.go create mode 100644 database/dialects/flatfiles/handler.go create mode 100644 database/dialects/flatfiles/index.go create mode 100644 database/dialects/interface/dialectinterface.go create mode 100644 database/dialects/mysql/exported.go create mode 100644 database/dialects/mysql/handler.go create mode 100644 database/dialects/mysql/mysqldatabase.go rename pastes/{ => model}/model.go (86%) delete mode 100644 pastes/queries.go diff --git a/README.md b/README.md index 2afd6e3..8cce0df 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/config/database.go b/config/database.go index 8cfb4a7..e224c19 100644 --- a/config/database.go +++ b/config/database.go @@ -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"` diff --git a/config/pastes.go b/config/pastes.go new file mode 100644 index 0000000..a230f67 --- /dev/null +++ b/config/pastes.go @@ -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"` +} diff --git a/config/struct.go b/config/struct.go index db2ae59..b802963 100644 --- a/config/struct.go +++ b/config/struct.go @@ -29,4 +29,5 @@ type ConfigStruct struct { Database ConfigDatabase `yaml:"database"` Logging ConfigLogging `yaml:"logging"` HTTP ConfigHTTP `yaml:"http"` + Pastes ConfigPastes `yaml:"pastes"` } diff --git a/context/context.go b/context/context.go index d9057ce..41dbe96 100644 --- a/context/context.go +++ b/context/context.go @@ -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() } diff --git a/context/exported.go b/context/exported.go index 19b447c..4919408 100644 --- a/context/exported.go +++ b/context/exported.go @@ -26,7 +26,7 @@ package context const ( // Version . - Version = "0.1.1" + Version = "0.1.2-dev" ) // New creates new context. diff --git a/database/database.go b/database/database.go index 0de4751..0138276 100644 --- a/database/database.go +++ b/database/database.go @@ -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() } diff --git a/database/dialects/flatfiles/exported.go b/database/dialects/flatfiles/exported.go new file mode 100644 index 0000000..baa0efa --- /dev/null +++ b/database/dialects/flatfiles/exported.go @@ -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{})) +} diff --git a/database/dialects/flatfiles/flatfiles.go b/database/dialects/flatfiles/flatfiles.go new file mode 100644 index 0000000..4b862f1 --- /dev/null +++ b/database/dialects/flatfiles/flatfiles.go @@ -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 + } +} diff --git a/database/dialects/flatfiles/handler.go b/database/dialects/flatfiles/handler.go new file mode 100644 index 0000000..2089993 --- /dev/null +++ b/database/dialects/flatfiles/handler.go @@ -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() +} diff --git a/database/dialects/flatfiles/index.go b/database/dialects/flatfiles/index.go new file mode 100644 index 0000000..dbdb37f --- /dev/null +++ b/database/dialects/flatfiles/index.go @@ -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"` +} diff --git a/database/dialects/interface/dialectinterface.go b/database/dialects/interface/dialectinterface.go new file mode 100644 index 0000000..553766b --- /dev/null +++ b/database/dialects/interface/dialectinterface.go @@ -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() +} diff --git a/database/dialects/mysql/exported.go b/database/dialects/mysql/exported.go new file mode 100644 index 0000000..21c5ca7 --- /dev/null +++ b/database/dialects/mysql/exported.go @@ -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{})) +} diff --git a/database/dialects/mysql/handler.go b/database/dialects/mysql/handler.go new file mode 100644 index 0000000..25ecd01 --- /dev/null +++ b/database/dialects/mysql/handler.go @@ -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() +} diff --git a/database/dialects/mysql/mysqldatabase.go b/database/dialects/mysql/mysqldatabase.go new file mode 100644 index 0000000..df65e45 --- /dev/null +++ b/database/dialects/mysql/mysqldatabase.go @@ -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()) + } +} diff --git a/database/handler.go b/database/handler.go index 775c253..d15576c 100644 --- a/database/handler.go +++ b/database/handler.go @@ -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() +} diff --git a/database/interface/databaseinterface.go b/database/interface/databaseinterface.go index 64e4548..cd85e8a 100644 --- a/database/interface/databaseinterface.go +++ b/database/interface/databaseinterface.go @@ -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() } diff --git a/database/migrations/exported.go b/database/migrations/exported.go index 0b5d36e..7431a41 100644 --- a/database/migrations/exported.go +++ b/database/migrations/exported.go @@ -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") } } diff --git a/examples/fastpastebin.yaml.dist b/examples/fastpastebin.yaml.dist index f492421..285f477 100644 --- a/examples/fastpastebin.yaml.dist +++ b/examples/fastpastebin.yaml.dist @@ -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 \ No newline at end of file + allow_insecure: true + +# Pastes configuration. +pastes: + # Pastes per page. + pagination: 10 \ No newline at end of file diff --git a/pastes/api_http.go b/pastes/api_http.go index 9edf738..a4f3923 100644 --- a/pastes/api_http.go +++ b/pastes/api_http.go @@ -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/") diff --git a/pastes/model.go b/pastes/model/model.go similarity index 86% rename from pastes/model.go rename to pastes/model/model.go index 49b985e..b53ed8e 100644 --- a/pastes/model.go +++ b/pastes/model/model.go @@ -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. diff --git a/pastes/queries.go b/pastes/queries.go deleted file mode 100644 index d03f519..0000000 --- a/pastes/queries.go +++ /dev/null @@ -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 -}