"//constants",
"//ephs",
"//utils/context",
+ "//utils/fsutil",
"//utils/log",
"@com_github_urfave_cli_v3//:cli",
],
package main
import (
+ "bytes"
"errors"
"flag"
"fmt"
"io"
"os"
+ "os/exec"
"path"
"strings"
+ "unicode/utf8"
"github.com/urfave/cli/v3"
"go.fuhry.dev/runtime/constants"
"go.fuhry.dev/runtime/ephs"
"go.fuhry.dev/runtime/utils/context"
+ "go.fuhry.dev/runtime/utils/fsutil"
"go.fuhry.dev/runtime/utils/log"
)
return nil
}
+func cmdEdit(ctx context.Context, cmd *cli.Command) error {
+ client, err := ephs.DefaultClient()
+ if err != nil {
+ return err
+ }
+
+ buf := bytes.NewBuffer(nil)
+ temp, err := os.CreateTemp(os.TempDir(), "ephs*")
+ if err != nil {
+ return err
+ }
+ tempFileName := temp.Name()
+ defer os.Remove(tempFileName)
+
+ reader, err := client.GetContext(ctx, cmd.StringArg("path"))
+ if err == nil {
+ tee := io.TeeReader(reader, buf)
+
+ io.Copy(temp, tee)
+ temp.Close()
+
+ if !utf8.Valid(buf.Bytes()) {
+ return errors.New("cannot edit a binary file")
+ }
+ } else if ephs.IsNotFound(err) {
+ temp.Close()
+ log.Default().Infof("path %q not found, creating new file", cmd.StringArg("path"))
+ } else {
+ temp.Close()
+ return err
+ }
+
+ editor := os.Getenv("EDITOR")
+ if editor == "" {
+ for _, f := range []string{"/usr/bin/vim", "/usr/bin/vi", "/usr/bin/nano"} {
+ if err := fsutil.FileExistsAndIsReadable(f); err == nil {
+ editor = f
+ break
+ }
+ }
+ }
+ if editor == "" {
+ return errors.New("EDITOR not set and cannot find any suitable fallback")
+ }
+ if !path.IsAbs(editor) {
+ editor, err = exec.LookPath(editor)
+ if err != nil {
+ return err
+ }
+ }
+
+ editorProc, err := os.StartProcess(
+ editor,
+ []string{
+ editor,
+ tempFileName,
+ },
+ &os.ProcAttr{
+ Files: []*os.File{
+ os.Stdin,
+ os.Stdout,
+ os.Stderr,
+ },
+ },
+ )
+ if err != nil {
+ return err
+ }
+ editorState, err := editorProc.Wait()
+ if err != nil {
+ return err
+ }
+ if status := editorState.ExitCode(); status != 0 {
+ return fmt.Errorf("editor %s exited with status %d", editor, status)
+ }
+
+ newContents, err := os.ReadFile(tempFileName)
+ if err != nil {
+ return err
+ }
+
+ if bytes.Equal(newContents, buf.Bytes()) {
+ log.Default().Info("file unchanged, not reuploading to ephs")
+ return nil
+ }
+
+ newBuf := bytes.NewBuffer(newContents)
+ putResult, err := client.PutContext(ctx, cmd.StringArg("path"), uint64(len(newContents)), newBuf)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(ephs.FormatFsEntry(putResult))
+
+ return nil
+}
+
func main() {
ctx, cancel := context.Interruptible()
defer cancel()
},
},
},
+ {
+ Name: "edit",
+ Description: "edit a text file",
+ Action: cmdEdit,
+ Arguments: []cli.Argument{
+ &cli.StringArg{
+ Name: "path",
+ UsageText: "path to edit",
+ },
+ },
+ },
},
Flags: []cli.Flag{
}
if err := cmd.Run(ctx, os.Args); err != nil {
- log.Default().Panic(err)
+ log.Fatal(err)
+ os.Exit(1)
}
}
}
type getReader struct {
+ first *ephs_pb.GetResponse
stream ephs_pb.Ephs_GetClient
buf *bytes.Buffer
cancel context.CancelFunc
return strings.HasPrefix(p, KeyPrefix)
}
+type notFoundError struct {
+ ephsPath string
+}
+
+func (e *notFoundError) Error() string {
+ return fmt.Sprintf("path was not found in ephs: %s", e.ephsPath)
+}
+
+var _ error = ¬FoundError{}
+
+func IsNotFound(err error) bool {
+ if err == nil {
+ return false
+ }
+ _, ok := err.(*notFoundError)
+ return ok
+}
+
+func maybeMakeNotFound(err error) error {
+ if st, ok := status.FromError(err); ok {
+ if st.Code() == codes.NotFound {
+ return ¬FoundError{st.Message()}
+ }
+ }
+
+ return err
+}
+
func FormatFsEntry(e *ephs_pb.FsEntry) string {
if e == nil {
return "<nil>"
return nil, err
}
- return &getReader{stream, &bytes.Buffer{}, nil}, nil
+ // peek the first message.
+ // this is where any errors returned from the rpc function will be raised, so we
+ // need to catch this before we initialize the reader stream.
+ msg, err := stream.Recv()
+ if err != nil {
+ return nil, maybeMakeNotFound(err)
+ }
+ return &getReader{msg, stream, &bytes.Buffer{}, nil}, nil
}
func (r *getReader) Read(p []byte) (int, error) {
if r.buf.Len() < 1 {
- msg, err := r.stream.Recv()
- if err != nil {
- return 0, err
+ var msg *ephs_pb.GetResponse
+
+ if f := r.first; f != nil {
+ msg = f
+ r.first = nil
+ } else {
+ m, err := r.stream.Recv()
+ if err != nil {
+ return 0, err
+ }
+ msg = m
}
if len(msg.Chunk) == 0 {
}
resp, err := rpc.Stat(ctx, req)
if err != nil {
- return nil, err
+ return nil, maybeMakeNotFound(err)
}
return resp.GetEntry(), nil
Recursive: recursive,
}
_, err = rpc.Delete(ctx, req)
- return err
+ return maybeMakeNotFound(err)
}
func (c *clientImpl) MkDir(path string, recursive bool) error {
Recursive: recursive,
}
_, err = rpc.MkDir(ctx, req)
- return err
+ return maybeMakeNotFound(err)
}
func (c *clientImpl) Put(path string, size uint64, r io.Reader) (*ephs_pb.FsEntry, error) {
req.Chunk = req.Chunk[:n]
err = stream.Send(req)
if err != nil {
- return nil, err
+ return nil, maybeMakeNotFound(err)
}
nw += uint64(n)
if err == io.EOF {
}
stream, err := rpc.Watch(ctx, req)
if err != nil {
- return nil, err
+ return nil, maybeMakeNotFound(err)
}
ch := make(chan *ephs_pb.WatchResponse)
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/utils/option"
)
var s3Endpoint = "s3." + constants.WebServicesDomain
var s3Prefix = ""
func WithAWSEnvCredentials() Option {
- return &genericOption{
- apply: func(s *ephsServicer) error {
- s.s3Creds = credentials.NewEnvAWS()
- return nil
- },
- }
+ return option.NewOption(func(s *ephsServicer) error {
+ s.s3Creds = credentials.NewEnvAWS()
+
+ cc := &credentials.CredContext{
+ Endpoint: s3Endpoint,
+ }
+ _, err := s.s3Creds.GetWithContext(cc)
+
+ return err
+ })
}
func WithAWSCredentialFile(filename string) Option {
- return &genericOption{
- apply: func(s *ephsServicer) error {
- s.s3Creds = credentials.NewFileAWSCredentials(filename, "default")
- return nil
- },
- }
+ return option.NewOption(func(s *ephsServicer) error {
+ s.s3Creds = credentials.NewFileAWSCredentials(filename, "default")
+
+ cc := &credentials.CredContext{
+ Endpoint: s3Endpoint,
+ }
+ _, err := s.s3Creds.GetWithContext(cc)
+ return err
+ })
}
func (s *ephsServicer) newS3Client() (*minio.Client, error) {
}
if len(obj.Kvs) < 1 {
- return nil, status.Errorf(
+ return nil, status.Error(
codes.NotFound,
- "object does not exist: %q",
path)
}