Skip to content

Commit f1cc334

Browse files
deevusclaude
andcommitted
feat: add WriteFile to FilesystemService, deprecate on client
Move the filesystem.file_receive API call implementation from the transport layer (client.Client) to FilesystemService where it belongs alongside Stat and SetPermissions. The WriteFile method on FileCaller is marked deprecated — callers should use FilesystemService.WriteFile instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c655871 commit f1cc334

File tree

4 files changed

+136
-0
lines changed

4 files changed

+136
-0
lines changed

filesystem_service.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package truenas
22

33
import (
44
"context"
5+
"encoding/base64"
56
"encoding/json"
67
"fmt"
78
)
@@ -41,6 +42,36 @@ func (s *FilesystemService) Client() FileCaller {
4142
return s.client
4243
}
4344

45+
// WriteFile writes content to a file on the remote system via filesystem.file_receive.
46+
func (s *FilesystemService) WriteFile(ctx context.Context, path string, params WriteFileParams) error {
47+
b64Content := base64.StdEncoding.EncodeToString(params.Content)
48+
49+
uid := -1
50+
if params.UID != nil {
51+
uid = *params.UID
52+
}
53+
gid := -1
54+
if params.GID != nil {
55+
gid = *params.GID
56+
}
57+
58+
apiParams := []any{
59+
path,
60+
b64Content,
61+
map[string]any{
62+
"mode": int(params.Mode),
63+
"uid": uid,
64+
"gid": gid,
65+
},
66+
}
67+
68+
_, err := s.client.Call(ctx, "filesystem.file_receive", apiParams)
69+
if err != nil {
70+
return fmt.Errorf("write file %q: %w", path, err)
71+
}
72+
return nil
73+
}
74+
4475
// Stat returns filesystem stat information for the given path.
4576
// Mode is masked with 0o777 to strip file type bits.
4677
func (s *FilesystemService) Stat(ctx context.Context, path string) (*StatResult, error) {

filesystem_service_iface.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "context"
55
// FilesystemServiceAPI defines the interface for filesystem operations.
66
type FilesystemServiceAPI interface {
77
Client() FileCaller
8+
WriteFile(ctx context.Context, path string, params WriteFileParams) error
89
Stat(ctx context.Context, path string) (*StatResult, error)
910
SetPermissions(ctx context.Context, opts SetPermOpts) error
1011
}
@@ -16,6 +17,7 @@ var _ FilesystemServiceAPI = (*MockFilesystemService)(nil)
1617
// MockFilesystemService is a test double for FilesystemServiceAPI.
1718
type MockFilesystemService struct {
1819
ClientFunc func() FileCaller
20+
WriteFileFunc func(ctx context.Context, path string, params WriteFileParams) error
1921
StatFunc func(ctx context.Context, path string) (*StatResult, error)
2022
SetPermissionsFunc func(ctx context.Context, opts SetPermOpts) error
2123
}
@@ -27,6 +29,13 @@ func (m *MockFilesystemService) Client() FileCaller {
2729
return nil
2830
}
2931

32+
func (m *MockFilesystemService) WriteFile(ctx context.Context, path string, params WriteFileParams) error {
33+
if m.WriteFileFunc != nil {
34+
return m.WriteFileFunc(ctx, path, params)
35+
}
36+
return nil
37+
}
38+
3039
func (m *MockFilesystemService) Stat(ctx context.Context, path string) (*StatResult, error) {
3140
if m.StatFunc != nil {
3241
return m.StatFunc(ctx, path)

filesystem_service_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,101 @@ func sampleFileStatJSON() json.RawMessage {
1515
return json.RawMessage(`{"mode": 33188, "uid": 0, "gid": 0}`)
1616
}
1717

18+
func TestFilesystemService_WriteFile(t *testing.T) {
19+
mock := &mockFileCaller{
20+
mockAsyncCaller: mockAsyncCaller{
21+
mockCaller: mockCaller{
22+
callFunc: func(ctx context.Context, method string, params any) (json.RawMessage, error) {
23+
if method != "filesystem.file_receive" {
24+
t.Errorf("expected method filesystem.file_receive, got %s", method)
25+
}
26+
args := params.([]any)
27+
if args[0] != "/mnt/pool/test.txt" {
28+
t.Errorf("expected path /mnt/pool/test.txt, got %v", args[0])
29+
}
30+
// Content should be base64-encoded "hello"
31+
if args[1] != "aGVsbG8=" {
32+
t.Errorf("expected base64 'aGVsbG8=', got %v", args[1])
33+
}
34+
opts := args[2].(map[string]any)
35+
if opts["mode"] != int(0o644) {
36+
t.Errorf("expected mode %d, got %v", int(0o644), opts["mode"])
37+
}
38+
if opts["uid"] != -1 {
39+
t.Errorf("expected uid -1 (unset), got %v", opts["uid"])
40+
}
41+
if opts["gid"] != -1 {
42+
t.Errorf("expected gid -1 (unset), got %v", opts["gid"])
43+
}
44+
return nil, nil
45+
},
46+
},
47+
},
48+
}
49+
50+
svc := NewFilesystemService(mock, Version{})
51+
err := svc.WriteFile(context.Background(), "/mnt/pool/test.txt", WriteFileParams{
52+
Content: []byte("hello"),
53+
Mode: 0o644,
54+
})
55+
if err != nil {
56+
t.Fatalf("unexpected error: %v", err)
57+
}
58+
}
59+
60+
func TestFilesystemService_WriteFile_WithUID(t *testing.T) {
61+
mock := &mockFileCaller{
62+
mockAsyncCaller: mockAsyncCaller{
63+
mockCaller: mockCaller{
64+
callFunc: func(ctx context.Context, method string, params any) (json.RawMessage, error) {
65+
args := params.([]any)
66+
opts := args[2].(map[string]any)
67+
if opts["uid"] != 1000 {
68+
t.Errorf("expected uid 1000, got %v", opts["uid"])
69+
}
70+
if opts["gid"] != 1000 {
71+
t.Errorf("expected gid 1000, got %v", opts["gid"])
72+
}
73+
return nil, nil
74+
},
75+
},
76+
},
77+
}
78+
79+
svc := NewFilesystemService(mock, Version{})
80+
uid, gid := 1000, 1000
81+
err := svc.WriteFile(context.Background(), "/mnt/pool/test.txt", WriteFileParams{
82+
Content: []byte("hello"),
83+
Mode: 0o644,
84+
UID: &uid,
85+
GID: &gid,
86+
})
87+
if err != nil {
88+
t.Fatalf("unexpected error: %v", err)
89+
}
90+
}
91+
92+
func TestFilesystemService_WriteFile_Error(t *testing.T) {
93+
mock := &mockFileCaller{
94+
mockAsyncCaller: mockAsyncCaller{
95+
mockCaller: mockCaller{
96+
callFunc: func(ctx context.Context, method string, params any) (json.RawMessage, error) {
97+
return nil, errors.New("permission denied")
98+
},
99+
},
100+
},
101+
}
102+
103+
svc := NewFilesystemService(mock, Version{})
104+
err := svc.WriteFile(context.Background(), "/mnt/pool/test.txt", WriteFileParams{
105+
Content: []byte("hello"),
106+
Mode: 0o644,
107+
})
108+
if err == nil {
109+
t.Fatal("expected error")
110+
}
111+
}
112+
18113
func TestFilesystemService_Stat(t *testing.T) {
19114
mock := &mockFileCaller{
20115
mockAsyncCaller: mockAsyncCaller{

interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type AsyncCaller interface {
2222
// Embeds AsyncCaller because filesystem.setperm is job-based.
2323
type FileCaller interface {
2424
AsyncCaller
25+
// Deprecated: Use FilesystemService.WriteFile instead.
2526
WriteFile(ctx context.Context, path string, params WriteFileParams) error
2627
ReadFile(ctx context.Context, path string) ([]byte, error)
2728
DeleteFile(ctx context.Context, path string) error

0 commit comments

Comments
 (0)