diff options
| author | pablu <me@pablu.de> | 2026-08-05 13:06:36 +0200 |
|---|---|---|
| committer | pablu <me@pablu.de> | 2026-08-05 16:11:59 +0200 |
| commit | aa9d403d09186b5b0cc9da3b322c1ba53c0f832a (patch) | |
| tree | 02ce07ae0a15e01f3f03bb469ccc2cb9fede56cc /client.go | |
| parent | 67916c5d1c7001bab5bf08b6fec3a7f14ea4dd64 (diff) | |
feat: create working client
Diffstat (limited to 'client.go')
| -rw-r--r-- | client.go | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/client.go b/client.go new file mode 100644 index 0000000..f799f07 --- /dev/null +++ b/client.go @@ -0,0 +1,293 @@ +package zw + +import ( + "context" + "errors" + "fmt" + "io" + "net/url" + + zwdaemonv1 "git.pablu.de/pablu/zw-go/gen/zwdaemon/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const supportedAPIMajor uint32 = 1 + +func optionalString(value string) *string { + if value == "" { + return nil + } + return &value +} + +type Client struct { + conn *grpc.ClientConn + rpc zwdaemonv1.ZwDaemonServiceClient +} + +func Connect(ctx context.Context, socketPath string) (*Client, error) { + target := (&url.URL{ + Scheme: "unix", + Path: socketPath, + }).String() + + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, fmt.Errorf("create daemon client: %w", err) + } + + client := &Client{ + conn: conn, + rpc: zwdaemonv1.NewZwDaemonServiceClient(conn), + } + + info, err := client.ServerInfo(ctx) + if err != nil { + _ = conn.Close() + return nil, err + } + + if info.APIMajor != supportedAPIMajor { + _ = conn.Close() + return nil, &IncompatibleAPIVersionError{ + ServerMajor: info.APIMajor, + ServerMinor: info.APIMinor, + SupportedMajor: supportedAPIMajor, + } + } + + return client, nil +} + +func (c *Client) Close() error { + return c.conn.Close() +} + +func (c *Client) ServerInfo(ctx context.Context) (ServerInfo, error) { + response, err := c.rpc.GetServerInfo(ctx, &zwdaemonv1.GetServerInfoRequest{}) + if err != nil { + return ServerInfo{}, convertRPCError(err) + } + + return ServerInfo{ + APIMajor: response.ApiMajor, + APIMinor: response.ApiMinor, + DaemonVersion: response.DaemonVersion, + }, nil +} + +func (c *Client) Create(ctx context.Context, vaultPath string, password string) error { + _, err := c.rpc.Create(ctx, &zwdaemonv1.CreateRequest{ + VaultPath: vaultPath, + Password: password, + }) + if err != nil { + return convertRPCError(err) + } + return nil +} + +func (c *Client) Unseal(ctx context.Context, vaultPath string, password string) error { + _, err := c.rpc.Unseal(ctx, &zwdaemonv1.UnsealRequest{ + VaultPath: vaultPath, + Password: password, + }) + if err != nil { + return convertRPCError(err) + } + return nil +} + +func (c *Client) Seal(ctx context.Context) error { + _, err := c.rpc.Seal(ctx, &zwdaemonv1.SealRequest{}) + if err != nil { + return convertRPCError(err) + } + return nil +} + +func (c *Client) ListFiles(ctx context.Context, filter ListFilter) ([]ListedFile, error) { + response, err := c.rpc.ListFiles(ctx, &zwdaemonv1.ListFilesRequest{ + Prefix: optionalString(filter.Prefix), + Contains: optionalString(filter.Contains), + }) + if err != nil { + return nil, convertRPCError(err) + } + + files := make([]ListedFile, len(response.GetFiles())) + for index, file := range response.GetFiles() { + files[index] = ListedFile{ + LogicalPath: file.GetLogicalPath(), + Size: file.GetSize(), + } + } + + return files, nil +} + +func (c *Client) ListPaths(ctx context.Context) ([]string, error) { + files, err := c.ListFiles(ctx, ListFilter{}) + if err != nil { + return nil, err + } + + paths := make([]string, len(files)) + for index, file := range files { + paths[index] = file.LogicalPath + } + + return paths, nil +} + +func (c *Client) DeleteFile(ctx context.Context, logicalPath string) error { + _, err := c.rpc.DeleteFile(ctx, &zwdaemonv1.DeleteFileRequest{ + LogicalPath: logicalPath, + }) + if err != nil { + return convertRPCError(err) + } + return nil +} + +func (c *Client) Compact(ctx context.Context) error { + _, err := c.rpc.Compact(ctx, &zwdaemonv1.CompactRequest{}) + if err != nil { + return convertRPCError(err) + } + return nil +} + +func (c *Client) GetFile( + ctx context.Context, + logicalPath string, +) (io.ReadCloser, error) { + streamCtx, cancel := context.WithCancel(ctx) + + stream, err := c.rpc.GetFile(streamCtx, &zwdaemonv1.GetFileRequest{ + LogicalPath: logicalPath, + }) + if err != nil { + cancel() + return nil, convertRPCError(err) + } + + return &downloadReader{ + cancel: cancel, + recv: func() ([]byte, error) { + response, err := stream.Recv() + if err != nil { + return nil, err + } + return response.GetData(), nil + }, + }, nil +} + +func (c *Client) GetFileRange( + ctx context.Context, + logicalPath string, + start uint64, + exclusiveEnd uint64, +) (io.ReadCloser, error) { + streamCtx, cancel := context.WithCancel(ctx) + + stream, err := c.rpc.GetFileRange(streamCtx, &zwdaemonv1.GetFileRangeRequest{ + LogicalPath: logicalPath, + Start: start, + ExclusiveEnd: exclusiveEnd, + }) + if err != nil { + cancel() + return nil, convertRPCError(err) + } + + return &downloadReader{ + cancel: cancel, + recv: func() ([]byte, error) { + response, err := stream.Recv() + if err != nil { + return nil, err + } + return response.GetData(), nil + }, + }, nil +} + +func (c *Client) AddFile( + ctx context.Context, + logicalPath string, + source io.Reader, + sourceLen uint64, +) error { + uploadCtx, cancel := context.WithCancel(ctx) + defer cancel() + + stream, err := c.rpc.AddFile(uploadCtx) + if err != nil { + return convertRPCError(err) + } + + send := func(request *zwdaemonv1.AddFileRequest) error { + err := stream.Send(request) + if err == nil { + return nil + } + + if errors.Is(err, io.EOF) { + _, receiveErr := stream.CloseAndRecv() + if receiveErr != nil { + return convertRPCError(receiveErr) + } + } + + return convertRPCError(err) + } + + err = send(&zwdaemonv1.AddFileRequest{ + Payload: &zwdaemonv1.AddFileRequest_Header{ + Header: &zwdaemonv1.AddFileHeader{ + LogicalPath: logicalPath, + SourceLen: sourceLen, + }, + }, + }) + if err != nil { + return err + } + + const chunkSize = 512 * 1024 + buffer := make([]byte, chunkSize) + + for { + n, readErr := source.Read(buffer) + + if n > 0 { + err := send(&zwdaemonv1.AddFileRequest{ + Payload: &zwdaemonv1.AddFileRequest_Chunk{ + Chunk: buffer[:n], + }, + }) + if err != nil { + return err + } + } + + switch { + case readErr == nil: + continue + case errors.Is(readErr, io.EOF): + _, err := stream.CloseAndRecv() + if err != nil { + return convertRPCError(err) + } + return nil + default: + return &UploadReadError{Err: readErr} + } + } +} |
