1
0
Fork 0
forked from jriou/coller

feat: Use snowflake identifiers

Fixes #29.

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-09-20 08:37:16 +02:00
commit 2c3ca08dbf
Signed by: jriou
GPG key ID: 9A099EDA51316854
9 changed files with 24 additions and 39 deletions

View file

@ -13,9 +13,10 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"git.riou.xyz/jriou/coller/internal"
"golang.design/x/clipboard" "golang.design/x/clipboard"
"golang.org/x/term" "golang.org/x/term"
"git.riou.xyz/jriou/coller/internal"
) )
var ( var (

View file

@ -18,7 +18,7 @@ The file format is **JSON**:
* **title** (string): Title of the website * **title** (string): Title of the website
* **database_type** (string): Type of the database (default "sqlite", "postgres" also supported) * **database_type** (string): Type of the database (default "sqlite", "postgres" also supported)
* **database_dsn** (string): Connection string for the database (default "collerd.db") * **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) * **password_length** (int): Number of characters for generated passwords (default 16)
* **expiration_interval** (int): Number of seconds to wait between two expiration runs * **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") * **listen_address** (string): Address to listen for the web server (default "0.0.0.0")

View file

@ -5,9 +5,10 @@ import (
"log/slog" "log/slog"
"os" "os"
"github.com/prometheus/client_golang/prometheus"
"git.riou.xyz/jriou/coller/internal" "git.riou.xyz/jriou/coller/internal"
"git.riou.xyz/jriou/coller/server" "git.riou.xyz/jriou/coller/server"
"github.com/prometheus/client_golang/prometheus"
) )
var ( var (
@ -69,7 +70,6 @@ func handleMain() int {
return internal.ReturnError(logger, "could not create server", err) return internal.ReturnError(logger, "could not create server", err)
} }
srv.SetIDLength(config.IDLength)
srv.SetPasswordLength(config.PasswordLength) srv.SetPasswordLength(config.PasswordLength)
if config.EnableMetrics { if config.EnableMetrics {

View file

@ -17,6 +17,7 @@ require (
require ( require (
github.com/beorn7/perks v1.0.1 // indirect 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/cespare/xxhash/v2 v2.3.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect

View file

@ -1,5 +1,7 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -10,7 +10,7 @@ type Config struct {
Title string `json:"title"` Title string `json:"title"`
DatabaseType string `json:"database_type"` DatabaseType string `json:"database_type"`
DatabaseDsn string `json:"database_dsn"` DatabaseDsn string `json:"database_dsn"`
IDLength int `json:"id_length"` NodeID int64 `json:"node_id"`
PasswordLength int `json:"password_length"` PasswordLength int `json:"password_length"`
ExpirationInterval int `json:"expiration_interval"` ExpirationInterval int `json:"expiration_interval"`
ListenAddress string `json:"listen_address"` ListenAddress string `json:"listen_address"`
@ -36,7 +36,7 @@ func NewConfig() *Config {
Title: "Coller", Title: "Coller",
DatabaseType: "sqlite", DatabaseType: "sqlite",
DatabaseDsn: "collerd.db", DatabaseDsn: "collerd.db",
IDLength: 5, NodeID: 1,
PasswordLength: 16, PasswordLength: 16,
ExpirationInterval: 60, // 1 minute ExpirationInterval: 60, // 1 minute
ListenAddress: "0.0.0.0", ListenAddress: "0.0.0.0",
@ -88,8 +88,8 @@ func (c *Config) Check() error {
} }
} }
if c.IDLength <= 0 { if c.NodeID < 0 || c.NodeID > 1023 {
return fmt.Errorf("identifiers length must be greater than zero") return fmt.Errorf("node id must be between 0 and 1023")
} }
if c.PasswordLength < internal.MIN_PASSWORD_LENGTH || c.PasswordLength > internal.MAX_PASSWORD_LENGTH { if c.PasswordLength < internal.MIN_PASSWORD_LENGTH || c.PasswordLength > internal.MAX_PASSWORD_LENGTH {

View file

@ -7,11 +7,13 @@ import (
"strings" "strings"
"time" "time"
"git.riou.xyz/jriou/coller/internal" "github.com/bwmarrin/snowflake"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"git.riou.xyz/jriou/coller/internal"
) )
type Database struct { type Database struct {
@ -22,6 +24,7 @@ type Database struct {
expiration int expiration int
languages []string languages []string
language string language string
node *snowflake.Node
} }
var gconfig = &gorm.Config{ 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") logger.Debug("connected to the database")
node, err := snowflake.NewNode(config.NodeID)
if err != nil {
return nil, err
}
d = &Database{ d = &Database{
logger: l, logger: l,
db: db, db: db,
@ -56,6 +64,7 @@ func NewDatabase(logger *slog.Logger, config *Config) (d *Database, err error) {
expiration: config.Expiration, expiration: config.Expiration,
languages: internal.ToLowerStringSlice(config.Languages), languages: internal.ToLowerStringSlice(config.Languages),
language: strings.ToLower(config.Language), language: strings.ToLower(config.Language),
node: node,
} }
if err = d.UpdateSchema(); err != nil { if err = d.UpdateSchema(); err != nil {
@ -132,6 +141,7 @@ func (d *Database) Create(content []byte, password string, encrypted bool, expir
} }
note = &Note{ note = &Note{
ID: d.node.Generate().String(),
Content: content, Content: content,
ExpiresAt: time.Now().Add(time.Duration(expiration) * time.Second), ExpiresAt: time.Now().Add(time.Duration(expiration) * time.Second),
Encrypted: encrypted, Encrypted: encrypted,

View file

@ -1,17 +1,11 @@
package server package server
import ( import (
"fmt"
"time" "time"
"git.riou.xyz/jriou/coller/internal"
"gorm.io/gorm" "gorm.io/gorm"
) )
const ID_MAX_RETRIES = 10
var idLength = 5
type Note struct { type Note struct {
ID string `json:"id" gorm:"primaryKey"` ID string `json:"id" gorm:"primaryKey"`
Content []byte `json:"content" gorm:"not null"` Content []byte `json:"content" gorm:"not null"`
@ -21,27 +15,8 @@ type Note struct {
Language string `json:"language"` 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) { 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(&note)
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) n.Content = Compress(n.Content)
return nil return nil
} }

View file

@ -41,10 +41,6 @@ func NewServer(logger *slog.Logger, db *Database, config *Config, version string
}, nil }, nil
} }
func (s *Server) SetIDLength(length int) {
idLength = length
}
func (s *Server) SetPasswordLength(length int) { func (s *Server) SetPasswordLength(length int) {
passwordLength = length passwordLength = length
} }