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 (
|
import (
|
||||||
"context"
|
"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/cmd/dsfxctl/client"
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/logging"
|
"koti.casa/numenor-labs/dsfx/pkg/disk"
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/network"
|
"koti.casa/numenor-labs/dsfx/pkg/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
c := client.New(disk.Default(), system.Default())
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
err := c.Run(context.Background())
|
||||||
// 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()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to get home directory", slog.Any("error", err))
|
panic(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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
|
"koti.casa/numenor-labs/dsfx/cmd/dsfxnode/node"
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/logging"
|
"koti.casa/numenor-labs/dsfx/pkg/disk"
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/network"
|
"koti.casa/numenor-labs/dsfx/pkg/storage/scoped"
|
||||||
|
"koti.casa/numenor-labs/dsfx/pkg/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ()
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
sys := system.Default()
|
||||||
// Logger
|
|
||||||
homedir, err := os.UserHomeDir()
|
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 {
|
if err != nil {
|
||||||
logUserMsg("error: failed to get home directory\n")
|
// Log the error and exit with a non-zero status code.
|
||||||
logUserMsg("error: are you on a real computer?\n")
|
slog.Error("Error running dsfxnode", slog.Any("error", err))
|
||||||
// Look up why this function might fail...
|
sys.Exit(1)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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