From 2c3ca08dbfdd6b8a3ae5e8fe4cd220f6db3312d8 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Sat, 20 Sep 2025 08:37:16 +0200 Subject: [PATCH] feat: Use snowflake identifiers Fixes #29. Signed-off-by: Julien Riou --- src/cmd/coller/main.go | 3 ++- src/cmd/collerd/README.md | 2 +- src/cmd/collerd/main.go | 4 ++-- src/go.mod | 1 + src/go.sum | 2 ++ src/server/config.go | 8 ++++---- src/server/db.go | 12 +++++++++++- src/server/note.go | 27 +-------------------------- src/server/server.go | 4 ---- 9 files changed, 24 insertions(+), 39 deletions(-) diff --git a/src/cmd/coller/main.go b/src/cmd/coller/main.go index dc75eab..67a7e59 100644 --- a/src/cmd/coller/main.go +++ b/src/cmd/coller/main.go @@ -13,9 +13,10 @@ import ( "path/filepath" "syscall" - "git.riou.xyz/jriou/coller/internal" "golang.design/x/clipboard" "golang.org/x/term" + + "git.riou.xyz/jriou/coller/internal" ) var ( diff --git a/src/cmd/collerd/README.md b/src/cmd/collerd/README.md index 5f9a06d..b85139a 100644 --- a/src/cmd/collerd/README.md +++ b/src/cmd/collerd/README.md @@ -18,7 +18,7 @@ The file format is **JSON**: * **title** (string): Title of the website * **database_type** (string): Type of the database (default "sqlite", "postgres" also supported) * **database_dsn** (string): Connection string for the database (default "collerd.db") -* **id_length** (int): Number of characters for note identifiers (default 5) +* **node_id** (int): Number between 0 and 1023 to define the node generating identifiers (see [snowflake](https://github.com/bwmarrin/snowflake)) * **password_length** (int): Number of characters for generated passwords (default 16) * **expiration_interval** (int): Number of seconds to wait between two expiration runs * **listen_address** (string): Address to listen for the web server (default "0.0.0.0") diff --git a/src/cmd/collerd/main.go b/src/cmd/collerd/main.go index 35ca6a2..4e45b1f 100644 --- a/src/cmd/collerd/main.go +++ b/src/cmd/collerd/main.go @@ -5,9 +5,10 @@ import ( "log/slog" "os" + "github.com/prometheus/client_golang/prometheus" + "git.riou.xyz/jriou/coller/internal" "git.riou.xyz/jriou/coller/server" - "github.com/prometheus/client_golang/prometheus" ) var ( @@ -69,7 +70,6 @@ func handleMain() int { return internal.ReturnError(logger, "could not create server", err) } - srv.SetIDLength(config.IDLength) srv.SetPasswordLength(config.PasswordLength) if config.EnableMetrics { diff --git a/src/go.mod b/src/go.mod index 35d8e39..cb3f6c5 100644 --- a/src/go.mod +++ b/src/go.mod @@ -17,6 +17,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect diff --git a/src/go.sum b/src/go.sum index 2f772f9..1d47cb0 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,5 +1,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/src/server/config.go b/src/server/config.go index f07c9a7..97983f9 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -10,7 +10,7 @@ type Config struct { Title string `json:"title"` DatabaseType string `json:"database_type"` DatabaseDsn string `json:"database_dsn"` - IDLength int `json:"id_length"` + NodeID int64 `json:"node_id"` PasswordLength int `json:"password_length"` ExpirationInterval int `json:"expiration_interval"` ListenAddress string `json:"listen_address"` @@ -36,7 +36,7 @@ func NewConfig() *Config { Title: "Coller", DatabaseType: "sqlite", DatabaseDsn: "collerd.db", - IDLength: 5, + NodeID: 1, PasswordLength: 16, ExpirationInterval: 60, // 1 minute ListenAddress: "0.0.0.0", @@ -88,8 +88,8 @@ func (c *Config) Check() error { } } - if c.IDLength <= 0 { - return fmt.Errorf("identifiers length must be greater than zero") + if c.NodeID < 0 || c.NodeID > 1023 { + return fmt.Errorf("node id must be between 0 and 1023") } if c.PasswordLength < internal.MIN_PASSWORD_LENGTH || c.PasswordLength > internal.MAX_PASSWORD_LENGTH { diff --git a/src/server/db.go b/src/server/db.go index 0186b72..e158618 100644 --- a/src/server/db.go +++ b/src/server/db.go @@ -7,11 +7,13 @@ import ( "strings" "time" - "git.riou.xyz/jriou/coller/internal" + "github.com/bwmarrin/snowflake" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" + + "git.riou.xyz/jriou/coller/internal" ) type Database struct { @@ -22,6 +24,7 @@ type Database struct { expiration int languages []string language string + node *snowflake.Node } var gconfig = &gorm.Config{ @@ -48,6 +51,11 @@ func NewDatabase(logger *slog.Logger, config *Config) (d *Database, err error) { logger.Debug("connected to the database") + node, err := snowflake.NewNode(config.NodeID) + if err != nil { + return nil, err + } + d = &Database{ logger: l, db: db, @@ -56,6 +64,7 @@ func NewDatabase(logger *slog.Logger, config *Config) (d *Database, err error) { expiration: config.Expiration, languages: internal.ToLowerStringSlice(config.Languages), language: strings.ToLower(config.Language), + node: node, } if err = d.UpdateSchema(); err != nil { @@ -132,6 +141,7 @@ func (d *Database) Create(content []byte, password string, encrypted bool, expir } note = &Note{ + ID: d.node.Generate().String(), Content: content, ExpiresAt: time.Now().Add(time.Duration(expiration) * time.Second), Encrypted: encrypted, diff --git a/src/server/note.go b/src/server/note.go index bff6b73..f6bf156 100644 --- a/src/server/note.go +++ b/src/server/note.go @@ -1,17 +1,11 @@ package server import ( - "fmt" "time" - "git.riou.xyz/jriou/coller/internal" "gorm.io/gorm" ) -const ID_MAX_RETRIES = 10 - -var idLength = 5 - type Note struct { ID string `json:"id" gorm:"primaryKey"` Content []byte `json:"content" gorm:"not null"` @@ -21,27 +15,8 @@ type Note struct { Language string `json:"language"` } -// Generate ID and compress content before saving to the database +// Compress content before saving to the database func (n *Note) BeforeCreate(trx *gorm.DB) (err error) { - for i := 0; i < ID_MAX_RETRIES; i++ { - if n.ID != "" { - continue - } - - id := internal.GenerateChars(idLength) - - var note Note - trx.Where("id = ?", id).Find(¬e) - - if note.ID == "" { - n.ID = id - continue - } - } - if n.ID == "" { - return fmt.Errorf("could not find unique id before creating the note") - } - n.Content = Compress(n.Content) return nil } diff --git a/src/server/server.go b/src/server/server.go index 0c9bae5..f64e2cc 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -41,10 +41,6 @@ func NewServer(logger *slog.Logger, db *Database, config *Config, version string }, nil } -func (s *Server) SetIDLength(length int) { - idLength = length -} - func (s *Server) SetPasswordLength(length int) { passwordLength = length }