urapt is a self-hostable APT repository server with a companion CLI for pushing and managing Debian .deb packages. Server (urapt-server): - REST API + APT endpoint, SQLite storage (pure-Go modernc driver, no CGO) - .deb files stored content-addressed on disk, reference-counted for dedup - Server-managed RSA-4096 OpenPGP signing key (ProtonMail/go-crypto) - APT indices (Release/InRelease/Packages[.gz/.xz]) generated on demand from the DB, cached in memory, signed with the server key - Full APT model: repositories -> distributions -> components -> architectures - Bearer-token auth for REST; HTTP Basic auth for private-repo APT reads - First registrant becomes admin; repo-scoped permissions (read/write/read-write/admin) plus owner and server-admin roles - Multipart package push with control-field extraction, list/show/delete, pool serving, blob ref-count cleanup - Audit log CLI (urapt): - register/login/logout/whoami, token management - repo/distro/component/arch CRUD, member management - push/pull/ls/show/rm for packages - apt-config helper that emits apt setup commands (key, sources.list, auth.conf for private repos) Packaging & docs: - Dockerfile (multi-stage distroless), docker-compose.yml, sample config - README quick start, architecture overview, config reference, security notes - PLAN.md design blueprint, CHANGELOG.md, GPL-3.0 LICENSE - GitHub Actions CI (test, lint, cross-build for linux/darwin amd64/arm64) - Makefile release target producing static binaries + tarballs + checksums Tests cover the data-access layer, auth/permission checks, APT index generation, .deb parsing, GPG signing, the REST API, and the typed API client. Verified end-to-end on a Raspberry Pi (arm64) pushing and installing a real package.
81 lines
2.7 KiB
Go
81 lines
2.7 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// GetBlob returns a blob by its sha256, or ErrNotFound.
|
|
func (s *Store) GetBlob(ctx context.Context, sha256 string) (filename string, size, refCount int64, err error) {
|
|
err = s.db.QueryRowContext(ctx, `SELECT filename, size, ref_count FROM blobs WHERE sha256 = ?`, sha256).
|
|
Scan(&filename, &size, &refCount)
|
|
if isErrNoRows(err) {
|
|
return "", 0, 0, ErrNotFound
|
|
}
|
|
return filename, size, refCount, err
|
|
}
|
|
|
|
// CreateBlob creates a new blob row with ref_count=1. It returns
|
|
// (created=true) when a new row was inserted, or (created=false) when the
|
|
// blob already existed (in which case its ref_count is left unchanged here;
|
|
// use IncBlobRef to bump it).
|
|
func (s *Store) CreateBlob(ctx context.Context, sha256, filename string, size int64) (created bool, err error) {
|
|
now := s.now()
|
|
res, err := s.exec(ctx, `INSERT OR IGNORE INTO blobs (sha256, filename, size, ref_count, created_at) VALUES (?, ?, ?, 1, ?)`,
|
|
sha256, filename, size, now)
|
|
if err != nil {
|
|
return false, fmt.Errorf("insert blob: %w", err)
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
return n > 0, nil
|
|
}
|
|
|
|
// IncBlobRef atomically increments a blob's ref_count and returns the new value.
|
|
func (s *Store) IncBlobRef(ctx context.Context, sha256 string) (int64, error) {
|
|
res, err := s.exec(ctx, `UPDATE blobs SET ref_count = ref_count + 1 WHERE sha256 = ?`, sha256)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("inc blob: %w", err)
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return 0, ErrNotFound
|
|
}
|
|
var rc int64
|
|
if err := s.db.QueryRowContext(ctx, `SELECT ref_count FROM blobs WHERE sha256 = ?`, sha256).Scan(&rc); err != nil {
|
|
return 0, err
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
// DecBlobRef atomically decrements a blob's ref_count and returns the new
|
|
// value. When it reaches 0 the caller should delete the on-disk file and call
|
|
// DeleteBlob.
|
|
func (s *Store) DecBlobRef(ctx context.Context, sha256 string) (int64, error) {
|
|
res, err := s.exec(ctx, `UPDATE blobs SET ref_count = ref_count - 1 WHERE sha256 = ? AND ref_count > 0`, sha256)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("dec blob: %w", err)
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
var rc int64
|
|
if e := s.db.QueryRowContext(ctx, `SELECT ref_count FROM blobs WHERE sha256 = ?`, sha256).Scan(&rc); e != nil {
|
|
if isErrNoRows(e) {
|
|
return 0, ErrNotFound
|
|
}
|
|
return 0, e
|
|
}
|
|
return rc, nil
|
|
}
|
|
var rc int64
|
|
if err := s.db.QueryRowContext(ctx, `SELECT ref_count FROM blobs WHERE sha256 = ?`, sha256).Scan(&rc); err != nil {
|
|
return 0, err
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
// DeleteBlob removes a blob row.
|
|
func (s *Store) DeleteBlob(ctx context.Context, sha256 string) error {
|
|
_, err := s.exec(ctx, `DELETE FROM blobs WHERE sha256 = ?`, sha256)
|
|
return err
|
|
}
|