118 lines
2.6 KiB
Go
118 lines
2.6 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
package storage
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// ErrNotFound is returned by Get when no device row matches the given id.
|
|
var ErrNotFound = errors.New("device not found")
|
|
|
|
type Device struct {
|
|
ID string
|
|
FCMToken string
|
|
PublicKey []byte // 32-byte X25519
|
|
CreatedAt time.Time
|
|
LastSeenAt time.Time
|
|
}
|
|
|
|
type Store struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func Open(path string) (*Store, error) {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := db.Ping(); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := db.Exec(schema); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Store{db: db}, nil
|
|
}
|
|
|
|
const schema = `
|
|
CREATE TABLE IF NOT EXISTS devices (
|
|
id TEXT PRIMARY KEY,
|
|
fcm_token TEXT NOT NULL,
|
|
public_key BLOB NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL
|
|
);
|
|
`
|
|
|
|
// Get returns the device row for id, or ErrNotFound if no row exists.
|
|
func (s *Store) Get(id string) (*Device, error) {
|
|
var d Device
|
|
var created, lastSeen int64
|
|
err := s.db.QueryRow(
|
|
`SELECT id, fcm_token, public_key, created_at, last_seen_at FROM devices WHERE id = ?`,
|
|
id,
|
|
).Scan(&d.ID, &d.FCMToken, &d.PublicKey, &created, &lastSeen)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.CreatedAt = time.Unix(created, 0)
|
|
d.LastSeenAt = time.Unix(lastSeen, 0)
|
|
return &d, nil
|
|
}
|
|
|
|
func (s *Store) Upsert(d Device) error {
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO devices (id, fcm_token, public_key, created_at, last_seen_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
fcm_token = excluded.fcm_token,
|
|
public_key = excluded.public_key,
|
|
last_seen_at = excluded.last_seen_at`,
|
|
d.ID, d.FCMToken, d.PublicKey, d.CreatedAt.Unix(), d.LastSeenAt.Unix(),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) ListAll() ([]Device, error) {
|
|
rows, err := s.db.Query(`SELECT id, fcm_token, public_key, created_at, last_seen_at FROM devices`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var devices []Device
|
|
for rows.Next() {
|
|
var d Device
|
|
var created, lastSeen int64
|
|
if err := rows.Scan(&d.ID, &d.FCMToken, &d.PublicKey, &created, &lastSeen); err != nil {
|
|
return nil, err
|
|
}
|
|
d.CreatedAt = time.Unix(created, 0)
|
|
d.LastSeenAt = time.Unix(lastSeen, 0)
|
|
devices = append(devices, d)
|
|
}
|
|
return devices, rows.Err()
|
|
}
|
|
|
|
func (s *Store) Delete(id string) error {
|
|
res, err := s.db.Exec(`DELETE FROM devices WHERE id = ?`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) Close() error {
|
|
return s.db.Close()
|
|
}
|