feat: implement ListFiles and DownloadFile RPCs with documentation updates in GRPC.md

This commit is contained in:
2026-07-17 18:14:51 +09:00
parent 27d5b59106
commit 8f183d597e
6 changed files with 569 additions and 65 deletions
+62
View File
@@ -4,7 +4,9 @@ import (
"context"
"fmt"
"grpccanary/examples/grpcentity/protoapi"
"io"
"math/rand"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -64,6 +66,31 @@ func AskUploadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName st
return stream.CloseAndRecv()
}
func AskListFiles(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.FileList, error) {
return m.ListFiles(ctx, &protoapi.EmptyRequest{})
}
func AskDownloadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string) ([]byte, error) {
stream, err := m.DownloadFile(ctx, &protoapi.DownloadRequest{FileName: fileName})
if err != nil {
return nil, err
}
var buffer []byte
for {
chunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
buffer = append(buffer, chunk.GetContent()...)
}
return buffer, nil
}
func ClientRun(addr string) {
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
@@ -109,4 +136,39 @@ func ClientRun(addr string) {
fmt.Println("Upload Success:", status.Success)
fmt.Println("Upload Message:", status.Message)
fmt.Printf("Uploaded Bytes: %d bytes\n", status.BytesUploaded)
// 5단계: 파일 리스트 조회 실행 예제
fmt.Println("Querying uploaded files metadata from Server...")
list, err := AskListFiles(context.Background(), client)
if err != nil {
fmt.Println("Failed to list files:", err)
return
}
for i, f := range list.GetFiles() {
fmt.Printf("[%d] Name: %s, Size: %d bytes, UploadedAt: %s\n",
i+1, f.GetFileName(), f.GetFileSize(), time.Unix(f.GetUploadedAt(), 0).Format("2006-01-02 15:04:05"))
}
// 6단계: 파일 다운로드 스트리밍 실행 예제
fmt.Println("Downloading file 'firmware.bin' via Server Streaming...")
downloadedData, err := AskDownloadFile(context.Background(), client, "firmware.bin")
if err != nil {
fmt.Println("Failed to download file:", err)
return
}
fmt.Printf("Download completed. Received %d bytes.\n", len(downloadedData))
// 데이터 정합성(Integrity) 검증
isMatch := true
if len(dummyData) != len(downloadedData) {
isMatch = false
} else {
for i := range dummyData {
if dummyData[i] != downloadedData[i] {
isMatch = false
break
}
}
}
fmt.Printf("Data Integrity Checked (Upload vs Download matches?): %t\n", isMatch)
}
+18
View File
@@ -7,6 +7,8 @@ service IoTService {
rpc UpdateSensingData (SensingData) returns (SensingResponse);
rpc GetRandomPass (RequestPass) returns (RandomPass);
rpc UploadFile (stream FileChunk) returns (UploadStatus);
rpc ListFiles (EmptyRequest) returns (FileList);
rpc DownloadFile (DownloadRequest) returns (stream FileChunk);
}
message FileChunk {
@@ -46,4 +48,20 @@ message RequestPass {
message RandomPass {
string Password = 1;
}
message EmptyRequest {}
message FileMetadata {
string FileName = 1;
int64 FileSize = 2;
int64 UploadedAt = 3;
}
message FileList {
repeated FileMetadata Files = 1;
}
message DownloadRequest {
string FileName = 1;
}
+224 -17
View File
@@ -429,6 +429,190 @@ func (x *RandomPass) GetPassword() string {
return ""
}
type EmptyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EmptyRequest) Reset() {
*x = EmptyRequest{}
mi := &file_protoapi_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EmptyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EmptyRequest) ProtoMessage() {}
func (x *EmptyRequest) ProtoReflect() protoreflect.Message {
mi := &file_protoapi_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EmptyRequest.ProtoReflect.Descriptor instead.
func (*EmptyRequest) Descriptor() ([]byte, []int) {
return file_protoapi_proto_rawDescGZIP(), []int{8}
}
type FileMetadata struct {
state protoimpl.MessageState `protogen:"open.v1"`
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
FileSize int64 `protobuf:"varint,2,opt,name=FileSize,proto3" json:"FileSize,omitempty"`
UploadedAt int64 `protobuf:"varint,3,opt,name=UploadedAt,proto3" json:"UploadedAt,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FileMetadata) Reset() {
*x = FileMetadata{}
mi := &file_protoapi_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FileMetadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FileMetadata) ProtoMessage() {}
func (x *FileMetadata) ProtoReflect() protoreflect.Message {
mi := &file_protoapi_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FileMetadata.ProtoReflect.Descriptor instead.
func (*FileMetadata) Descriptor() ([]byte, []int) {
return file_protoapi_proto_rawDescGZIP(), []int{9}
}
func (x *FileMetadata) GetFileName() string {
if x != nil {
return x.FileName
}
return ""
}
func (x *FileMetadata) GetFileSize() int64 {
if x != nil {
return x.FileSize
}
return 0
}
func (x *FileMetadata) GetUploadedAt() int64 {
if x != nil {
return x.UploadedAt
}
return 0
}
type FileList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Files []*FileMetadata `protobuf:"bytes,1,rep,name=Files,proto3" json:"Files,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FileList) Reset() {
*x = FileList{}
mi := &file_protoapi_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FileList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FileList) ProtoMessage() {}
func (x *FileList) ProtoReflect() protoreflect.Message {
mi := &file_protoapi_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FileList.ProtoReflect.Descriptor instead.
func (*FileList) Descriptor() ([]byte, []int) {
return file_protoapi_proto_rawDescGZIP(), []int{10}
}
func (x *FileList) GetFiles() []*FileMetadata {
if x != nil {
return x.Files
}
return nil
}
type DownloadRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DownloadRequest) Reset() {
*x = DownloadRequest{}
mi := &file_protoapi_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DownloadRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DownloadRequest) ProtoMessage() {}
func (x *DownloadRequest) ProtoReflect() protoreflect.Message {
mi := &file_protoapi_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DownloadRequest.ProtoReflect.Descriptor instead.
func (*DownloadRequest) Descriptor() ([]byte, []int) {
return file_protoapi_proto_rawDescGZIP(), []int{11}
}
func (x *DownloadRequest) GetFileName() string {
if x != nil {
return x.FileName
}
return ""
}
var File_protoapi_proto protoreflect.FileDescriptor
const file_protoapi_proto_rawDesc = "" +
@@ -457,7 +641,18 @@ const file_protoapi_proto_rawDesc = "" +
"\x06Length\x18\b \x01(\x03R\x06Length\"(\n" +
"\n" +
"RandomPass\x12\x1a\n" +
"\bPassword\x18\x01 \x01(\tR\bPassword2\xc0\x01\n" +
"\bPassword\x18\x01 \x01(\tR\bPassword\"\x0e\n" +
"\fEmptyRequest\"f\n" +
"\fFileMetadata\x12\x1a\n" +
"\bFileName\x18\x01 \x01(\tR\bFileName\x12\x1a\n" +
"\bFileSize\x18\x02 \x01(\x03R\bFileSize\x12\x1e\n" +
"\n" +
"UploadedAt\x18\x03 \x01(\x03R\n" +
"UploadedAt\"/\n" +
"\bFileList\x12#\n" +
"\x05Files\x18\x01 \x03(\v2\r.FileMetadataR\x05Files\"-\n" +
"\x0fDownloadRequest\x12\x1a\n" +
"\bFileName\x18\x01 \x01(\tR\bFileName2\x97\x02\n" +
"\n" +
"IoTService\x12&\n" +
"\aGetDate\x12\x10.RequestDateTime\x1a\t.DateTime\x123\n" +
@@ -465,7 +660,10 @@ const file_protoapi_proto_rawDesc = "" +
"\rGetRandomPass\x12\f.RequestPass\x1a\v.RandomPass\x12)\n" +
"\n" +
"UploadFile\x12\n" +
".FileChunk\x1a\r.UploadStatus(\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
".FileChunk\x1a\r.UploadStatus(\x01\x12%\n" +
"\tListFiles\x12\r.EmptyRequest\x1a\t.FileList\x12.\n" +
"\fDownloadFile\x12\x10.DownloadRequest\x1a\n" +
".FileChunk0\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
var (
file_protoapi_proto_rawDescOnce sync.Once
@@ -479,7 +677,7 @@ func file_protoapi_proto_rawDescGZIP() []byte {
return file_protoapi_proto_rawDescData
}
var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_protoapi_proto_goTypes = []any{
(*FileChunk)(nil), // 0: FileChunk
(*UploadStatus)(nil), // 1: UploadStatus
@@ -489,21 +687,30 @@ var file_protoapi_proto_goTypes = []any{
(*RequestDateTime)(nil), // 5: RequestDateTime
(*RequestPass)(nil), // 6: RequestPass
(*RandomPass)(nil), // 7: RandomPass
(*EmptyRequest)(nil), // 8: EmptyRequest
(*FileMetadata)(nil), // 9: FileMetadata
(*FileList)(nil), // 10: FileList
(*DownloadRequest)(nil), // 11: DownloadRequest
}
var file_protoapi_proto_depIdxs = []int32{
5, // 0: IoTService.GetDate:input_type -> RequestDateTime
2, // 1: IoTService.UpdateSensingData:input_type -> SensingData
6, // 2: IoTService.GetRandomPass:input_type -> RequestPass
0, // 3: IoTService.UploadFile:input_type -> FileChunk
4, // 4: IoTService.GetDate:output_type -> DateTime
3, // 5: IoTService.UpdateSensingData:output_type -> SensingResponse
7, // 6: IoTService.GetRandomPass:output_type -> RandomPass
1, // 7: IoTService.UploadFile:output_type -> UploadStatus
4, // [4:8] is the sub-list for method output_type
0, // [0:4] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
9, // 0: FileList.Files:type_name -> FileMetadata
5, // 1: IoTService.GetDate:input_type -> RequestDateTime
2, // 2: IoTService.UpdateSensingData:input_type -> SensingData
6, // 3: IoTService.GetRandomPass:input_type -> RequestPass
0, // 4: IoTService.UploadFile:input_type -> FileChunk
8, // 5: IoTService.ListFiles:input_type -> EmptyRequest
11, // 6: IoTService.DownloadFile:input_type -> DownloadRequest
4, // 7: IoTService.GetDate:output_type -> DateTime
3, // 8: IoTService.UpdateSensingData:output_type -> SensingResponse
7, // 9: IoTService.GetRandomPass:output_type -> RandomPass
1, // 10: IoTService.UploadFile:output_type -> UploadStatus
10, // 11: IoTService.ListFiles:output_type -> FileList
0, // 12: IoTService.DownloadFile:output_type -> FileChunk
7, // [7:13] is the sub-list for method output_type
1, // [1:7] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_protoapi_proto_init() }
@@ -517,7 +724,7 @@ func file_protoapi_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
@@ -23,6 +23,8 @@ const (
IoTService_UpdateSensingData_FullMethodName = "/IoTService/UpdateSensingData"
IoTService_GetRandomPass_FullMethodName = "/IoTService/GetRandomPass"
IoTService_UploadFile_FullMethodName = "/IoTService/UploadFile"
IoTService_ListFiles_FullMethodName = "/IoTService/ListFiles"
IoTService_DownloadFile_FullMethodName = "/IoTService/DownloadFile"
)
// IoTServiceClient is the client API for IoTService service.
@@ -33,6 +35,8 @@ type IoTServiceClient interface {
UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error)
GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error)
UploadFile(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[FileChunk, UploadStatus], error)
ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error)
DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error)
}
type ioTServiceClient struct {
@@ -86,6 +90,35 @@ func (c *ioTServiceClient) UploadFile(ctx context.Context, opts ...grpc.CallOpti
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type IoTService_UploadFileClient = grpc.ClientStreamingClient[FileChunk, UploadStatus]
func (c *ioTServiceClient) ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(FileList)
err := c.cc.Invoke(ctx, IoTService_ListFiles_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *ioTServiceClient) DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &IoTService_ServiceDesc.Streams[1], IoTService_DownloadFile_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[DownloadRequest, FileChunk]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type IoTService_DownloadFileClient = grpc.ServerStreamingClient[FileChunk]
// IoTServiceServer is the server API for IoTService service.
// All implementations must embed UnimplementedIoTServiceServer
// for forward compatibility.
@@ -94,6 +127,8 @@ type IoTServiceServer interface {
UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error)
GetRandomPass(context.Context, *RequestPass) (*RandomPass, error)
UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error
ListFiles(context.Context, *EmptyRequest) (*FileList, error)
DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error
mustEmbedUnimplementedIoTServiceServer()
}
@@ -116,6 +151,12 @@ func (UnimplementedIoTServiceServer) GetRandomPass(context.Context, *RequestPass
func (UnimplementedIoTServiceServer) UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error {
return status.Error(codes.Unimplemented, "method UploadFile not implemented")
}
func (UnimplementedIoTServiceServer) ListFiles(context.Context, *EmptyRequest) (*FileList, error) {
return nil, status.Error(codes.Unimplemented, "method ListFiles not implemented")
}
func (UnimplementedIoTServiceServer) DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error {
return status.Error(codes.Unimplemented, "method DownloadFile not implemented")
}
func (UnimplementedIoTServiceServer) mustEmbedUnimplementedIoTServiceServer() {}
func (UnimplementedIoTServiceServer) testEmbeddedByValue() {}
@@ -198,6 +239,35 @@ func _IoTService_UploadFile_Handler(srv interface{}, stream grpc.ServerStream) e
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type IoTService_UploadFileServer = grpc.ClientStreamingServer[FileChunk, UploadStatus]
func _IoTService_ListFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EmptyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IoTServiceServer).ListFiles(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IoTService_ListFiles_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IoTServiceServer).ListFiles(ctx, req.(*EmptyRequest))
}
return interceptor(ctx, in, info, handler)
}
func _IoTService_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(DownloadRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(IoTServiceServer).DownloadFile(m, &grpc.GenericServerStream[DownloadRequest, FileChunk]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type IoTService_DownloadFileServer = grpc.ServerStreamingServer[FileChunk]
// IoTService_ServiceDesc is the grpc.ServiceDesc for IoTService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -217,6 +287,10 @@ var IoTService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetRandomPass",
Handler: _IoTService_GetRandomPass_Handler,
},
{
MethodName: "ListFiles",
Handler: _IoTService_ListFiles_Handler,
},
},
Streams: []grpc.StreamDesc{
{
@@ -224,6 +298,11 @@ var IoTService_ServiceDesc = grpc.ServiceDesc{
Handler: _IoTService_UploadFile_Handler,
ClientStreams: true,
},
{
StreamName: "DownloadFile",
Handler: _IoTService_DownloadFile_Handler,
ServerStreams: true,
},
},
Metadata: "protoapi.proto",
}
+73
View File
@@ -7,16 +7,30 @@ import (
"io"
"math/rand"
"net"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
)
var min = 0
var max = 100
var port = ":8080"
type UploadedFile struct {
FileName string
Content []byte
UploadedAt int64
}
var (
fileStore = make(map[string]*UploadedFile)
storeMu sync.RWMutex
)
func random(min, max int, src rand.Source) int {
return rand.New(src).Intn(max-min) + min
}
@@ -89,11 +103,23 @@ func (IoTServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) (*p
func (IoTServer) UploadFile(stream protoapi.IoTService_UploadFileServer) error {
var totalBytes int64
var fileName string
var buffer []byte
for {
chunk, err := stream.Recv()
if err == io.EOF {
fmt.Printf("File upload completed. Received %d bytes for file '%s'\n", totalBytes, fileName)
if fileName != "" {
storeMu.Lock()
fileStore[fileName] = &UploadedFile{
FileName: fileName,
Content: buffer,
UploadedAt: time.Now().Unix(),
}
storeMu.Unlock()
}
return stream.SendAndClose(&protoapi.UploadStatus{
Success: true,
Message: fmt.Sprintf("File '%s' uploaded successfully.", fileName),
@@ -108,10 +134,57 @@ func (IoTServer) UploadFile(stream protoapi.IoTService_UploadFileServer) error {
if fileName == "" {
fileName = chunk.GetFileName()
}
buffer = append(buffer, chunk.GetContent()...)
totalBytes += int64(len(chunk.GetContent()))
}
}
func (IoTServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*protoapi.FileList, error) {
storeMu.RLock()
defer storeMu.RUnlock()
var files []*protoapi.FileMetadata
for _, f := range fileStore {
files = append(files, &protoapi.FileMetadata{
FileName: f.FileName,
FileSize: int64(len(f.Content)),
UploadedAt: f.UploadedAt,
})
}
return &protoapi.FileList{Files: files}, nil
}
func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTService_DownloadFileServer) error {
storeMu.RLock()
f, exists := fileStore[r.GetFileName()]
storeMu.RUnlock()
if !exists {
return status.Errorf(codes.NotFound, "file %s not found", r.GetFileName())
}
chunkSize := 1024 // 1KB 청크 단위
totalBytes := len(f.Content)
for i := 0; i < totalBytes; i += chunkSize {
end := i + chunkSize
if end > totalBytes {
end = totalBytes
}
err := stream.Send(&protoapi.FileChunk{
FileName: f.FileName,
Content: f.Content[i:end],
})
if err != nil {
return err
}
}
return nil
}
func ServerRun(addr string) {
server := grpc.NewServer()
var iotServer IoTServer