refactor(ax): tighten AX-facing docs
This commit is contained in:
parent
16d968b551
commit
a8eaaa1581
14 changed files with 66 additions and 170 deletions
|
|
@ -1,14 +1,9 @@
|
|||
// Package datanode provides an in-memory io.Medium backed by Borg's DataNode.
|
||||
// Package datanode keeps io.Medium data in Borg's DataNode.
|
||||
//
|
||||
// medium := datanode.New()
|
||||
// _ = medium.Write("jobs/run.log", "started")
|
||||
// snapshot, _ := medium.Snapshot()
|
||||
// restored, _ := datanode.FromTar(snapshot)
|
||||
//
|
||||
// DataNode is an in-memory fs.FS that serialises to tar. Wrapping it as a
|
||||
// Medium lets any code that works with io.Medium transparently operate on an
|
||||
// in-memory filesystem that can be snapshotted, shipped as a crash report, or
|
||||
// wrapped in a TIM container for runc execution.
|
||||
package datanode
|
||||
|
||||
import (
|
||||
|
|
@ -39,16 +34,12 @@ var (
|
|||
// Example: medium := datanode.New()
|
||||
// _ = medium.Write("jobs/run.log", "started")
|
||||
// snapshot, _ := medium.Snapshot()
|
||||
//
|
||||
// All paths are relative (no leading slash). Thread-safe via RWMutex.
|
||||
type Medium struct {
|
||||
dataNode *borgdatanode.DataNode
|
||||
directorySet map[string]bool // explicit directories that exist without file contents
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Example: medium := datanode.New()
|
||||
// _ = medium.Write("jobs/run.log", "started")
|
||||
func New() *Medium {
|
||||
return &Medium{
|
||||
dataNode: borgdatanode.New(),
|
||||
|
|
@ -56,11 +47,9 @@ func New() *Medium {
|
|||
}
|
||||
}
|
||||
|
||||
// FromTar restores a Medium from tar bytes.
|
||||
//
|
||||
// sourceMedium := datanode.New()
|
||||
// snapshot, _ := sourceMedium.Snapshot()
|
||||
// restored, _ := datanode.FromTar(snapshot)
|
||||
// Example: sourceMedium := datanode.New()
|
||||
// snapshot, _ := sourceMedium.Snapshot()
|
||||
// restored, _ := datanode.FromTar(snapshot)
|
||||
func FromTar(data []byte) (*Medium, error) {
|
||||
dataNode, err := borgdatanode.FromTar(data)
|
||||
if err != nil {
|
||||
|
|
@ -73,7 +62,6 @@ func FromTar(data []byte) (*Medium, error) {
|
|||
}
|
||||
|
||||
// Example: snapshot, _ := medium.Snapshot()
|
||||
// Use this for crash reports, workspace packaging, or TIM creation.
|
||||
func (m *Medium) Snapshot() ([]byte, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
|
@ -98,7 +86,6 @@ func (m *Medium) Restore(data []byte) error {
|
|||
}
|
||||
|
||||
// Example: dataNode := medium.DataNode()
|
||||
// Use this to wrap the filesystem in a TIM container.
|
||||
func (m *Medium) DataNode() *borgdatanode.DataNode {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
|
|
|||
6
doc.go
6
doc.go
|
|
@ -1,11 +1,7 @@
|
|||
// Package io defines the storage boundary used across CoreGO.
|
||||
// Package io gives CoreGO a single storage surface.
|
||||
//
|
||||
// medium, _ := io.NewSandboxed("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// backup, _ := io.NewSandboxed("/srv/backup")
|
||||
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
|
||||
//
|
||||
// Callers work against Medium so the same code can read and write state from
|
||||
// sandboxed local paths, in-memory nodes, SQLite, S3, or other backends
|
||||
// without changing application logic.
|
||||
package io
|
||||
|
|
|
|||
40
io.go
40
io.go
|
|
@ -10,12 +10,10 @@ import (
|
|||
"dappco.re/go/core/io/local"
|
||||
)
|
||||
|
||||
// Medium is the storage boundary used across CoreGO.
|
||||
//
|
||||
// medium, _ := io.NewSandboxed("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// backup, _ := io.NewSandboxed("/srv/backup")
|
||||
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
|
||||
// Example: medium, _ := io.NewSandboxed("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// backup, _ := io.NewSandboxed("/srv/backup")
|
||||
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
|
||||
type Medium interface {
|
||||
Read(path string) (string, error)
|
||||
|
||||
|
|
@ -61,7 +59,7 @@ type Medium interface {
|
|||
IsDir(path string) bool
|
||||
}
|
||||
|
||||
// FileInfo provides a simple implementation of fs.FileInfo for mock testing.
|
||||
// FileInfo is a test helper that satisfies fs.FileInfo.
|
||||
type FileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
|
|
@ -82,7 +80,7 @@ func (fi FileInfo) IsDir() bool { return fi.isDir }
|
|||
|
||||
func (fi FileInfo) Sys() any { return nil }
|
||||
|
||||
// DirEntry provides a simple implementation of fs.DirEntry for mock testing.
|
||||
// DirEntry is a test helper that satisfies fs.DirEntry.
|
||||
type DirEntry struct {
|
||||
name string
|
||||
isDir bool
|
||||
|
|
@ -98,9 +96,7 @@ func (de DirEntry) Type() fs.FileMode { return de.mode.Type() }
|
|||
|
||||
func (de DirEntry) Info() (fs.FileInfo, error) { return de.info, nil }
|
||||
|
||||
// Local is the unsandboxed filesystem medium rooted at "/".
|
||||
//
|
||||
// io.Local.Read("/etc/hostname")
|
||||
// Example: io.Local.Read("/etc/hostname")
|
||||
var Local Medium
|
||||
|
||||
var _ Medium = (*local.Medium)(nil)
|
||||
|
|
@ -113,14 +109,8 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
// Use NewSandboxed to confine file operations to a root directory.
|
||||
// All file operations are restricted to paths within the root, and the root
|
||||
// directory will be created if it does not exist.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// medium, _ := io.NewSandboxed("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// Example: medium, _ := io.NewSandboxed("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
func NewSandboxed(root string) (Medium, error) {
|
||||
return local.New(root)
|
||||
}
|
||||
|
|
@ -171,10 +161,8 @@ func Copy(source Medium, sourcePath string, destination Medium, destinationPath
|
|||
|
||||
// --- MockMedium ---
|
||||
|
||||
// MockMedium is an in-memory Medium for tests.
|
||||
//
|
||||
// medium := io.NewMockMedium()
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// Example: medium := io.NewMockMedium()
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
type MockMedium struct {
|
||||
Files map[string]string
|
||||
Dirs map[string]bool
|
||||
|
|
@ -183,10 +171,8 @@ type MockMedium struct {
|
|||
|
||||
var _ Medium = (*MockMedium)(nil)
|
||||
|
||||
// Use NewMockMedium when tests need an in-memory Medium.
|
||||
//
|
||||
// medium := io.NewMockMedium()
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// Example: medium := io.NewMockMedium()
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
func NewMockMedium() *MockMedium {
|
||||
return &MockMedium{
|
||||
Files: make(map[string]string),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Package local provides the local filesystem implementation of io.Medium.
|
||||
// Package local binds io.Medium to the local filesystem.
|
||||
//
|
||||
// medium, _ := local.New("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
|
|
@ -13,18 +13,16 @@ import (
|
|||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
// Medium is the local filesystem backend returned by New.
|
||||
// Example: medium, _ := local.New("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
type Medium struct {
|
||||
filesystemRoot string
|
||||
}
|
||||
|
||||
var unrestrictedFileSystem = (&core.Fs{}).NewUnrestricted()
|
||||
|
||||
// local.New("/") exposes the full filesystem.
|
||||
// local.New("/srv/app") confines access to a project root.
|
||||
//
|
||||
// medium, _ := local.New("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// Example: medium, _ := local.New("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
func New(root string) (*Medium, error) {
|
||||
absoluteRoot := absolutePath(root)
|
||||
// Resolve symlinks so sandbox checks compare like-for-like.
|
||||
|
|
@ -179,7 +177,7 @@ func logSandboxEscape(root, path, attempted string) {
|
|||
core.Security("sandbox escape detected", "root", root, "path", path, "attempted", attempted, "user", username)
|
||||
}
|
||||
|
||||
// sandboxedPath sanitises and returns the full filesystem path.
|
||||
// sandboxedPath resolves a path inside the filesystem root.
|
||||
// Absolute paths are sandboxed under root (unless root is "/").
|
||||
func (m *Medium) sandboxedPath(path string) string {
|
||||
if path == "" {
|
||||
|
|
|
|||
13
node/node.go
13
node/node.go
|
|
@ -1,12 +1,9 @@
|
|||
// Package node provides an in-memory filesystem implementation of io.Medium.
|
||||
// Package node keeps io.Medium data in memory.
|
||||
//
|
||||
// nodeTree := node.New()
|
||||
// nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
|
||||
// snapshot, _ := nodeTree.ToTar()
|
||||
// restored, _ := node.FromTar(snapshot)
|
||||
//
|
||||
// It stores files in memory with implicit directory structure and supports
|
||||
// tar serialisation.
|
||||
package node
|
||||
|
||||
import (
|
||||
|
|
@ -27,18 +24,14 @@ import (
|
|||
// nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
|
||||
// snapshot, _ := nodeTree.ToTar()
|
||||
// restored, _ := node.FromTar(snapshot)
|
||||
//
|
||||
// Directories are implicit: they exist whenever a file path contains a "/".
|
||||
type Node struct {
|
||||
files map[string]*dataFile
|
||||
}
|
||||
|
||||
// compile-time interface checks
|
||||
// Compile-time interface checks.
|
||||
var _ coreio.Medium = (*Node)(nil)
|
||||
var _ fs.ReadFileFS = (*Node)(nil)
|
||||
|
||||
// Example: nodeTree := node.New()
|
||||
// nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
|
||||
func New() *Node {
|
||||
return &Node{files: make(map[string]*dataFile)}
|
||||
}
|
||||
|
|
@ -89,7 +82,7 @@ func (n *Node) ToTar() ([]byte, error) {
|
|||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Use FromTar(data) to restore an in-memory tree from tar bytes.
|
||||
// Example: restored, _ := node.FromTar(snapshot)
|
||||
func FromTar(data []byte) (*Node, error) {
|
||||
n := New()
|
||||
if err := n.LoadTar(data); err != nil {
|
||||
|
|
|
|||
12
s3/s3.go
12
s3/s3.go
|
|
@ -1,4 +1,4 @@
|
|||
// Package s3 provides an S3-backed io.Medium.
|
||||
// Package s3 stores io.Medium data in S3 objects.
|
||||
//
|
||||
// client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"})
|
||||
// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
|
||||
|
|
@ -21,8 +21,8 @@ import (
|
|||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// Client is the subset of the AWS S3 client API used by this package.
|
||||
// Tests can provide any mock that satisfies the same method set.
|
||||
// Example: client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"})
|
||||
// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
|
||||
type Client interface {
|
||||
GetObject(ctx context.Context, params *awss3.GetObjectInput, optFns ...func(*awss3.Options)) (*awss3.GetObjectOutput, error)
|
||||
PutObject(ctx context.Context, params *awss3.PutObjectInput, optFns ...func(*awss3.Options)) (*awss3.PutObjectOutput, error)
|
||||
|
|
@ -33,10 +33,8 @@ type Client interface {
|
|||
CopyObject(ctx context.Context, params *awss3.CopyObjectInput, optFns ...func(*awss3.Options)) (*awss3.CopyObjectOutput, error)
|
||||
}
|
||||
|
||||
// Medium is the S3-backed io.Medium returned by New.
|
||||
//
|
||||
// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
|
||||
// _ = medium.Write("reports/daily.txt", "done")
|
||||
// Example: medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
|
||||
// _ = medium.Write("reports/daily.txt", "done")
|
||||
type Medium struct {
|
||||
client Client
|
||||
bucket string
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
// Package sigil provides pre-obfuscation helpers for XChaCha20-Poly1305.
|
||||
// Package sigil wraps XChaCha20-Poly1305 with deterministic pre-obfuscation.
|
||||
//
|
||||
// cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
|
||||
// ciphertext, _ := cipherSigil.In([]byte("payload"))
|
||||
// plaintext, _ := cipherSigil.Out(ciphertext)
|
||||
//
|
||||
// Use NewChaChaPolySigilWithObfuscator when you want ShuffleMaskObfuscator
|
||||
// instead of the default XOR pre-obfuscation layer.
|
||||
package sigil
|
||||
|
||||
import (
|
||||
|
|
@ -32,10 +29,7 @@ var (
|
|||
NoKeyConfiguredError = core.E("sigil.NoKeyConfiguredError", "no encryption key configured", nil)
|
||||
)
|
||||
|
||||
// PreObfuscator is the hook ChaChaPolySigil uses before and after encryption.
|
||||
//
|
||||
// XORObfuscator is the default. ShuffleMaskObfuscator is available when you
|
||||
// want byte shuffling as well as masking.
|
||||
// PreObfuscator customises the bytes mixed in before and after encryption.
|
||||
type PreObfuscator interface {
|
||||
// Obfuscate transforms plaintext before encryption using the provided entropy.
|
||||
// The entropy is typically the encryption nonce, ensuring the transformation
|
||||
|
|
@ -47,7 +41,7 @@ type PreObfuscator interface {
|
|||
Deobfuscate(data []byte, entropy []byte) []byte
|
||||
}
|
||||
|
||||
// XORObfuscator is the default pre-obfuscator returned by NewChaChaPolySigil.
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil(key)
|
||||
type XORObfuscator struct{}
|
||||
|
||||
// Obfuscate XORs the data with a key stream derived from the entropy.
|
||||
|
|
@ -215,11 +209,9 @@ type ChaChaPolySigil struct {
|
|||
randomReader goio.Reader // for testing injection
|
||||
}
|
||||
|
||||
// NewChaChaPolySigil returns a ChaChaPolySigil backed by a 32-byte key.
|
||||
//
|
||||
// cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
|
||||
// ciphertext, _ := cipherSigil.In([]byte("payload"))
|
||||
// plaintext, _ := cipherSigil.Out(ciphertext)
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
|
||||
// ciphertext, _ := cipherSigil.In([]byte("payload"))
|
||||
// plaintext, _ := cipherSigil.Out(ciphertext)
|
||||
func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) {
|
||||
if len(key) != 32 {
|
||||
return nil, InvalidKeyError
|
||||
|
|
@ -235,14 +227,14 @@ func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// NewChaChaPolySigilWithObfuscator returns a ChaChaPolySigil with a custom pre-obfuscator.
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator(
|
||||
//
|
||||
// cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator(
|
||||
// []byte("0123456789abcdef0123456789abcdef"),
|
||||
// &sigil.ShuffleMaskObfuscator{},
|
||||
// )
|
||||
// ciphertext, _ := cipherSigil.In([]byte("payload"))
|
||||
// plaintext, _ := cipherSigil.Out(ciphertext)
|
||||
// []byte("0123456789abcdef0123456789abcdef"),
|
||||
// &sigil.ShuffleMaskObfuscator{},
|
||||
//
|
||||
// )
|
||||
// ciphertext, _ := cipherSigil.In([]byte("payload"))
|
||||
// plaintext, _ := cipherSigil.Out(ciphertext)
|
||||
func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*ChaChaPolySigil, error) {
|
||||
cipherSigil, err := NewChaChaPolySigil(key)
|
||||
if err != nil {
|
||||
|
|
@ -334,9 +326,7 @@ func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
|
|||
return plaintext, nil
|
||||
}
|
||||
|
||||
// GetNonceFromCiphertext returns the nonce embedded in ciphertext.
|
||||
//
|
||||
// nonce, _ := sigil.GetNonceFromCiphertext(ciphertext)
|
||||
// Example: nonce, _ := sigil.GetNonceFromCiphertext(ciphertext)
|
||||
func GetNonceFromCiphertext(ciphertext []byte) ([]byte, error) {
|
||||
nonceSize := chacha20poly1305.NonceSizeX
|
||||
if len(ciphertext) < nonceSize {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,23 @@
|
|||
// Package sigil provides the Sigil transformation framework for composable,
|
||||
// reversible data transformations.
|
||||
// Package sigil chains reversible byte transformations.
|
||||
//
|
||||
// hexSigil, _ := sigil.NewSigil("hex")
|
||||
// gzipSigil, _ := sigil.NewSigil("gzip")
|
||||
// encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
|
||||
// decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
|
||||
//
|
||||
// Sigils are the core abstraction - each sigil implements a specific
|
||||
// transformation (encoding, compression, hashing, encryption) with a uniform
|
||||
// interface. Sigils can be chained together to create transformation pipelines.
|
||||
package sigil
|
||||
|
||||
import core "dappco.re/go/core"
|
||||
|
||||
// Sigil defines the interface for a data transformer.
|
||||
//
|
||||
// A Sigil represents a single transformation unit that can be applied to byte data.
|
||||
// Sigils may be reversible (encoding, compression, encryption) or irreversible (hashing).
|
||||
//
|
||||
// For reversible sigils: Out(In(x)) == x for all valid x
|
||||
// For irreversible sigils: Out returns the input unchanged
|
||||
// For symmetric sigils: In(x) == Out(x)
|
||||
//
|
||||
// Implementations must handle nil input by returning nil without error,
|
||||
// and empty input by returning an empty slice without error.
|
||||
// Sigil transforms byte slices.
|
||||
type Sigil interface {
|
||||
// In applies the forward transformation to the data.
|
||||
// For encoding sigils, this encodes the data.
|
||||
// For compression sigils, this compresses the data.
|
||||
// For hash sigils, this computes the digest.
|
||||
// Example: encoded, _ := hexSigil.In([]byte("payload"))
|
||||
In(data []byte) ([]byte, error)
|
||||
|
||||
// Out applies the reverse transformation to the data.
|
||||
// For reversible sigils, this recovers the original data.
|
||||
// For irreversible sigils (e.g., hashing), this returns the input unchanged.
|
||||
// Example: decoded, _ := hexSigil.Out(encoded)
|
||||
Out(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Transmute applies a series of sigils to data in sequence.
|
||||
//
|
||||
// Each sigil's In method is called in order, with the output of one sigil
|
||||
// becoming the input of the next. If any sigil returns an error, Transmute
|
||||
// stops immediately and returns nil with that error.
|
||||
//
|
||||
// To reverse a transmutation, call each sigil's Out method in reverse order.
|
||||
// Example: encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
|
||||
func Transmute(data []byte, sigils []Sigil) ([]byte, error) {
|
||||
var err error
|
||||
for _, s := range sigils {
|
||||
|
|
@ -55,11 +29,7 @@ func Transmute(data []byte, sigils []Sigil) ([]byte, error) {
|
|||
return data, nil
|
||||
}
|
||||
|
||||
// Untransmute reverses a transmutation by applying Out in reverse order.
|
||||
//
|
||||
// Each sigil's Out method is called in reverse order, with the output of one sigil
|
||||
// becoming the input of the next. If any sigil returns an error, Untransmute
|
||||
// stops immediately and returns nil with that error.
|
||||
// Example: decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
|
||||
func Untransmute(data []byte, sigils []Sigil) ([]byte, error) {
|
||||
var err error
|
||||
for i := len(sigils) - 1; i >= 0; i-- {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Package sqlite persists io.Medium content in a SQLite database.
|
||||
// Package sqlite stores io.Medium content in SQLite.
|
||||
//
|
||||
// medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
|
|
@ -18,10 +18,8 @@ import (
|
|||
_ "modernc.org/sqlite" // Pure Go SQLite driver
|
||||
)
|
||||
|
||||
// Medium stores filesystem-shaped content in SQLite.
|
||||
//
|
||||
// medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
type Medium struct {
|
||||
database *sql.DB
|
||||
table string
|
||||
|
|
@ -29,7 +27,6 @@ type Medium struct {
|
|||
|
||||
var _ coreio.Medium = (*Medium)(nil)
|
||||
|
||||
// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:", Table: "files"})
|
||||
type Options struct {
|
||||
// Path is the SQLite database path. Use ":memory:" for tests.
|
||||
Path string
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
// Package store provides a SQLite-backed group-namespaced key-value store.
|
||||
// Package store maps grouped keys onto SQLite rows.
|
||||
//
|
||||
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
// _ = keyValueStore.Set("app", "theme", "midnight")
|
||||
// medium := keyValueStore.AsMedium()
|
||||
// _ = medium.Write("app/theme", "midnight")
|
||||
//
|
||||
// It also exposes an io.Medium adapter so grouped values can participate in
|
||||
// the same storage workflows as filesystem-backed mediums.
|
||||
package store
|
||||
|
|
|
|||
|
|
@ -12,11 +12,8 @@ import (
|
|||
|
||||
// Example: medium, _ := store.NewMedium(store.Options{Path: "config.db"})
|
||||
// _ = medium.Write("app/theme", "midnight")
|
||||
// entries, _ := medium.List("")
|
||||
// entries, _ := medium.List("app")
|
||||
//
|
||||
// Paths are mapped as group/key - the first segment is the group,
|
||||
// the rest is the key. List("") returns groups as directories,
|
||||
// List("group") returns keys as files.
|
||||
type Medium struct {
|
||||
store *Store
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,15 @@ import (
|
|||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// NotFoundError is returned when a key does not exist in the store.
|
||||
// Example: _, err := keyValueStore.Get("app", "theme")
|
||||
// err matches store.NotFoundError when the key is missing.
|
||||
var NotFoundError = errors.New("key not found")
|
||||
|
||||
// Store is the grouped key/value database returned by New.
|
||||
//
|
||||
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
type Store struct {
|
||||
database *sql.DB
|
||||
}
|
||||
|
||||
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
type Options struct {
|
||||
// Path is the SQLite database path. Use ":memory:" for tests.
|
||||
Path string
|
||||
|
|
@ -134,13 +132,9 @@ func (s *Store) GetAll(group string) (map[string]string, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// Render loads all key-value pairs from a group and renders a Go template.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
// _ = keyValueStore.Set("user", "name", "alice")
|
||||
// out, _ := keyValueStore.Render("hello {{ .name }}", "user")
|
||||
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
// _ = keyValueStore.Set("user", "name", "alice")
|
||||
// out, _ := keyValueStore.Render("hello {{ .name }}", "user")
|
||||
func (s *Store) Render(templateText, group string) (string, error) {
|
||||
rows, err := s.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
// Package workspace provides encrypted user workspaces backed by io.Medium.
|
||||
// Package workspace creates encrypted workspaces on top of io.Medium.
|
||||
//
|
||||
// service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
|
||||
// workspaceID, _ := service.CreateWorkspace("alice", "pass123")
|
||||
// _ = service.SwitchWorkspace(workspaceID)
|
||||
// _ = service.WorkspaceFileSet("notes/todo.txt", "ship it")
|
||||
//
|
||||
// Workspaces are rooted under the caller's configured home directory and keep
|
||||
// file access constrained to the active workspace.
|
||||
package workspace
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import (
|
|||
"dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// Workspace is the workspace service interface returned by New.
|
||||
//
|
||||
// service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
|
||||
// Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
|
||||
type Workspace interface {
|
||||
CreateWorkspace(identifier, password string) (string, error)
|
||||
SwitchWorkspace(workspaceID string) error
|
||||
|
|
@ -34,7 +32,7 @@ type Options struct {
|
|||
Crypt CryptProvider
|
||||
}
|
||||
|
||||
// Service is the Workspace implementation returned by New.
|
||||
// Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
|
||||
type Service struct {
|
||||
core *core.Core
|
||||
crypt CryptProvider
|
||||
|
|
@ -77,8 +75,6 @@ func New(options Options) (*Service, error) {
|
|||
}
|
||||
|
||||
// Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123")
|
||||
// Identifier is hashed (SHA-256) to create the directory name.
|
||||
// A PGP keypair is generated using the password.
|
||||
func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue