Skip to content

Commit

Permalink
Adding file.Put operation (#27)
Browse files Browse the repository at this point in the history
This enables gnoigo to perform file.Put operations.
  • Loading branch information
alshabib authored Mar 20, 2024
1 parent fe29de7 commit 4dbbf14
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 0 deletions.
124 changes: 124 additions & 0 deletions file/file.go
Original file line number Diff line number Diff line change
@@ -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()
}
156 changes: 156 additions & 0 deletions file/file_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions file/testdata/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some really important data

0 comments on commit 4dbbf14

Please sign in to comment.