Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ require (
connectrpc.com/connect v1.18.1
connectrpc.com/grpcreflect v1.2.0
github.com/google/go-cmp v0.6.0
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0
golang.org/x/sync v0.16.0
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e
google.golang.org/grpc v1.71.0
Expand All @@ -22,6 +23,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down Expand Up @@ -752,8 +754,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -839,8 +841,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
113 changes: 107 additions & 6 deletions internal/examples/fileserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@ import (
"bytes"
"context"
"flag"
"fmt"
"html/template"
"io/fs"
"io"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"

"connectrpc.com/connect"
"connectrpc.com/vanguard"
testv1 "connectrpc.com/vanguard/internal/gen/vanguard/test/v1"
"connectrpc.com/vanguard/internal/gen/vanguard/test/v1/testv1connect"
"github.com/spf13/afero"
Comment thread
scbizu marked this conversation as resolved.
Outdated
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/protobuf/types/known/emptypb"
)

func main() {
Expand All @@ -40,10 +44,13 @@ func main() {
if err := flagset.Parse(os.Args[1:]); err != nil {
log.Fatal(err)
}

fs := afero.NewOsFs()
// Create Connect handler.
serviceHandler := &ContentService{
FS: os.DirFS(*directory),
Fs: &prefixFS{
Fs: fs,
prefix: *directory,
},
}
// And wrap it with Vanguard.
service := vanguard.NewService(testv1connect.NewContentServiceHandler(serviceHandler))
Expand All @@ -57,6 +64,21 @@ func main() {
log.Fatal(http.ListenAndServe(":"+*port, handler))
}

// PrefixFS is something like os.DirFS()
// but now only wraps Create and Open
type prefixFS struct {
afero.Fs
prefix string
}

func (pf *prefixFS) Create(name string) (afero.File, error) {
return os.Create(path.Join(pf.prefix, name))
}

func (pf *prefixFS) Open(name string) (afero.File, error) {
return os.Open(path.Join(pf.prefix, name))
}

var indexHTMLTemplate = template.Must(template.New("http").Parse(`
<html>
<head>
Expand All @@ -78,7 +100,9 @@ var indexHTMLTemplate = template.Must(template.New("http").Parse(`

type ContentService struct {
testv1connect.UnimplementedContentServiceHandler
fs.FS
// Since std io fs.Fs is a read-only fs abstraction
// For some ops like uploading , we need to modify the user file system
afero.Fs
}

func (c *ContentService) Index(_ context.Context, req *connect.Request[testv1.IndexRequest]) (*connect.Response[httpbody.HttpBody], error) {
Expand All @@ -101,7 +125,7 @@ func (c *ContentService) Index(_ context.Context, req *connect.Request[testv1.In
var data []byte
if !stat.IsDir() {
contentType = mime.TypeByExtension(filepath.Ext(name))
data, err = fs.ReadFile(c.FS, name)
data, err = afero.ReadFile(c.Fs, name)
if err != nil {
return nil, err
}
Expand All @@ -113,7 +137,7 @@ func (c *ContentService) Index(_ context.Context, req *connect.Request[testv1.In
Title: name,
Files: make(map[string]string),
}
entries, err := fs.ReadDir(c.FS, name)
entries, err := afero.ReadDir(c.Fs, name)
if err != nil {
return nil, err
}
Expand All @@ -132,3 +156,80 @@ func (c *ContentService) Index(_ context.Context, req *connect.Request[testv1.In
Data: data,
}), nil
}

// Upload impls the connect RPC stream upload mechanism
// Common Usage(curl):
//
// ```bash
// echo "hello from nace" > hello.txt
// curl -X POST --data-binary "@hello.txt" -H "Content-Type: application/octet-stream" localhost:8100/upload_hello.txt:upload
// ```
func (c *ContentService) Upload(
ctx context.Context,
stream *connect.ClientStream[testv1.UploadRequest],
) (*connect.Response[emptypb.Empty], error) {
if !stream.Receive() {
if err := stream.Err(); err != nil {
return nil, err
}
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("no upload message received"))
}
msg := stream.Msg()
log.Printf("Upload: filename=%q contentType=%q size=%d",
msg.GetFilename(),
msg.GetFile().GetContentType(),
len(msg.GetFile().GetData()),
)
Comment thread
scbizu marked this conversation as resolved.
Outdated

// Write file.
file, err := c.Fs.Create(msg.GetFilename())
if err != nil {
return nil, err
}
defer file.Close()
if _, err := file.Write(msg.GetFile().GetData()); err != nil {
return nil, err
}

// Ensure the client didn’t send more than one message (optional guard).
if stream.Receive() {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("unexpected extra message in upload stream"))
}
if err := stream.Err(); err != nil {
return nil, err
}

return connect.NewResponse(&emptypb.Empty{}), nil
}

// Download impls the connect RPC stream download stream
// Common Usage(curl):
//
// ```bash
// curl -X GET -H "Content-Type: application/json" http://localhost:8100/upload_hello.txt:download > download_hello.txt
// ```
func (c *ContentService) Download(
ctx context.Context,
req *connect.Request[testv1.DownloadRequest],
stream *connect.ServerStream[testv1.DownloadResponse],
) error {
file, err := c.Fs.Open(req.Msg.Filename)
if err != nil {
return err
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return err
}
if err := stream.Send(&testv1.DownloadResponse{
File: &httpbody.HttpBody{
ContentType: "application/octet-stream",
Data: data,
},
}); err != nil {
return err
}
Comment thread
scbizu marked this conversation as resolved.
Outdated
return nil
}