mirror of
https://git.numenor-labs.us/dsfx.git
synced 2025-04-29 08:10:34 +00:00
refactor(project): add external abstractions
This commit is contained in:
parent
30f031fe1c
commit
1bfada68df
143
cmd/dsfxctl/client/app.go
Normal file
143
cmd/dsfxctl/client/app.go
Normal 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
34
cmd/dsfxctl/conf/conf.go
Normal 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
|
||||
}
|
@ -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
42
cmd/dsfxnode/conf/conf.go
Normal 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
|
||||
}
|
@ -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
243
cmd/dsfxnode/node/node.go
Normal 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
64
pkg/disk/default.go
Normal 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
57
pkg/disk/default_test.go
Normal 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
137
pkg/disk/disk.go
Normal 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
|
||||
}
|
66
pkg/storage/scoped/scoped.go
Normal file
66
pkg/storage/scoped/scoped.go
Normal 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)
|
||||
}
|
103
pkg/storage/scoped/scoped_test.go
Normal file
103
pkg/storage/scoped/scoped_test.go
Normal 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
85
pkg/system/default.go
Normal 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
30
pkg/system/system.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user