diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aca8eb8 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/leafee98/paste-abroad + +go 1.19 + +require ( + github.com/fasthttp/router v1.4.14 + github.com/mattn/go-sqlite3 v1.14.16 + github.com/valyala/fasthttp v1.44.0 +) + +require ( + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect +) diff --git a/internel/storage/sqlite/sqlite.go b/internel/storage/sqlite/sqlite.go new file mode 100644 index 0000000..48ea265 --- /dev/null +++ b/internel/storage/sqlite/sqlite.go @@ -0,0 +1,120 @@ +package sqlite + +import ( + "github.com/leafee98/paste-abroad/internel/storage" + "github.com/leafee98/paste-abroad/internel/utils" + + "github.com/mattn/go-sqlite3" + + "database/sql" + "errors" + "time" +) + +type StorageSqlite struct { + db *sql.DB + idLength int +} + +func New(db_path string, idLength int) (*StorageSqlite, error) { + var s StorageSqlite + var err error + + s.idLength = idLength + + s.db, err = sql.Open("sqlite3", db_path) + if err != nil { + return &s, err + } + + if err := initDatabase(s.db); err != nil { + return nil, err + } + + return &s, nil +} + +func initDatabase(db *sql.DB) error { + sqlStmt := ` + create table if not exists paste ( + id text unique, + content blob, + expire integer + ); + + create index if not exists paste_id + on paste ( id ); + ` + + if _, err := db.Exec(sqlStmt); err != nil { + return err + } + + return nil +} + +func (s *StorageSqlite) Close() error { + return s.db.Close() +} + +func (s *StorageSqlite) Save(p *storage.Paste) (string, error) { + stmt, err := s.db.Prepare(`insert into paste (id, content, expire) values (?, ?, ?);`) + if err != nil { + return "", err + } + defer stmt.Close() + + var id string + + for true { + id = utils.GenerateId(s.idLength) + _, err = stmt.Exec(id, p.Content, p.Expire) + if err == nil { + break + } else if errors.Is(err, sqlite3.ErrConstraint) { + continue + } else { + return "", err + } + } + + return id, nil +} + +func (s *StorageSqlite) Get(id string) (*storage.Paste, error) { + stmt, err := s.db.Prepare(`select content, expire from paste where id = ?`) + if err != nil { + return nil, err + } + rows, err := stmt.Query(id) + + var p storage.Paste + + if !rows.Next() { + return nil, errors.New("No such paste") + } + + if err = rows.Scan(&p.Content, &p.Expire); err != nil { + return nil, err + } + + return &p, nil +} + +func (s *StorageSqlite) Purge() (int64, error) { + stmt, err := s.db.Prepare(`delete from paste where expire < ?`) + if err != nil { + return 0, err + } + + affected, err := stmt.Exec(time.Now().UnixMilli()) + if err != nil { + return 0, err + } + + if affectedRows, err := affected.RowsAffected() ; err != nil { + return 0, err + } else { + return affectedRows, nil + } +} diff --git a/internel/storage/sqlite_test.go b/internel/storage/sqlite_test.go new file mode 100644 index 0000000..9447207 --- /dev/null +++ b/internel/storage/sqlite_test.go @@ -0,0 +1,76 @@ +package storage_test + +import ( + "github.com/leafee98/paste-abroad/internel/storage" + "github.com/leafee98/paste-abroad/internel/storage/sqlite" + + "time" + "testing" +) + +func compareBytes(a []byte, b []byte) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +func TestSqlite(t * testing.T) { + s, err := sqlite.New("", 8) + if err != nil { + t.Fatalf("Fatal on New: %v", err) + } + defer s.Close() + + testGetSave(s, t) +} + +func testGetSave(s *sqlite.StorageSqlite, t * testing.T) { + var a = storage.Paste { + Content: []byte("abc"), + Expire: time.Now().UnixMilli() + 15 * 1000, + } + var b = storage.Paste { + Content: []byte("def"), + Expire: time.Now().UnixMilli() - 15 * 1000, + } + + id_a, err := s.Save(&a) + if err != nil { + t.Fatalf("Fatal on Save a: %v", err) + } + + id_b, err := s.Save(&b) + if err != nil { + t.Fatalf("Fatal on Save b: %v", err) + } + + affected, err := s.Purge() + if err != nil { + t.Fatalf("Fatal on Purge: %v", err) + } + if affected != 1 { + t.Fatalf("Purged %v rows, expect 1 row", affected) + } + + var p_a, _ *storage.Paste + + if p_a, err = s.Get(id_a); err != nil { + t.Fatalf("Fatal on Get a: %v", err) + + if !compareBytes(p_a.Content, []byte("abc")) { + t.Fatal("Content not match") + } + } + if _, err = s.Get(id_b); err == nil { + t.Fatalf("Fatal on Get b, it shoud have been purged") + + } +} diff --git a/internel/storage/storage.go b/internel/storage/storage.go new file mode 100644 index 0000000..40f3dc5 --- /dev/null +++ b/internel/storage/storage.go @@ -0,0 +1,13 @@ +package storage + +type Storage interface { + Save(Paste) (string, error) + Get(string) (*Paste, error) + Purge() (int64, error) +} + +type Paste struct { + Content []byte + Expire int64 +} +