The Filesystem module follows the principle of protocol-agnostic file access. Consumers interact with a single client.Client interface regardless of whether files reside on a local disk, an SMB share, an FTP server, a WebDAV endpoint, or an NFS mount. The factory pattern decouples client creation from usage, allowing storage backends to be configured at runtime.
graph TB
Consumer["Consumer (catalog-api)"]
Factory["factory.DefaultFactory"]
Interface["client.Client Interface"]
Consumer -->|"CreateClient(config)"| Factory
Factory -->|"returns"| Interface
Interface --- SMB["smb.Client"]
Interface --- FTP["ftp.Client"]
Interface --- NFS["nfs.Client"]
Interface --- WebDAV["webdav.Client"]
Interface --- Local["local.Client"]
SMB -->|"go-smb2"| SMBServer["SMB Server"]
FTP -->|"jlaffaye/ftp"| FTPServer["FTP Server"]
NFS -->|"syscall.Mount"| NFSServer["NFS Server"]
WebDAV -->|"net/http"| WebDAVServer["WebDAV Server"]
Local -->|"os package"| Disk["Local Disk"]
The DefaultFactory maps protocol strings to adapter constructors. It extracts typed configuration values from the generic Settings map and passes protocol-specific Config structs to each adapter.
sequenceDiagram
participant C as Consumer
participant F as DefaultFactory
participant H as GetStringSetting/GetIntSetting
participant A as Protocol Adapter
C->>F: CreateClient(StorageConfig{Protocol: "smb", Settings: {...}})
F->>F: switch config.Protocol
F->>H: Extract host, port, share, username, password, domain
H-->>F: Typed values with defaults
F->>A: smb.NewSMBClient(smbConfig)
A-->>F: *smb.Client (as client.Client)
F-->>C: client.Client, nil
StorageConfig.Protocol --> Adapter Constructor
"smb" --> smb.NewSMBClient(Config{Host, Port, Share, Username, Password, Domain})
"ftp" --> ftp.NewFTPClient(Config{Host, Port, Username, Password, Path})
"nfs" --> nfs.NewNFSClient(Config{Host, Path, MountPoint, Options}) [Linux only]
"webdav" --> webdav.NewWebDAVClient(Config{URL, Username, Password, Path})
"local" --> local.NewLocalClient(Config{BasePath})
classDiagram
class Client {
<<interface>>
+Connect(ctx) error
+Disconnect(ctx) error
+IsConnected() bool
+TestConnection(ctx) error
+ReadFile(ctx, path) ReadCloser, error
+WriteFile(ctx, path, data) error
+GetFileInfo(ctx, path) *FileInfo, error
+FileExists(ctx, path) bool, error
+DeleteFile(ctx, path) error
+CopyFile(ctx, src, dst) error
+ListDirectory(ctx, path) []*FileInfo, error
+CreateDirectory(ctx, path) error
+DeleteDirectory(ctx, path) error
+GetProtocol() string
+GetConfig() interface{}
}
class Factory {
<<interface>>
+CreateClient(config) Client, error
+SupportedProtocols() []string
}
class ConnectionPool {
<<interface>>
+GetClient(config) Client, error
+ReturnClient(client) error
+CloseAll() error
}
class FileInfo {
+Name string
+Size int64
+ModTime time.Time
+IsDir bool
+Mode os.FileMode
+Path string
}
class StorageConfig {
+ID string
+Name string
+Protocol string
+Enabled bool
+MaxDepth int
+Settings map[string]interface{}
+CreatedAt time.Time
+UpdatedAt time.Time
}
class CopyOperation {
+SourcePath string
+DestinationPath string
+OverwriteExisting bool
}
class CopyResult {
+Success bool
+BytesCopied int64
+Error error
+TimeTaken time.Duration
}
Factory --> Client : creates
Factory --> StorageConfig : uses
ConnectionPool --> Client : manages
Client --> FileInfo : returns
Client ..> CopyOperation : uses
Client ..> CopyResult : produces
stateDiagram-v2
[*] --> Disconnected: NewXClient(config)
Disconnected --> Connected: Connect(ctx)
Connected --> Disconnected: Disconnect(ctx)
Connected --> Connected: TestConnection(ctx)
Connected --> Connected: File/Directory Operations
Disconnected --> [*]
note right of Connected
All file and directory operations
require IsConnected() == true.
Operations return "not connected"
error if called while disconnected.
end note
Uses go-smb2 library for SMB2/3 protocol support.
- Connect: TCP dial -> NTLM authentication -> mount share
- Disconnect: Unmount share -> logoff session -> close TCP connection (collects errors from each step)
- IsConnected: Checks that
share,session, andconnare all non-nil - File ops: Delegate to
smb2.Sharemethods (Open,Create,Stat,ReadDir,Remove,Mkdir)
Uses jlaffaye/ftp library.
- Connect: Dial with 30-second timeout -> login -> change to base directory
- Disconnect: Sends
QUITcommand - Path resolution: Prepends
Config.Pathprefix to all relative paths - GetFileInfo: Uses
FileSizefor size,Listto detect directories; modification time defaults totime.Now() - WriteFile: Auto-creates parent directory before
STOR
Linux-only. Uses syscall.Mount/syscall.Unmount directly.
- Platform gating: Build tags
//go:build linuxand//go:build !linuxinfactory/nfs_linux.goandfactory/nfs_other.go - Connect: Creates mount point directory ->
syscall.Mount(host:path, mountpoint, "nfs", 0, options) - Disconnect:
syscall.Unmount(mountpoint, 0) - File ops: Standard
ospackage operations on the mount point (same as local, but on NFS-mounted path) - Mount detection: Checks
/proc/mountsfor presence of mount
Pure HTTP implementation using net/http.
- Connect: Sends
PROPFINDwithDepth: 0to verify server accessibility - ReadFile:
GETrequest, returnsresp.Bodyasio.ReadCloser - WriteFile:
PUTrequest with data as request body - GetFileInfo:
HEADrequest, parsesContent-LengthandLast-Modifiedheaders - ListDirectory:
PROPFINDwithDepth: 1, parses DAV XML response (manual string parsing) - CreateDirectory:
MKCOLrequest - DeleteFile/DeleteDirectory:
DELETErequest - CopyFile:
COPYrequest withDestinationheader - Authentication: HTTP Basic Auth when username is configured
Direct local filesystem access via Go os package.
- Connect: Validates that
BasePathexists and is a directory - Disconnect: No-op (sets
connected = false) - Path resolution:
filepath.Clean+ strip..+filepath.Join(basePath, path) - WriteFile: Auto-creates parent directories with
os.MkdirAll - CopyFile: Opens source -> creates destination ->
io.Copy
All adapters sanitize paths before use:
graph LR
Input["User path: ../../etc/passwd"]
Clean["filepath.Clean: ../../etc/passwd"]
Strip["Strip '..': etc/passwd"]
Join["filepath.Join(basePath, 'etc/passwd')"]
Result["/data/media/etc/passwd"]
Input --> Clean --> Strip --> Join --> Result
This prevents path traversal attacks that could escape the configured base directory.
graph LR
factory --> client
factory --> smb
factory --> ftp
factory --> nfs
factory --> webdav
factory --> local
smb --> client
ftp --> client
nfs --> client
webdav --> client
local --> client
smb -.->|"go-smb2"| ext1["github.com/hirochachacha/go-smb2"]
ftp -.->|"jlaffaye/ftp"| ext2["github.com/jlaffaye/ftp"]
style client fill:#e1f5fe
style factory fill:#fff3e0
The client package has zero external dependencies. Protocol adapters depend on client for types. The factory package depends on all protocol adapters. External library dependencies are limited to go-smb2 (SMB) and jlaffaye/ftp (FTP).