- Go 95.9%
- Makefile 4.1%
| docs | ||
| .gitignore | ||
| .golangci.yml | ||
| .pre-commit-config.yaml | ||
| go.mod | ||
| go.sum | ||
| henka.go | ||
| henka_test.go | ||
| Makefile | ||
| README.md | ||
Table of Contents
henka - Simplified 9P Server Abstraction
Overview
Minimal abstraction layer over github.com/knusbaum/go9p for easy 9P server setup. Flat map storage with path-based semantics. Supports static values and io.Reader/io.Writer interfaces.
Integration with go9p
// henka provides a complete 9P server implementation:
h := henka.New()
// ... add nodes ...
server := h.NewServer(true) // dotu = true/false
// Server implements go9p.Srv and is ready to use with go9p listeners
// Alternative: Get server directly
server := h.Server() // Equivalent to h.NewServer(true)
Core Types
Node
type Node struct {
Name string // Full path: "dir/file"
IsDir bool // Directory marker
Value interface{} // Content: static or io.Reader/io.Writer
Mode os.FileMode // Permissions (default: 0644 files, 0755 dirs)
}
Abstraction (provides filesystem root for go9p)
type Abstraction struct {
nodes map[string]*Node
mu sync.RWMutex
}
Path Resolution
- Paths normalized (no trailing "/")
- Directories auto-created when needed
- Example: Adding "/a/b/c" creates "/a" and "/a/b" as directories if missing
Simple API
Setup
// Create and populate
fs := henka.New()
// Add directory (optional, will auto-create)
fs.AddNode("/tmp", nil, true)
// Add file with static value
fs.AddNode("/tmp/msg", "Hello 9P", false)
fs.AddNode("/tmp/count", int64(42), false)
// Add io.Writer for logging
var buf bytes.Buffer
fs.AddNode("/tmp/log", &buf, false)
// Ready for go9p
server := fs.NewServer(true)
Automatic Directory Creation
Directories are implicitly created when adding files:
// This creates /config and /config/db as directories automatically
fs.AddNode("/config/db/host", "localhost", false)
Supported Value Types
- Read/Write: []byte, *bytes.Buffer, string, int64, float64
- Write-only: io.Writer (append only, offset must be 0), io.WriterAt (arbitrary offsets)
- Read-only: io.Reader (reads from beginning), io.ReaderAt (arbitrary offsets)
- Directories: Value = nil, IsDir = true
Note: Types implementing multiple interfaces (io.ReadWriter, io.ReadWriteSeeker) will work according to their implemented interfaces.
I/O Behavior
Reading
// All read operations through henka.Read()
data, err := fs.Read("/tmp/msg", 0, 100)
- io.ReaderAt: ReadAt(offset, count)
- io.Reader: Reads all data from beginning, then slices (no seeking)
- Static types: Return appropriate slice
- []byte, *bytes.Buffer: slice[offset:offset+count]
- string: UTF-8 bytes
- int64/float64: binary representation (8 bytes)
- Returns error if unsupported
Writing
// All write operations through henka.Write()
err := fs.Write("/tmp/count", 0, []byte{0,0,0,0,0,0,0,43})
- io.WriterAt: WriteAt(data, offset)
- io.Writer: Write(data) (offset must be 0, append only)
- []byte: modify slice at offset, extends if needed
- *bytes.Buffer: writes at offset, extends with zeros if needed
- Other static types (string, int64, float64): replace entire value (offset must be 0)
- Returns error if unsupported
Permissions
-
Default: files 0644, directories 0755
-
Customizable per node:
fs.AddNodeWithMode("/secret", "data", false, 0600)
Complete Example
package main
import (
"bytes"
"net"
"github.com/knusbaum/go9p"
"git.lan.thwap.org/thwap/henka"
)
func main() {
// Create henka filesystem
h := henka.New()
// Add some files
h.AddNode("/README", "Welcome to 9P", false)
h.AddNode("/count", int64(0), false)
// Add a directory with files
h.AddNode("/logs/access", &bytes.Buffer{}, false)
h.AddNode("/logs/error", &bytes.Buffer{}, false)
// Create go9p server
server := h.NewServer(true)
// Start listening using go9p.Serve
listener, _ := net.Listen("tcp", ":5640")
go9p.Serve(listener, server)
}
Error Handling
- Missing parent directories: auto-created (simplifies setup)
- Invalid paths: returns error
- Unsupported operations: returns clear error
Advantages for go9p Users
- No boilerplate: Skip manual fs.Node implementation
- Automatic structure: Paths automatically create directory hierarchy
- Flexible backends: Use static data, buffers, or custom readers/writers
- Simple integration: Just create a henka instance with henka.New() and call NewServer()
Client API
The library includes a client for connecting to remote 9P servers:
import "git.lan.thwap.org/thwap/henka"
// Connect to a 9P server
client, err := henka.NewClient("localhost:564")
if err != nil {
log.Fatal(err)
}
defer client.Close()
// List directory contents
entries, err := client.Ls("/")
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
fmt.Printf("%s %d %v\n", e.Name, e.Size, e.ModTime)
}
// Read file data
data, err := client.Read("/file.txt", 0, 100)
if err != nil {
log.Fatal(err)
}
// Write to file
err = client.Write("/file.txt", 0, []byte("new content"))
if err != nil {
log.Fatal(err)
}
Testing with External Servers
Client tests can run against an external 9P server by setting the 9P_SERVER environment variable:
export 9P_SERVER="localhost:564"
go test -v -run TestClient
This is useful for integration testing with compliant 9P servers.
Development
The project includes comprehensive testing and quality assurance tools:
- Testing: Run
go test ./...ormake testfor unit tests - Race Detection: Run
go test -race ./...to detect concurrency issues - Code Quality: Pre-commit hooks enforce formatting and linting (see
.pre-commit-config.yaml) - Build:
make buildcompiles the package - Formatting:
make fmtruns go fmt
See Makefile for all available commands.
Notes
- Designed specifically for go9p compatibility
- Minimal overhead over raw go9p
- Focus on developer ergonomics for common 9P server use cases