Unified multi-protocol filesystem client for Go. Round-246 deep-doc + paired-mutation challenge enrichment.
Protocols: SMB | FTP | NFS (Linux) | WebDAV | Local
Module: digital.vasic.filesystem
Go: 1.25+
License: Proprietary
- Single
client.Clientinterface spanning five protocols — pick the backend at config time, leave call sites unchanged. - Factory-driven construction (
factory.NewDefaultFactory()) — every protocol selectable by string (local,ftp,smb,nfs,webdav) withStorageConfig.Settingscarrying per-protocol parameters. - Optional seekable extension (
client.SeekableClient/OpenSeekable) for protocols that natively support random access (SMB viasmb2_lseek, local viaos.File.Seek) — enables HTTP Range request serving for media streaming. - Platform-aware NFS —
pkg/factory/nfs_linux.goactivates the real syscall path on Linux;pkg/factory/nfs_other.goreturns a clear error elsewhere; the protocol still appears inSupportedProtocols. - Path-traversal guards on local backend —
../escapes the configuredbase_pathare rejected, verified by edge-case unit tests. - UTF-8 / diacritic filename support — exercised end-to-end by the
round-246 bilingual fixtures (Latin Serbian:
dnevnik/početak.log). - Zero hidden state in the interface — every method takes
ctx, reports errors verbatim, and never silently retries.
go get digital.vasic.filesystempackage main
import (
"context"
"fmt"
"log"
"digital.vasic.filesystem/pkg/client"
"digital.vasic.filesystem/pkg/factory"
)
func main() {
f := factory.NewDefaultFactory()
c, err := f.CreateClient(&client.StorageConfig{
ID: "media-local",
Name: "Local Media",
Protocol: "local",
Enabled: true,
Settings: map[string]interface{}{
"base_path": "/data/media",
},
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if err := c.Connect(ctx); err != nil {
log.Fatal(err)
}
defer c.Disconnect(ctx)
files, err := c.ListDirectory(ctx, "movies")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
fmt.Printf("%s (%d bytes, dir=%v)\n", file.Name, file.Size, file.IsDir)
}
}Every protocol adapter implements client.Client:
type Client interface {
Connect(ctx context.Context) error
Disconnect(ctx context.Context) error
IsConnected() bool
TestConnection(ctx context.Context) error
ReadFile(ctx context.Context, path string) (io.ReadCloser, error)
WriteFile(ctx context.Context, path string, data io.Reader) error
GetFileInfo(ctx context.Context, path string) (*FileInfo, error)
FileExists(ctx context.Context, path string) (bool, error)
DeleteFile(ctx context.Context, path string) error
CopyFile(ctx context.Context, srcPath, dstPath string) error
ListDirectory(ctx context.Context, path string) ([]*FileInfo, error)
CreateDirectory(ctx context.Context, path string) error
DeleteDirectory(ctx context.Context, path string) error
GetProtocol() string
GetConfig() interface{}
}Optional random-access extension:
type SeekableClient interface {
OpenSeekable(ctx context.Context, path string) (ReadSeekCloser, error)
}| Protocol | Required settings | Optional |
|---|---|---|
local |
base_path |
— |
ftp |
host, username, password |
port (21), path |
smb |
host, share, username, password |
port (445), domain (WORKGROUP) |
nfs |
host, path, mount_point |
options |
webdav |
url, username, password |
path |
Environment variable convention used by integration tests: each <setting> is
overridable by FILESYSTEM_<PROTOCOL>_<SETTING> (e.g. FILESYSTEM_SMB_HOST).
Real-network coverage is gated behind those env vars + SKIP-OK: markers per
CONST-035.
| Protocol | Linux | macOS | Windows |
|---|---|---|---|
| SMB | Yes | Yes | Yes |
| FTP | Yes | Yes | Yes |
| NFS | Yes | No | No |
| WebDAV | Yes | Yes | Yes |
| Local | Yes | Yes | Yes |
NFS uses Linux syscall.Mount and is gated behind //go:build linux build
tags. On non-Linux platforms, the factory returns an error for NFS protocol
requests while still listing it in SupportedProtocols().
- Empty body — writing a zero-byte file succeeds;
FileInfo.Size == 0. - Nested directories —
WriteFile("a/b/c.txt", ...)auto-creates parents. - UTF-8 filenames — diacritic and non-Latin paths preserved byte-for-byte
through write -> read round-trip (verified by
challenges/fixtures/sr-Latn.yaml). - Path traversal —
../escapes of the configuredbase_pathare rejected. - Symlinks —
GetFileInfofollows;ReadFilereads target content. - Not-connected operations — every method returns a clear "not connected"
error if invoked before
Connect. - Double connect / double disconnect — idempotent.
- Non-existent paths — every read/list/info call returns wrapped error.
# Unit + edge tests (all packages, race-tested)
go test -count=1 -race ./...
# Verbose
go test -count=1 -race -v ./...
# Per-package
go test -count=1 -race -v ./pkg/local/
# Coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html# Clean-mode (must exit 0 — proves docs, fixtures, runner, real-IO all green)
bash challenges/filesystem_describe_challenge.sh
# Paired-mutation mode (must exit 99 — proves gate actually catches drift)
bash challenges/filesystem_describe_challenge.sh --anti-bluff-mutate
# Run the bilingual runner directly
go run ./challenges/runner -fixtures challenges/fixturesThe Challenge validates: deep-doc ledger present + version-tagged + carries
the verbatim Article XI §11.9 mandate; every exported symbol from
pkg/{client,factory,local} is cross-referenced in the ledger; bilingual
fixtures parse and cover at least 2 locales; runner builds and round-trips
both ASCII and UTF-8 (diacritic) filenames through the real local client;
README declares the round-246 anti-bluff guarantees section.
go test -count=1 -race ./...runs every package's unit + edge suite — zerot.Skip()without aSKIP-OK: #<ticket>marker.challenges/filesystem_describe_challenge.shis paired-mutation aware (--anti-bluff-mutateexits 99 to prove the gate actually detects a planted ledger-vs-source rename).- Bilingual fixtures (
challenges/fixtures/{en,sr-Latn}.yaml) exercise non-ASCII filenames + UTF-8 file bodies; the runner asserts byte-equal round-trip through the realclient.Client.WriteFile/ReadFile. docs/test-coverage.mdenumerates every public symbol with its test sources — drift between the file andgo test -coveris treated as a CONST-035 / Article XI §11.9 bluff at the documentation-truth layer.- The runner's tmp tree is preserved on FAIL under
/tmp/fs-round246-*for forensic inspection (§11.4.2); cleaned only on PASS.
| Package | Import path | Description |
|---|---|---|
client |
digital.vasic.filesystem/pkg/client |
Core interfaces, FileInfo, StorageConfig |
factory |
digital.vasic.filesystem/pkg/factory |
DefaultFactory, helpers, platform gates |
local |
digital.vasic.filesystem/pkg/local |
Local filesystem adapter |
ftp |
digital.vasic.filesystem/pkg/ftp |
FTP protocol adapter |
smb |
digital.vasic.filesystem/pkg/smb |
SMB/CIFS protocol adapter |
nfs |
digital.vasic.filesystem/pkg/nfs |
NFS protocol adapter (Linux only) |
webdav |
digital.vasic.filesystem/pkg/webdav |
WebDAV protocol adapter |
- Architecture — Design patterns, Mermaid diagrams, package relationships
- User Guide — Step-by-step usage instructions
- API Reference — Complete API documentation for all packages
- Test Coverage Ledger (round-246) — symbol -> test source mapping
| Dependency | Purpose |
|---|---|
github.com/hirochachacha/go-smb2 |
SMB2/3 protocol implementation |
github.com/jlaffaye/ftp |
FTP client library |
github.com/stretchr/testify |
Test assertions |
Filesystem inherits Article XI §11.9 (anti-bluff), CONST-035 (zero-bluff),
CONST-047 (recursive submodule application), CONST-048 (full-automation-
coverage), CONST-050 (no-fakes-beyond-unit-tests + 100%-test-type-coverage),
CONST-051 (submodules-as-equal-codebase + decoupling), CONST-053 (.gitignore /
no-versioned-build-artifacts) from the constitution submodule. See
CONSTITUTION.md, CLAUDE.md, AGENTS.md for the verbatim mandates.
See the parent project for licensing terms.