refactor(project): add external abstractions

This commit is contained in:
Dustin Stiles 2025-03-21 20:23:15 -04:00
parent 30f031fe1c
commit 1bfada68df
Signed by: duwstiles
GPG Key ID: BCD9912EC231FC87
13 changed files with 1026 additions and 314 deletions

143
cmd/dsfxctl/client/app.go Normal file
View File

@ -0,0 +1,143 @@
package client
import (
"context"
"crypto/ed25519"
"encoding/base64"
"errors"
"log/slog"
"net"
"koti.casa/numenor-labs/dsfx/cmd/dsfxctl/conf"
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
"koti.casa/numenor-labs/dsfx/pkg/disk"
"koti.casa/numenor-labs/dsfx/pkg/logging"
"koti.casa/numenor-labs/dsfx/pkg/network"
"koti.casa/numenor-labs/dsfx/pkg/storage/scoped"
"koti.casa/numenor-labs/dsfx/pkg/system"
)
// Client represents the client application for dsfxctl.
type Client struct {
// resources
disk disk.Disk
system system.System
// configuration
conf conf.Conf
// storage scopes
configScope disk.Disk
}
// New creates a new Client instance with the provided disk, system, and
// configuration.
func New(disk disk.Disk, system system.System) *Client {
conf := conf.FromSystem(system)
return &Client{
// resources
disk: disk,
system: system,
// configuration
conf: conf,
// storage scopes
configScope: scoped.New(disk, conf.ConfigDir),
}
}
// Run executes the main logic of the application.
func (a *Client) Run(ctx context.Context) error {
// ---------------------------------------------------------------------------
// Logger
opts := &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelDebug,
}
logger := slog.New(slog.NewTextHandler(a.system.Stdout(), opts))
// Everything in the application will attempt to use the logger in stored in
// the context, but we also set the default with slog as a fallback. In cases
// where the context is not available, or the context is not a child of the
// context with the logger, the default logger will be used.
slog.SetDefault(logger)
ctx = logging.WithContext(ctx, logger)
keyFile, err := a.configScope.Open("key")
if err != nil {
logger.WarnContext(ctx, "key file is missing, reinitializing")
logger.WarnContext(ctx, "if this is your first time running dsfxctl, you can ignore this")
}
if keyFile == nil {
logger.InfoContext(ctx, "generating new key")
keyFile, err = a.configScope.Create("key")
if err != nil {
logger.ErrorContext(ctx, "failed to create key file", slog.Any("error", err))
return err
}
privkey, err := identity.Generate()
if err != nil {
logger.ErrorContext(ctx, "failed to generate key", slog.Any("error", err))
return err
}
_, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
if err != nil {
logger.ErrorContext(ctx, "failed to write key", slog.Any("error", err))
return err
}
}
defer keyFile.Close()
keyRaw := make([]byte, ed25519.PrivateKeySize)
n, err := keyFile.Read(keyRaw)
if err != nil {
logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err))
return err
}
if n != ed25519.PrivateKeySize {
logger.ErrorContext(ctx, "key file is not the correct size", slog.Int("size", n))
return err
}
id := ed25519.PrivateKey(keyRaw)
laddr := network.NewAddr(
net.ParseIP("0.0.0.0"),
0, // port 0 means any available port
identity.ToPublicKey(id),
)
logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String()))
switch a.system.Arg(0) {
case "test":
raddrRaw := a.system.Arg(1)
if raddrRaw == "" {
logger.ErrorContext(ctx, "no remote address provided")
return err
}
testConnection(ctx, id, laddr, raddrRaw)
case "":
return errors.New("no command provided")
default:
return errors.New("unknown command: " + a.system.Arg(0))
}
return nil
}
func testConnection(ctx context.Context, id ed25519.PrivateKey, laddr *network.Addr, raddrRaw string) {
logger := logging.FromContext(context.Background())
raddr, err := network.ParseAddr(raddrRaw)
if err != nil {
logger.ErrorContext(ctx, "failed to parse server address", slog.Any("error", err))
return
}
conn, err := network.Dial(ctx, id, laddr, raddr)
if err != nil {
logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err))
return
}
defer conn.Close()
}

34
cmd/dsfxctl/conf/conf.go Normal file
View File

@ -0,0 +1,34 @@
package conf
import "koti.casa/numenor-labs/dsfx/pkg/system"
const (
// DefaultConfigDir is the default directory for the dsfxctl configuration.
DefaultConfigDir = "/etc/dsfxctl"
// DefaultHost is the default host for the dsfxctl application.
DefaultHost = "0.0.0.0"
)
// Conf holds the configuration for the dsfxctl application.
type Conf struct {
// Directories
ConfigDir string
// Networking
Host string
}
func FromSystem(sys system.System) Conf {
var c Conf
c.ConfigDir = sys.GetEnv("DSFXCTL_CONFIG_DIR")
if c.ConfigDir == "" {
c.ConfigDir = DefaultConfigDir
}
c.Host = sys.GetEnv("DSFXCTL_HOST")
if c.Host == "" {
c.Host = DefaultHost
}
return c
}

View File

@ -2,141 +2,17 @@ package main
import (
"context"
"crypto/ed25519"
"encoding/base64"
"errors"
"flag"
"fmt"
"log/slog"
"net"
"os"
"path/filepath"
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
"koti.casa/numenor-labs/dsfx/pkg/logging"
"koti.casa/numenor-labs/dsfx/pkg/network"
"koti.casa/numenor-labs/dsfx/cmd/dsfxctl/client"
"koti.casa/numenor-labs/dsfx/pkg/disk"
"koti.casa/numenor-labs/dsfx/pkg/system"
)
func main() {
ctx := context.Background()
c := client.New(disk.Default(), system.Default())
// ---------------------------------------------------------------------------
// Logger
opts := &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelDebug,
}
logger := slog.New(slog.NewTextHandler(os.Stdout, opts))
// Everything in the application will attempt to use the logger in stored in
// the context, but we also set the default with slog as a fallback. In cases
// where the context is not available, or the context is not a child of the
// context with the logger, the default logger will be used.
slog.SetDefault(logger)
ctx = logging.WithContext(ctx, logger)
homedir, err := os.UserHomeDir()
err := c.Run(context.Background())
if err != nil {
logger.ErrorContext(ctx, "failed to get home directory", slog.Any("error", err))
os.Exit(1)
}
flagDir := flag.String("dir", filepath.Join(homedir, ".dsfxctl"), "data directory")
err = os.Mkdir(*flagDir, 0777)
if errors.Is(err, os.ErrExist) {
// If the directory already exists, we can ignore the error.
err = nil
}
if err != nil {
logger.ErrorContext(ctx, "failed to create directory", slog.Any("error", err))
os.Exit(1)
}
keyFile, err := os.Open(filepath.Join(*flagDir, "key"))
if err != nil {
logger.WarnContext(ctx, "key file is missing, reinitializing")
logger.WarnContext(ctx, "if this is your first time running dsfxctl, you can ignore this")
}
if keyFile == nil {
logger.InfoContext(ctx, "generating new key")
keyFile, err = os.Create(filepath.Join(*flagDir, "key"))
if err != nil {
logger.ErrorContext(ctx, "failed to create key file", slog.Any("error", err))
os.Exit(1)
}
privkey, err := identity.Generate()
if err != nil {
logger.ErrorContext(ctx, "failed to generate key", slog.Any("error", err))
os.Exit(1)
}
_, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
if err != nil {
logger.ErrorContext(ctx, "failed to write key", slog.Any("error", err))
os.Exit(1)
}
}
defer keyFile.Close()
// ---------------------------------------------------------------------------
// Commands
flag.Parse()
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: dsfxctl [command] [args]\n")
fmt.Fprintf(os.Stderr, "Commands:\n")
fmt.Fprintf(os.Stderr, " test <remote_addr> Test the connection to the server\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
}
id, err := identity.LoadSigningKeyFromFile(filepath.Join(*flagDir, "key"))
if err != nil {
logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err))
os.Exit(1)
}
laddr := network.NewAddr(
net.ParseIP("0.0.0.0"),
0, // port 0 means any available port
identity.ToPublicKey(id),
)
logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String()))
switch flag.Arg(0) {
case "test":
raddrRaw := flag.Arg(1)
if raddrRaw == "" {
logger.ErrorContext(ctx, "no remote address provided")
os.Exit(1)
}
testConnection(ctx, id, laddr, raddrRaw)
case "":
logger.InfoContext(ctx, "no command provided")
os.Exit(1)
default:
logger.InfoContext(ctx, "unknown command")
os.Exit(1)
panic(err)
}
}
func testConnection(ctx context.Context, id ed25519.PrivateKey, laddr *network.Addr, raddrRaw string) {
logger := logging.FromContext(context.Background())
raddr, err := network.ParseAddr(raddrRaw)
if err != nil {
logger.ErrorContext(ctx, "failed to parse server address", slog.Any("error", err))
return
}
conn, err := network.Dial(ctx, id, laddr, raddr)
if err != nil {
logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err))
return
}
defer conn.Close()
}

42
cmd/dsfxnode/conf/conf.go Normal file
View File

@ -0,0 +1,42 @@
package conf
import "koti.casa/numenor-labs/dsfx/pkg/system"
const (
// DefaultConfigDir is the default directory for the dsfxctl configuration.
DefaultConfigDir = "/etc/dsfxnode/config"
// DefaultHost is the default host for the dsfxctl application.
DefaultHost = "0.0.0.0"
// DefaultPort is the default port for the dsfxctl application.
DefaultPort = "8000"
)
// Conf holds the configuration for the dsfxctl application.
type Conf struct {
// Directories
ConfigDir string
// Networking
Host string
Port string
}
func FromSystem(sys system.System) Conf {
var c Conf
c.ConfigDir = sys.GetEnv("DSFXCTL_CONFIG_DIR")
if c.ConfigDir == "" {
c.ConfigDir = DefaultConfigDir
}
c.Host = sys.GetEnv("DSFXCTL_HOST")
if c.Host == "" {
c.Host = DefaultHost
}
c.Port = sys.GetEnv("DSFXCTL_PORT")
if c.Port == "" {
c.Port = DefaultPort
}
return c
}

View File

@ -2,197 +2,29 @@ package main
import (
"context"
"encoding/base64"
"errors"
"flag"
"fmt"
"log/slog"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
"koti.casa/numenor-labs/dsfx/pkg/logging"
"koti.casa/numenor-labs/dsfx/pkg/network"
"koti.casa/numenor-labs/dsfx/cmd/dsfxnode/node"
"koti.casa/numenor-labs/dsfx/pkg/disk"
"koti.casa/numenor-labs/dsfx/pkg/storage/scoped"
"koti.casa/numenor-labs/dsfx/pkg/system"
)
var ()
func main() {
ctx := context.Background()
// ---------------------------------------------------------------------------
// Logger
homedir, err := os.UserHomeDir()
sys := system.Default()
configDir := sys.GetEnv("DSFXNODE_CONFIG_DIR")
if configDir == "" {
configDir = "/etc/dsfxnode/config"
}
configScope := scoped.New(disk.Default(), configDir)
err := node.New(configScope, sys).Run(ctx)
if err != nil {
logUserMsg("error: failed to get home directory\n")
logUserMsg("error: are you on a real computer?\n")
// Look up why this function might fail...
os.Exit(1)
}
flagHost := flag.String("host", "localhost", "the host to listen on")
flagPort := flag.Int("port", 8000, "the port to listen on")
flagDir := flag.String("dir", filepath.Join(homedir, ".dsfxnode"), "the directory to store data in")
flagLogFile := flag.String("log", "logs", "the file to write logs to, relative to -dir")
flag.Parse()
dataDir := *flagDir
err = os.Mkdir(dataDir, 0777)
if errors.Is(err, os.ErrExist) {
// If the directory already exists, we can ignore the error.
err = nil
}
if err != nil {
logUserMsg("error: failed to create the data directory: %v\n", err)
os.Exit(1)
}
var logFile *os.File
if *flagLogFile == "stdout" {
logFile = os.Stdout
} else {
logFilePath := filepath.Join(dataDir, *flagLogFile)
logFile, err = os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
logUserMsg("warn: log file missing, making a new one: %s\n", logFilePath)
logFile, err = os.Create(logFilePath)
if err != nil {
logUserMsg("error: failed to create log file: %v\n", err)
os.Exit(1)
}
}
}
defer logFile.Close()
opts := &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelDebug,
}
logger := slog.New(slog.NewTextHandler(logFile, opts))
// Everything in the application will attempt to use the logger in stored in
// the context, but we also set the default with slog as a fallback. In cases
// where the context is not available, or the context is not a child of the
// context with the logger, the default logger will be used.
slog.SetDefault(logger)
ctx = logging.WithContext(ctx, logger)
keyFile, err := os.Open(filepath.Join(dataDir, "key"))
if err != nil {
logUserMsg("warn: key file is missing, making a new one\n")
logUserMsg("warn: if this is your first time running dsfxctl, you can ignore this\n")
keyFile, err = os.Create(filepath.Join(dataDir, "key"))
if err != nil {
logUserMsg("error: failed to create key file: %v\n", err)
os.Exit(1)
}
privkey, err := identity.Generate()
if err != nil {
logUserMsg("error: failed to generate key: %v\n", err)
os.Exit(1)
}
_, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
if err != nil {
logUserMsg("error: failed to write key: %v\n", err)
os.Exit(1)
}
}
defer keyFile.Close()
// ---------------------------------------------------------------------------
// Flags
masterKey, err := identity.LoadSigningKeyFromFile(filepath.Join(dataDir, "key"))
if err != nil {
logUserMsg("error: failed to load key: %v\n", err)
os.Exit(1)
}
var admins []string
adminsFile, err := os.ReadFile(filepath.Join(dataDir, "admins"))
if err != nil {
logUserMsg("warn: failed to read admins file.. no one will be able to control this server..\n")
logUserMsg(`
If you aren't sure what this means, here's a brief explanation:
The admins file is a list of public keys that are allowed to control
the server. The base are base64 encoded and separated by newlines.
The application looks for this file at <dir>/admins, where <dir> is
the directory you specified with the -dir flag when starting the
system. By default, this is ~/.dsfxnode.
When you run the dsfxctl command, it will generate a key for you and
print it to the console. You can copy this key and paste it into the
admins file to give yourself control over the server.
`)
logUserMsg("\n")
}
if adminsFile != nil {
rawAdmins := strings.Split(string(adminsFile), "\n")
for _, admin := range rawAdmins {
if admin != "" {
admins = append(admins, admin)
}
}
logUserMsg("info: loaded admins: %v\n", admins)
}
tcpAddrRaw := net.JoinHostPort(*flagHost, fmt.Sprint(*flagPort))
tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
if err != nil {
logUserMsg("warn: invalid host or port: %v\n", err)
os.Exit(1)
}
addr := network.FromTCPAddr(tcpAddr, identity.ToPublicKey(masterKey))
// ---------------------------------------------------------------------------
// Listener
listener, err := network.Listen(ctx, masterKey, addr)
if err != nil {
logger.ErrorContext(ctx, "listener error", slog.Any("error", err))
os.Exit(1)
}
logUserMsg("info: listener created\n")
logUserMsg(">> Operating System: %s\n", runtime.GOOS)
logUserMsg(">> Architecture: %s\n", runtime.GOARCH)
logUserMsg(">> CPU Cores: %d\n", runtime.NumCPU())
logUserMsg(">> Public Address: %s\n", addr.String())
for {
conn, err := listener.Accept()
if err != nil {
logger.ErrorContext(ctx, "accept failure", slog.Any("error", err))
continue
}
go handleConnection(ctx, conn)
// Log the error and exit with a non-zero status code.
slog.Error("Error running dsfxnode", slog.Any("error", err))
sys.Exit(1)
}
}
func handleConnection(ctx context.Context, conn net.Conn) error {
defer conn.Close()
logger := logging.FromContext(ctx)
msg := make([]byte, 1024)
n, err := conn.Read(msg)
if err != nil {
logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err))
return err
}
logger.InfoContext(ctx, "received msg", slog.Int("bytes", n))
return nil
}
func logUserMsg(msg string, args ...any) {
fmt.Fprintf(os.Stderr, msg, args...)
}

243
cmd/dsfxnode/node/node.go Normal file
View File

@ -0,0 +1,243 @@
package node
import (
"context"
"crypto/ed25519"
"encoding/base64"
"fmt"
"log/slog"
"net"
"os"
"strings"
"koti.casa/numenor-labs/dsfx/cmd/dsfxnode/conf"
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
"koti.casa/numenor-labs/dsfx/pkg/disk"
"koti.casa/numenor-labs/dsfx/pkg/logging"
"koti.casa/numenor-labs/dsfx/pkg/network"
"koti.casa/numenor-labs/dsfx/pkg/system"
)
type Node struct {
config disk.Disk
system system.System
conf conf.Conf
}
func New(config disk.Disk, system system.System) *Node {
conf := conf.FromSystem(system)
return &Node{config, system, conf}
}
func (a *Node) Run(ctx context.Context) error {
opts := &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelDebug,
}
logger := slog.New(slog.NewJSONHandler(a.system.Stdout(), opts))
// Everything in the application will attempt to use the logger in stored in
// the context, but we also set the default with slog as a fallback. In cases
// where the context is not available, or the context is not a child of the
// context with the logger, the default logger will be used.
slog.SetDefault(logger)
ctx = logging.WithContext(ctx, logger)
ki := &KeyInit{disk: a.config}
// Check if the key file exists and is not empty
hasKey, err := ki.Has()
if err != nil {
logger.ErrorContext(ctx, "failed to check key file", slog.Any("error", err))
return err
}
if !hasKey {
logger.InfoContext(ctx, "key file does not exist or is empty, generating new key")
err = ki.Init()
if err != nil {
logger.ErrorContext(ctx, "failed to initialize key file", slog.Any("error", err))
return err
}
}
// Read the key file
id, err := ki.Read()
if err != nil {
logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err))
return err
}
admins := []string{}
ai := &AdminInit{disk: a.config}
// Check if the admins file exists and is not empty
hasAdmins, err := ai.Has()
if err != nil {
logger.ErrorContext(ctx, "failed to check admins file", slog.Any("error", err))
return err
}
if hasAdmins {
logger.InfoContext(ctx, "admins file exists, reading admins")
admins, err = ai.Read()
if err != nil {
logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err))
return err
}
logger.InfoContext(ctx, "loaded admins", slog.Any("admins", admins))
} else {
logger.WarnContext(ctx, "admins file does not exist or is empty, no admins will be loaded")
}
tcpAddrRaw := net.JoinHostPort(a.conf.Host, a.conf.Port)
tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
if err != nil {
return err
}
addr := network.FromTCPAddr(tcpAddr, id.Public().(ed25519.PublicKey))
// ---------------------------------------------------------------------------
// Listener
listener, err := network.Listen(ctx, id, addr)
if err != nil {
logger.ErrorContext(ctx, "listener error", slog.Any("error", err))
return err
}
for {
conn, err := listener.Accept()
if err != nil {
logger.ErrorContext(ctx, "accept failure", slog.Any("error", err))
continue
}
go handleConnection(ctx, conn)
}
}
func handleConnection(ctx context.Context, conn net.Conn) error {
defer conn.Close()
logger := logging.FromContext(ctx)
msg := make([]byte, 1024)
n, err := conn.Read(msg)
if err != nil {
logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err))
return err
}
logger.InfoContext(ctx, "received msg", slog.Int("bytes", n))
return nil
}
type KeyInit struct{ disk disk.Disk }
func (ki *KeyInit) Has() (bool, error) {
f, err := ki.disk.Open("key")
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
defer f.Close()
stats, err := f.Stat()
if err != nil {
return false, err
}
return stats.Size() > 0, nil
}
func (ki *KeyInit) Init() error {
f, err := ki.disk.Create("key")
if err != nil {
return err
}
defer f.Close()
privkey, err := identity.Generate()
if err != nil {
return err
}
_, err = f.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
if err != nil {
return err
}
return nil
}
func (ki *KeyInit) Read() (ed25519.PrivateKey, error) {
f, err := ki.disk.Open("key")
if err != nil {
return nil, err
}
defer f.Close()
keyRaw := make([]byte, ed25519.PrivateKeySize)
n, err := f.Read(keyRaw)
if err != nil {
return nil, err
}
if n != ed25519.PrivateKeySize {
return nil, fmt.Errorf("key file is not the correct size: %d", n)
}
return ed25519.PrivateKey(keyRaw), nil
}
type AdminInit struct {
disk disk.Disk
}
func (ai *AdminInit) Has() (bool, error) {
f, err := ai.disk.Open("admins")
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
defer f.Close()
stats, err := f.Stat()
if err != nil {
return false, err
}
return stats.Size() > 0, nil
}
func (ai *AdminInit) Read() ([]string, error) {
f, err := ai.disk.Open("admins")
if err != nil {
return nil, err
}
defer f.Close()
adminsRaw := make([]byte, 0)
n, err := f.Read(adminsRaw)
if err != nil {
return nil, err
}
if n == 0 {
return nil, fmt.Errorf("admins file is empty")
}
rawAdmins := strings.Split(string(adminsRaw), "\n")
var admins []string
for _, admin := range rawAdmins {
if admin != "" {
admins = append(admins, admin)
}
}
return admins, nil
}

64
pkg/disk/default.go Normal file
View File

@ -0,0 +1,64 @@
package disk
import (
"io/fs"
"log/slog"
"os"
"time"
)
type osDisk struct {
logger *slog.Logger
}
// Default returns a new instance of the defaultDisk type, which implements
// the Disk interface. This is the default implementation for file operations
// using the standard os package.
func Default() Disk {
return &osDisk{logger: slog.Default()}
}
// Create implements Disk.
func (d *osDisk) Create(name string) (File, error) {
ts := time.Now()
file, err := os.Create(name)
el := time.Now().Sub(ts).String()
d.logger.Debug("(io) disk.Create", "duration", el)
return file, err
}
// Mkdir implements Disk.
func (d *osDisk) Mkdir(name string, perm fs.FileMode) error {
ts := time.Now()
err := os.Mkdir(name, perm)
el := time.Now().Sub(ts).String()
d.logger.Debug("(io) disk.Mkdir", "duration", el)
return err
}
// Open implements Disk.
func (d *osDisk) Open(name string) (File, error) {
ts := time.Now()
file, err := os.Open(name)
el := time.Now().Sub(ts).String()
d.logger.Debug("(io) disk.Open", "duration", el)
return file, err
}
// Remove implements Disk.
func (d *osDisk) Remove(name string) error {
ts := time.Now()
err := os.Remove(name)
el := time.Now().Sub(ts).String()
d.logger.Debug("(io) disk.Remove", "duration", el)
return err
}
// Stat implements Disk.
func (d *osDisk) Stat(name string) (fs.FileInfo, error) {
ts := time.Now()
info, err := os.Stat(name)
el := time.Now().Sub(ts).String()
d.logger.Debug("(io) disk.Stat", "duration", el)
return info, err
}

57
pkg/disk/default_test.go Normal file
View File

@ -0,0 +1,57 @@
package disk_test
import (
"testing"
"koti.casa/numenor-labs/dsfx/pkg/disk"
)
func TestDefaultDisk(t *testing.T) {
// Create a new disk instance using the default implementation
d := disk.Default()
// Test creating a directory
dirName := "testdir"
if err := d.Mkdir(dirName, 0755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
// Test creating a file
fileName := "testfile.txt"
file, err := d.Create(fileName)
if err != nil {
t.Fatalf("Failed to create file: %v", err)
}
defer file.Close()
// Test writing to the file
if _, err := file.Write([]byte("Hello, World!")); err != nil {
t.Fatalf("Failed to write to file: %v", err)
}
// Test statting the file
info, err := d.Stat(fileName)
if err != nil {
t.Fatalf("Failed to stat file: %v", err)
}
if info.Name() != fileName {
t.Errorf("Expected file name %s, got %s", fileName, info.Name())
}
// Test opening the file
openFile, err := d.Open(fileName)
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer openFile.Close()
// Test removing the file
if err := d.Remove(fileName); err != nil {
t.Fatalf("Failed to remove file: %v", err)
}
// Test removing the directory
if err := d.Remove(dirName); err != nil {
t.Fatalf("Failed to remove directory: %v", err)
}
}

137
pkg/disk/disk.go Normal file
View File

@ -0,0 +1,137 @@
package disk
import (
"io"
"io/fs"
"time"
)
// File interface extends the fs.File interface to include io.Writer.
// This is because the standard fs package is modeled after a read-only File
// System, but we need to write to files as well. The standard os.File type will
// satisfy this interface, as it implements both fs.File and io.Writer.
type File interface {
fs.File
io.Writer
}
// Disk interface defines the methods for a disk abstraction layer. This allows
// us to easily create mock disks for testing purposes, or to simulate different
// levels of failure and latency in a simuated environment.
type Disk interface {
// Mkdir creates a directory with the specified name and permissions.
Mkdir(name string, perm fs.FileMode) error
// Create creates a new file with the specified name and returns a File interface.
Create(name string) (File, error)
// Stat retrieves the file information for the specified name, returning
// fs.FileInfo and an error if any.
Stat(name string) (fs.FileInfo, error)
// Open opens an existing file with the specified name and returns a File interface.
Open(name string) (File, error)
// Remove deletes the file or directory with the specified name.
Remove(name string) error
}
type MockDisk struct {
MkdirFunc func(name string, perm fs.FileMode) error
CreateFunc func(name string) (File, error)
StatFunc func(name string) (fs.FileInfo, error)
OpenFunc func(name string) (File, error)
RemoveFunc func(name string) error
}
var _ Disk = &MockDisk{}
func (t *MockDisk) Mkdir(name string, perm fs.FileMode) error {
if t.MkdirFunc != nil {
return t.MkdirFunc(name, perm)
}
return nil
}
func (t *MockDisk) Create(name string) (File, error) {
if t.CreateFunc != nil {
return t.CreateFunc(name)
}
return nil, nil
}
func (t *MockDisk) Stat(name string) (fs.FileInfo, error) {
if t.StatFunc != nil {
return t.StatFunc(name)
}
return nil, nil
}
func (t *MockDisk) Open(name string) (File, error) {
if t.OpenFunc != nil {
return t.OpenFunc(name)
}
return nil, nil
}
func (t *MockDisk) Remove(name string) error {
if t.RemoveFunc != nil {
return t.RemoveFunc(name)
}
return nil
}
type MockFileInfo struct {
NameFunc func() string
SizeFunc func() int64
IsDirFunc func() bool
ModeFunc func() fs.FileMode
ModTimeFunc func() fs.FileInfo
SysFunc func() any
}
var _ fs.FileInfo = &MockFileInfo{}
// IsDir implements fs.FileInfo.
func (t *MockFileInfo) IsDir() bool {
if t.IsDirFunc != nil {
return t.IsDirFunc()
}
return false
}
// ModTime implements fs.FileInfo.
func (t *MockFileInfo) ModTime() time.Time {
if t.ModTimeFunc != nil {
return t.ModTimeFunc().ModTime()
}
return time.Time{}
}
// Mode implements fs.FileInfo.
func (t *MockFileInfo) Mode() fs.FileMode {
if t.ModeFunc != nil {
return t.ModeFunc()
}
return 0
}
// Name implements fs.FileInfo.
func (t *MockFileInfo) Name() string {
if t.NameFunc != nil {
return t.NameFunc()
}
return ""
}
// Size implements fs.FileInfo.
func (t *MockFileInfo) Size() int64 {
if t.SizeFunc != nil {
return t.SizeFunc()
}
return 0
}
// Sys implements fs.FileInfo.
func (t *MockFileInfo) Sys() any {
if t.SysFunc != nil {
return t.SysFunc()
}
return nil
}

View File

@ -0,0 +1,66 @@
package scoped
import (
"io/fs"
"path/filepath"
"koti.casa/numenor-labs/dsfx/pkg/disk"
)
// StorageScope is an interface that extends the disk.Disk interface by ensuring
// that all file operations are limited to a specific folder on the disk, which
// provides a dedicated storage scope that is isolated from other file operations.
type StorageScope interface {
disk.Disk
// Scope returns the storage scope as a string.
Scope() string
}
// scoped is a concrete implementation of the StorageScope interface.
type scoped struct {
disk disk.Disk
scope string
}
// New creates a new StorageScope with the specified scope.
func New(disk disk.Disk, scope string) StorageScope {
return &scoped{disk, scope}
}
// Create implements disk.Disk.
func (s *scoped) Create(name string) (disk.File, error) {
return s.disk.Create(s.path(name))
}
// Mkdir implements disk.Disk.
func (s *scoped) Mkdir(name string, perm fs.FileMode) error {
return s.disk.Mkdir(s.path(name), perm)
}
// Open implements disk.Disk.
func (s *scoped) Open(name string) (disk.File, error) {
return s.disk.Open(s.path(name))
}
// Remove implements disk.Disk.
func (s *scoped) Remove(name string) error {
return s.disk.Remove(s.path(name))
}
// Stat implements disk.Disk.
func (s *scoped) Stat(name string) (fs.FileInfo, error) {
return s.disk.Stat(s.path(name))
}
// Scope implements StorageScope.
func (s *scoped) Scope() string {
return s.scope
}
// path is a helper method that returns the full path for a given path relative
// to the storage scope. This ensures that all file operations are performed
// within the defined scope.
func (s *scoped) path(path string) string {
return filepath.Join(s.scope, path)
}

View File

@ -0,0 +1,103 @@
package scoped_test
import (
"io/fs"
"testing"
"koti.casa/numenor-labs/dsfx/pkg/disk"
"koti.casa/numenor-labs/dsfx/pkg/storage/scoped"
)
func TestScopedStorage_Scope(t *testing.T) {
mockDisk := &disk.MockDisk{}
storage := scoped.New(mockDisk, "testscope")
if scope := storage.Scope(); scope != "testscope" {
t.Errorf("expected 'testscope', got '%s'", scope)
}
}
func TestScopedStorage_Mkdir(t *testing.T) {
mockDisk := &disk.MockDisk{
MkdirFunc: func(name string, perm fs.FileMode) error {
if name != "testscope/testdir" {
t.Errorf("expected 'testscope/testdir', got '%s'", name)
}
return nil
},
}
storage := scoped.New(mockDisk, "testscope")
err := storage.Mkdir("testdir", 0755)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestScopedStorage_Create(t *testing.T) {
mockDisk := &disk.MockDisk{
CreateFunc: func(name string) (disk.File, error) {
if name != "testscope/testfile" {
t.Errorf("expected 'testscope/testfile', got '%s'", name)
}
return nil, nil
},
}
storage := scoped.New(mockDisk, "testscope")
_, err := storage.Create("testfile")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestScopedStorage_Stat(t *testing.T) {
mockDisk := &disk.MockDisk{
StatFunc: func(name string) (fs.FileInfo, error) {
if name != "testscope/testfile" {
t.Errorf("expected 'testscope/testfile', got '%s'", name)
}
return nil, nil
},
}
storage := scoped.New(mockDisk, "testscope")
_, err := storage.Stat("testfile")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestScopedStorage_Open(t *testing.T) {
mockDisk := &disk.MockDisk{
OpenFunc: func(name string) (disk.File, error) {
if name != "testscope/testfile" {
t.Errorf("expected 'testscope/testfile', got '%s'", name)
}
return nil, nil
},
}
storage := scoped.New(mockDisk, "testscope")
_, err := storage.Open("testfile")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestScopedStorage_Remove(t *testing.T) {
mockDisk := &disk.MockDisk{
RemoveFunc: func(name string) error {
if name != "testscope/testfile" {
t.Errorf("expected 'testscope/testfile', got '%s'", name)
}
return nil
},
}
storage := scoped.New(mockDisk, "testscope")
err := storage.Remove("testfile")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

85
pkg/system/default.go Normal file
View File

@ -0,0 +1,85 @@
package system
import (
"os"
"koti.casa/numenor-labs/dsfx/pkg/disk"
)
// Default returns a default implementation of the System interface.
func Default() System {
return &osSystem{}
}
type osSystem struct{}
// Args returns the command-line arguments passed to the program. The name of the
// program is filtered out, so it only returns the arguments provided by the user.
// It implements the System interface.
func (s *osSystem) Args() []string {
return os.Args[1:]
}
// Arg returns the command-line argument at the specified index. If the index is
// out of range, it returns an empty string.
// // It implements the System interface.
func (s *osSystem) Arg(i int) string {
if i < 0 || i >= len(os.Args) {
return ""
}
return os.Args[i+1] // +1 to skip the program name
}
// UserHomeDir returns the user's home directory.
// It implements the System interface.
func (s *osSystem) UserHomeDir() (string, error) {
return os.UserHomeDir()
}
// UserConfigDir returns the user's configuration directory.
// It implements the System interface.
func (s *osSystem) UserConfigDir() (string, error) {
return os.UserConfigDir()
}
// UserCacheDir returns the user's cache directory.
// It implements the System interface.
func (s *osSystem) UserCacheDir() (string, error) {
return os.UserCacheDir()
}
// TempDir returns the directory used for temporary files.
// It implements the System interface.
func (s *osSystem) TempDir() string {
return os.TempDir()
}
// Stdout returns the standard output file.
// It implements the System interface.
func (s *osSystem) Stdout() disk.File {
return os.Stdout
}
// Stderr returns the standard error file.
// It implements the System interface.
func (s *osSystem) Stderr() disk.File {
return os.Stderr
}
// Exit terminates the program with the given exit code.
// It implements the System interface.
func (s *osSystem) Exit(code int) {
os.Exit(code)
}
// GetEnv retrieves the value of the environment variable named by key.
// It implements the System interface.
func (s *osSystem) GetEnv(key string) string {
return os.Getenv(key)
}
// SetEnv sets the value of the environment variable named by key to value.
// It implements the System interface.
func (s *osSystem) SetEnv(key, value string) error {
return os.Setenv(key, value)
}

30
pkg/system/system.go Normal file
View File

@ -0,0 +1,30 @@
package system
import (
"koti.casa/numenor-labs/dsfx/pkg/disk"
)
type System interface {
// Args returns the command-line arguments passed to the program.
Args() []string
// Arg returns the command-line argument at the specified index.
Arg(int) string
// UserHomeDir returns the user's home directory.
UserHomeDir() (string, error)
// UserConfigDir returns the user's configuration directory.
UserConfigDir() (string, error)
// UserCacheDir returns the user's cache directory.
UserCacheDir() (string, error)
// Stdout returns the standard output file.
Stdout() disk.File
// Stderr returns the standard error file.
Stderr() disk.File
// TempDir returns the directory used for temporary files.
TempDir() string
// Exit terminates the program with the given exit code.
Exit(int)
// GetEnv retrieves the value of the environment variable named by key.
GetEnv(key string) string
// SetEnv sets the value of the environment variable named by key to value.
SetEnv(key, value string) error
}