diff --git a/README.md b/README.md index de3f3881..1e1db90b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Pastebins are a type of online content storage service where users can store pla - [x] Configurable ratelimiting, expiration, compression, etc. - [x] Modern, JavaScript-free user interface - [x] Syntax highlighting for all the most popular languages and Raw text mode -- [x] SQLite and PostgreSQL Support +- [x] SQLite, MySQL, and PostgreSQL Support - [x] Basic Auth for private instances - [ ] Password-protected encrypted pastes - [ ] Paste collections @@ -106,7 +106,7 @@ volumes: #### Manually > [!IMPORTANT] -> **Requires: [Git](https://git-scm.com/downloads), [Go 1.22.4](https://go.dev/doc/install), [GNU Makefile](https://www.gnu.org/software/make/#download), and a SQLite database or [PostgreSQL](https://www.postgresql.org/download/) [server](https://m.do.co/c/beaf675c3e00).** +> **Requires: [Git](https://git-scm.com/downloads), [Go 1.22.4](https://go.dev/doc/install), [GNU Makefile](https://www.gnu.org/software/make/#download), and a SQLite, MySQL, or [PostgreSQL](https://www.postgresql.org/download/) [server](https://m.do.co/c/beaf675c3e00).** ```sh # Clone the Github repository @@ -118,6 +118,7 @@ $ make spirit # Start Spacebin $ SPIRIT_CONNECTION_URI="sqlite://database.sqlite" ./bin/spirit # SQLite +$ SPIRIT_CONNECTION_URI="mysql://?parseTime=true" ./bin/spirit $ SPIRIT_CONNECTION_URI="postgres://" ./bin/spirit # PostgreSQL # Success! Spacebin is now available at port 9000 on your machine. @@ -149,6 +150,8 @@ Spacebin supports two database formats: **SQLite** and **Postgres** - For SQLite, use either the scheme `file://` or `sqlite://` and a file name. - Example: `file://database.db` - For PostgreSQL, use [the standard PostgreSQL URI format](https://stackoverflow.com/questions/3582552/what-is-the-format-for-the-postgresql-connection-string-url#20722229). +- For MySQL, use the [DSN format](https://github.com/go-sql-driver/mysql?tab=readme-ov-file#dsn-data-source-name) prefixed with `mysql://` or `mariadb://` + - You must set the `parseTime` option to true; append `?parseTime=true` to the end of the URI ### Usage diff --git a/cmd/spacebin/main.go b/cmd/spacebin/main.go index 4b2e8660..d073e5c0 100644 --- a/cmd/spacebin/main.go +++ b/cmd/spacebin/main.go @@ -61,29 +61,27 @@ func main() { // Connect either to SQLite or PostgreSQL switch uri.Scheme { case "file", "sqlite": - sq, err := database.NewSQLite(uri.Host) - if err != nil { - log.Fatal(). - Err(err). - Msg("Could not connect to database") - } - db = sq - case "postgresql": - pg, err := database.NewPostgres(uri.String()) - if err != nil { - log.Fatal(). - Err(err). - Msg("Could not connect to database") - } - db = pg + db, err = database.NewSQLite(uri) + case "postgresql", "postgres": + db, err = database.NewPostgres(uri) + case "mysql", "mariadb": + db, err = database.NewMySQL(uri) + } + + if err != nil { + log.Fatal(). + Err(err). + Msg("Could not connect to database") } + // Perform migrations if err := db.Migrate(context.Background()); err != nil { log.Fatal(). Err(err). Msg("Failed migrations; Could not create DOCUMENTS tables.") } + // Create a new server and register middleware, security headers, static files, and handlers m := server.NewServer(&config.Config, db) m.MountMiddleware() @@ -95,11 +93,13 @@ func main() { m.MountHandlers() + // Create the server on the specified host and port srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", config.Config.Host, config.Config.Port), Handler: m.Router, } + // Graceful shutdown srvCtx, srvStopCtx := context.WithCancel(context.Background()) // Watch for OS signals diff --git a/go.mod b/go.mod index 8d73617a..4d7dca9d 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect @@ -35,6 +36,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index d37cd66c..4146f655 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= @@ -28,6 +30,8 @@ github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZ github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= diff --git a/internal/database/database_mysql.go b/internal/database/database_mysql.go new file mode 100644 index 00000000..b97392af --- /dev/null +++ b/internal/database/database_mysql.go @@ -0,0 +1,79 @@ +/* + * Copyright 2020-2024 Luke Whritenour + + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "context" + "database/sql" + "net/url" + "strings" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +type MySQL struct { + *sql.DB +} + +func NewMySQL(uri *url.URL) (Database, error) { + _, uriTrimmed, _ := strings.Cut(uri.String(), uri.Scheme+"://") + db, err := sql.Open("mysql", uriTrimmed) + + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + + return &MySQL{db}, err +} + +func (m *MySQL) Migrate(ctx context.Context) error { + _, err := m.Exec(` +CREATE TABLE IF NOT EXISTS documents ( + id VARCHAR(255) PRIMARY KEY, + content TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +)`) + + return err +} + +func (m *MySQL) GetDocument(ctx context.Context, id string) (Document, error) { + doc := new(Document) + row := m.QueryRow("SELECT * FROM documents WHERE id=?", id) + err := row.Scan(&doc.ID, &doc.Content, &doc.CreatedAt, &doc.UpdatedAt) + + return *doc, err +} + +func (m *MySQL) CreateDocument(ctx context.Context, id, content string) error { + tx, err := m.Begin() + + if err != nil { + return err + } + + _, err = tx.Exec("INSERT INTO documents (id, content) VALUES (?, ?)", + id, content) // created_at and updated_at are auto-generated + + if err != nil { + return err + } + + return tx.Commit() +} diff --git a/internal/database/database_pg.go b/internal/database/database_pg.go index 3348eaf3..83ed2fd6 100644 --- a/internal/database/database_pg.go +++ b/internal/database/database_pg.go @@ -19,6 +19,7 @@ package database import ( "context" "database/sql" + "net/url" _ "github.com/lib/pq" ) @@ -27,8 +28,8 @@ type Postgres struct { *sql.DB } -func NewPostgres(uri string) (Database, error) { - db, err := sql.Open("postgres", uri) +func NewPostgres(uri *url.URL) (Database, error) { + db, err := sql.Open("postgres", uri.String()) return &Postgres{db}, err } diff --git a/internal/database/database_sqlite.go b/internal/database/database_sqlite.go index 2747c97d..288bbcf9 100644 --- a/internal/database/database_sqlite.go +++ b/internal/database/database_sqlite.go @@ -19,6 +19,7 @@ package database import ( "context" "database/sql" + "net/url" "sync" _ "modernc.org/sqlite" @@ -29,8 +30,8 @@ type SQLite struct { sync.RWMutex } -func NewSQLite(filesath string) (Database, error) { - db, err := sql.Open("sqlite", filesath) +func NewSQLite(uri *url.URL) (Database, error) { + db, err := sql.Open("sqlite", uri.Host) return &SQLite{db, sync.RWMutex{}}, err }