feat: Use snowflake identifiers
All checks were successful
/ pre-commit (push) Successful in 1m7s

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"
"syscall"
"git.riou.xyz/jriou/coller/internal"
"golang.design/x/clipboard"
"golang.org/x/term"
"git.riou.xyz/jriou/coller/internal"
)
var (

View file

@ -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")

View file

@ -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 {

View file

@ -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

View file

@ -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=

View file

@ -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 {

View file

@ -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,

View file

@ -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(&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)
return nil
}

View file

@ -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
}