docs(ax): align guidance with current medium surface
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
3054217038
commit
97535f650a
4 changed files with 25 additions and 24 deletions
|
|
@ -34,7 +34,7 @@ GOWORK=off go test -cover ./...
|
|||
|
||||
### Core Interface
|
||||
|
||||
`io.Medium` — 18 methods: Read, Write, EnsureDir, IsFile, FileGet, FileSet, Delete, DeleteAll, Rename, List, Stat, Open, Create, Append, ReadStream, WriteStream, Exists, IsDir.
|
||||
`io.Medium` — 17 methods: Read, Write, WriteMode, EnsureDir, IsFile, Delete, DeleteAll, Rename, List, Stat, Open, Create, Append, ReadStream, WriteStream, Exists, IsDir.
|
||||
|
||||
```go
|
||||
// Sandboxed to a project directory
|
||||
|
|
@ -60,7 +60,7 @@ io.Copy(s3Medium, "backup.tar", localMedium, "restore/backup.tar")
|
|||
| `datanode` | Borg DataNode | Thread-safe (RWMutex) in-memory, snapshot/restore via tar |
|
||||
| `store` | SQLite KV store | Group-namespaced key-value with Go template rendering |
|
||||
| `workspace` | Core service | Encrypted workspaces, SHA-256 IDs, PGP keypairs |
|
||||
| `MockMedium` | In-memory map | Testing — no filesystem needed |
|
||||
| `MemoryMedium` | In-memory map | Testing — no filesystem needed |
|
||||
|
||||
`store.Medium` maps filesystem paths as `group/key` — first path segment is the group, remainder is the key. `List("")` returns groups as directories.
|
||||
|
||||
|
|
@ -132,4 +132,4 @@ Sentinel errors (`var ErrNotFound`, `var ErrInvalidKey`, etc.) use standard `err
|
|||
|
||||
## Testing
|
||||
|
||||
Use `io.MockMedium` or `io.NewSandboxed(t.TempDir())` in tests — never hit real S3/SQLite unless integration testing. S3 tests use an interface-based mock (`s3.Client`).
|
||||
Use `io.NewMemoryMedium()` or `io.NewSandboxed(t.TempDir())` in tests — never hit real S3/SQLite unless integration testing. S3 tests use an interface-based mock (`s3.Client`).
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ The `Medium` interface is defined in `io.go`. It is the only type that consuming
|
|||
- **`io.Local`** — a package-level variable initialised in `init()` via `local.New("/")`. This gives unsandboxed access to the host filesystem, mirroring the behaviour of the standard `os` package.
|
||||
- **`io.NewSandboxed(root)`** — creates a `local.Medium` restricted to `root`. All path resolution is confined within that directory.
|
||||
- **`io.Copy(src, srcPath, dst, dstPath)`** — copies a file between any two mediums by reading from one and writing to the other.
|
||||
- **`io.MockMedium`** — a fully functional in-memory implementation for unit tests. It tracks files, directories, and modification times in plain maps.
|
||||
- **`io.NewMemoryMedium()`** — a fully functional in-memory implementation for unit tests. It tracks files, directories, and modification times in plain maps.
|
||||
|
||||
### FileInfo and DirEntry (root package)
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ Simple struct implementations of `fs.FileInfo` and `fs.DirEntry` are exported fr
|
|||
|
||||
### local.Medium
|
||||
|
||||
**File:** `local/client.go`
|
||||
**File:** `local/medium.go`
|
||||
|
||||
The local backend wraps the standard `os` package with two layers of path protection:
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ Key capabilities beyond `Medium`:
|
|||
|
||||
### datanode.Medium
|
||||
|
||||
**File:** `datanode/client.go`
|
||||
**File:** `datanode/medium.go`
|
||||
|
||||
A thread-safe `Medium` backed by Borg's `DataNode` (an in-memory `fs.FS` with tar serialisation). It adds:
|
||||
|
||||
|
|
@ -271,7 +271,7 @@ Application code
|
|||
+-- node.Node --> in-memory map + tar serialisation
|
||||
+-- datanode.Medium --> Borg DataNode + sync.RWMutex
|
||||
+-- store.Medium --> store.Store (SQLite KV) --> Medium adapter
|
||||
+-- MockMedium --> map[string]string (for tests)
|
||||
+-- MemoryMedium --> map[string]string (for tests)
|
||||
```
|
||||
|
||||
Every backend normalises paths using the same `path.Clean("/" + p)` pattern, ensuring consistent behaviour regardless of which backend is in use.
|
||||
|
|
|
|||
|
|
@ -88,18 +88,20 @@ func TestDelete_Bad_DirNotEmpty(t *testing.T) { /* returns error for non-empty d
|
|||
|
||||
## Writing Tests Against Medium
|
||||
|
||||
Use `MockMedium` from the root package for unit tests that need a storage backend but should not touch disk:
|
||||
Use `MemoryMedium` from the root package for unit tests that need a storage backend but should not touch disk:
|
||||
|
||||
```go
|
||||
func TestMyFeature(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
m.Files["config.yaml"] = "key: value"
|
||||
m.Dirs["data"] = true
|
||||
m := io.NewMemoryMedium()
|
||||
_ = m.Write("config.yaml", "key: value")
|
||||
_ = m.EnsureDir("data")
|
||||
|
||||
// Your code under test receives m as an io.Medium
|
||||
result, err := myFunction(m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "expected", m.Files["output.txt"])
|
||||
output, err := m.Read("output.txt")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "expected", output)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -134,7 +136,7 @@ func TestWithSQLite(t *testing.T) {
|
|||
To add a new `Medium` implementation:
|
||||
|
||||
1. Create a new package directory (e.g., `sftp/`).
|
||||
2. Define a struct that implements all 18 methods of `io.Medium`.
|
||||
2. Define a struct that implements all 17 methods of `io.Medium`.
|
||||
3. Add a compile-time check at the top of your file:
|
||||
|
||||
```go
|
||||
|
|
@ -142,7 +144,7 @@ var _ coreio.Medium = (*Medium)(nil)
|
|||
```
|
||||
|
||||
4. Normalise paths using `path.Clean("/" + p)` to prevent traversal escapes. This is the convention followed by every existing backend.
|
||||
5. Handle `nil` and empty input consistently: check how `MockMedium` and `local.Medium` behave and match that behaviour.
|
||||
5. Handle `nil` and empty input consistently: check how `MemoryMedium` and `local.Medium` behave and match that behaviour.
|
||||
6. Write tests using the `_Good` / `_Bad` / `_Ugly` naming convention.
|
||||
7. Add your package to the table in `docs/index.md`.
|
||||
|
||||
|
|
@ -171,13 +173,13 @@ To add a new data transformation:
|
|||
|
||||
```
|
||||
go-io/
|
||||
├── io.go # Medium interface, helpers, MockMedium
|
||||
├── client_test.go # Tests for MockMedium and helpers
|
||||
├── io.go # Medium interface, helpers, MemoryMedium
|
||||
├── medium_test.go # Tests for MemoryMedium and helpers
|
||||
├── bench_test.go # Benchmarks
|
||||
├── go.mod
|
||||
├── local/
|
||||
│ ├── client.go # Local filesystem backend
|
||||
│ └── client_test.go
|
||||
│ ├── medium.go # Local filesystem backend
|
||||
│ └── medium_test.go
|
||||
├── s3/
|
||||
│ ├── s3.go # S3 backend
|
||||
│ └── s3_test.go
|
||||
|
|
@ -188,8 +190,8 @@ go-io/
|
|||
│ ├── node.go # In-memory fs.FS + Medium
|
||||
│ └── node_test.go
|
||||
├── datanode/
|
||||
│ ├── client.go # Borg DataNode Medium wrapper
|
||||
│ └── client_test.go
|
||||
│ ├── medium.go # Borg DataNode Medium wrapper
|
||||
│ └── medium_test.go
|
||||
├── store/
|
||||
│ ├── store.go # KV store
|
||||
│ ├── medium.go # Medium adapter for KV store
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ _ = bucket.Write("photo.jpg", rawData)
|
|||
|
||||
| Package | Import Path | Purpose |
|
||||
|---------|-------------|---------|
|
||||
| `io` (root) | `forge.lthn.ai/core/go-io` | `Medium` interface, helper functions, `MockMedium` for tests |
|
||||
| `io` (root) | `forge.lthn.ai/core/go-io` | `Medium` interface, helper functions, `MemoryMedium` for tests |
|
||||
| `local` | `forge.lthn.ai/core/go-io/local` | Local filesystem backend with path sandboxing and symlink-escape protection |
|
||||
| `s3` | `forge.lthn.ai/core/go-io/s3` | Amazon S3 / S3-compatible backend (Garage, MinIO, etc.) |
|
||||
| `sqlite` | `forge.lthn.ai/core/go-io/sqlite` | SQLite-backed virtual filesystem (pure Go driver, no CGO) |
|
||||
|
|
@ -54,15 +54,14 @@ _ = bucket.Write("photo.jpg", rawData)
|
|||
|
||||
## The Medium Interface
|
||||
|
||||
Every storage backend implements the same 18-method interface:
|
||||
Every storage backend implements the same 17-method interface:
|
||||
|
||||
```go
|
||||
type Medium interface {
|
||||
// Content operations
|
||||
Read(path string) (string, error)
|
||||
Write(path, content string) error
|
||||
FileGet(path string) (string, error) // alias for Read
|
||||
FileSet(path, content string) error // alias for Write
|
||||
WriteMode(path, content string, mode fs.FileMode) error
|
||||
|
||||
// Streaming (for large files)
|
||||
ReadStream(path string) (io.ReadCloser, error)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue