diff --git a/file/file.go b/file/file.go new file mode 100644 index 0000000..1f17b28 --- /dev/null +++ b/file/file.go @@ -0,0 +1,124 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//     https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package file provides gNOI file operations. +package file + +import ( + "context" + "crypto/sha256" + "io" + "os" + + fpb "github.com/openconfig/gnoi/file" + tpb "github.com/openconfig/gnoi/types" + "github.com/openconfig/gnoigo/internal" +) + +const ( + // chunkSize is the maximal size of a file chunk as defined by the spec. + chunkSize = 64000 +) + +// PutOperation represents the parameters of a Put operation. +type PutOperation struct { + sourceFile string + req *fpb.PutRequest +} + +// NewPutOperation creates an empty PutOperation. +func NewPutOperation() *PutOperation { + return &PutOperation{ + req: &fpb.PutRequest{ + Request: &fpb.PutRequest_Open{ + Open: &fpb.PutRequest_Details{}, + }, + }, + } +} + +// Perms specifies the permissions to apply to the copied file. +func (p *PutOperation) Perms(perms uint32) *PutOperation { + p.req.GetOpen().Permissions = perms + return p +} + +// RemoteFile specifies the name of the file on the target. +func (p *PutOperation) RemoteFile(file string) *PutOperation { + p.req.GetOpen().RemoteFile = file + return p +} + +// SourceFile represents the source file to copy. +func (p *PutOperation) SourceFile(file string) *PutOperation { + p.sourceFile = file + return p +} + +// Execute executes the Put operation. +func (p *PutOperation) Execute(ctx context.Context, c *internal.Clients) (*fpb.PutResponse, error) { + pclient, err := c.File().Put(ctx) + if err != nil { + return nil, err + } + + f, err := os.Open(p.sourceFile) + if err != nil { + return nil, err + } + + if err := pclient.Send(p.req); err != nil { + return nil, err + } + + hasher := sha256.New() + buf := make([]byte, chunkSize) + for i, done := 0, false; !done; i++ { + n, err := f.ReadAt(buf, int64(i*chunkSize)) + if err != nil { + if err != io.EOF { + return nil, err + } + done = true + } + content := buf[:n] + + if _, err = hasher.Write(content); err != nil { + return nil, err + } + + req := &fpb.PutRequest{ + Request: &fpb.PutRequest_Contents{ + Contents: content, + }, + } + if err := pclient.Send(req); err != nil { + return nil, err + } + } + + req := &fpb.PutRequest{ + Request: &fpb.PutRequest_Hash{ + Hash: &tpb.HashType{ + Hash: hasher.Sum(nil), + Method: tpb.HashType_SHA256, + }, + }, + } + if err := pclient.Send(req); err != nil { + return nil, err + } + + return pclient.CloseAndRecv() +} diff --git a/file/file_test.go b/file/file_test.go new file mode 100644 index 0000000..a34180e --- /dev/null +++ b/file/file_test.go @@ -0,0 +1,156 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//     https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package file_test + +import ( + "context" + "crypto/sha256" + "testing" + + "github.com/google/go-cmp/cmp" + fpb "github.com/openconfig/gnoi/file" + "github.com/openconfig/gnoi/types" + "github.com/openconfig/gnoigo/file" + "github.com/openconfig/gnoigo/internal" + "google.golang.org/grpc" + "google.golang.org/protobuf/testing/protocmp" +) + +type fakeFileClient struct { + fpb.FileClient + PutFn func(ctx context.Context, opts ...grpc.CallOption) (fpb.File_PutClient, error) +} + +func (f *fakeFileClient) File() fpb.FileClient { + return f +} + +func (f *fakeFileClient) Put(ctx context.Context, opts ...grpc.CallOption) (fpb.File_PutClient, error) { + return f.PutFn(ctx, opts...) +} + +type fakePutClient struct { + fpb.File_PutClient + gotReq []*fpb.PutRequest +} + +func (fc *fakePutClient) Send(req *fpb.PutRequest) error { + fc.gotReq = append(fc.gotReq, req) + return nil +} + +func (fc *fakePutClient) Recv() (*fpb.PutResponse, error) { + return &fpb.PutResponse{}, nil +} + +func (fv *fakePutClient) CloseAndRecv() (*fpb.PutResponse, error) { + return &fpb.PutResponse{}, nil +} + +func (*fakePutClient) CloseSend() error { + return nil +} + +func TestPut(t *testing.T) { + hash := sha256.New() + _, err := hash.Write([]byte(`some really important data`)) + if err != nil { + t.Fatalf("Unable to hash string: %v", err) + } + tests := []struct { + desc string + op *file.PutOperation + wantReq []*fpb.PutRequest + wantErr bool + }{ + { + desc: "put-with-no-file", + op: file.NewPutOperation(), + wantErr: true, + }, + { + desc: "put-with-file", + op: file.NewPutOperation().SourceFile("testdata/data.txt"), + wantReq: []*fpb.PutRequest{ + { + Request: &fpb.PutRequest_Open{ + Open: &fpb.PutRequest_Details{}, + }, + }, + { + Request: &fpb.PutRequest_Contents{ + Contents: []byte(`some really important data`), + }, + }, + { + Request: &fpb.PutRequest_Hash{ + Hash: &types.HashType{ + Method: types.HashType_SHA256, + Hash: hash.Sum(nil), + }, + }, + }, + }, + }, + { + desc: "put-with-all-details", + op: file.NewPutOperation().SourceFile("testdata/data.txt").RemoteFile("/tmp/here").Perms(644), + wantReq: []*fpb.PutRequest{ + { + Request: &fpb.PutRequest_Open{ + Open: &fpb.PutRequest_Details{ + RemoteFile: "/tmp/here", + Permissions: 644, + }, + }, + }, + { + Request: &fpb.PutRequest_Contents{ + Contents: []byte(`some really important data`), + }, + }, + { + Request: &fpb.PutRequest_Hash{ + Hash: &types.HashType{ + Method: types.HashType_SHA256, + Hash: hash.Sum(nil), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + fpc := &fakePutClient{} + var fakeClient internal.Clients + fakeClient.FileClient = &fakeFileClient{ + PutFn: func(ctx context.Context, opts ...grpc.CallOption) (fpb.File_PutClient, error) { + return fpc, nil + }, + } + + _, err := tt.op.Execute(context.Background(), &fakeClient) + if (err != nil) != tt.wantErr { + t.Errorf("Execute() got unexpected error %v", err) + } + + if diff := cmp.Diff(fpc.gotReq, tt.wantReq, protocmp.Transform()); diff != "" { + t.Errorf("Execute returned diff (-got, +want):\n%s", diff) + } + }) + } +} diff --git a/file/testdata/data.txt b/file/testdata/data.txt new file mode 100644 index 0000000..3e499df --- /dev/null +++ b/file/testdata/data.txt @@ -0,0 +1 @@ +some really important data \ No newline at end of file