forked from hswaw/hscloud
222 lines
4.8 KiB
Go
222 lines
4.8 KiB
Go
|
package service
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"io"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/golang/glog"
|
||
|
"google.golang.org/grpc/codes"
|
||
|
"google.golang.org/grpc/status"
|
||
|
|
||
|
git "github.com/go-git/go-git/v5"
|
||
|
"github.com/go-git/go-git/v5/plumbing"
|
||
|
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||
|
"github.com/go-git/go-git/v5/storage"
|
||
|
"github.com/go-git/go-git/v5/storage/memory"
|
||
|
|
||
|
pb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
reHash = regexp.MustCompile(`[a-f0-9]{40,64}`)
|
||
|
)
|
||
|
|
||
|
type Service struct {
|
||
|
remote string
|
||
|
storer storage.Storer
|
||
|
|
||
|
mu sync.Mutex
|
||
|
repo *git.Repository
|
||
|
lastPull time.Time
|
||
|
}
|
||
|
|
||
|
func New(remote string) *Service {
|
||
|
return &Service{
|
||
|
remote: remote,
|
||
|
storer: memory.NewStorage(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Service) ensureRepo() error {
|
||
|
// Clone repository if necessary.
|
||
|
if s.repo == nil {
|
||
|
repo, err := git.Clone(s.storer, nil, &git.CloneOptions{
|
||
|
URL: s.remote,
|
||
|
})
|
||
|
if err != nil {
|
||
|
glog.Errorf("Clone(%q): %v", s.remote, err)
|
||
|
return status.Error(codes.Unavailable, "could not clone repository")
|
||
|
}
|
||
|
s.repo = repo
|
||
|
s.lastPull = time.Now()
|
||
|
}
|
||
|
|
||
|
// Fetch if necessary.
|
||
|
if time.Since(s.lastPull) > time.Minute {
|
||
|
err := s.repo.Fetch(&git.FetchOptions{})
|
||
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||
|
glog.Errorf("Fetch(): %v", err)
|
||
|
} else {
|
||
|
s.lastPull = time.Now()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *Service) Resolve(ctx context.Context, req *pb.ResolveRequest) (*pb.ResolveResponse, error) {
|
||
|
s.mu.Lock()
|
||
|
defer s.mu.Unlock()
|
||
|
|
||
|
if req.Ref == "" {
|
||
|
return nil, status.Error(codes.InvalidArgument, "ref must be set")
|
||
|
}
|
||
|
|
||
|
if err := s.ensureRepo(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
h, err := s.repo.ResolveRevision(plumbing.Revision(req.Ref))
|
||
|
switch {
|
||
|
case err == plumbing.ErrReferenceNotFound:
|
||
|
return &pb.ResolveResponse{Hash: "", LastChecked: s.lastPull.UnixNano()}, nil
|
||
|
case err != nil:
|
||
|
return nil, status.Errorf(codes.Unavailable, "git resolve error: %v", err)
|
||
|
default:
|
||
|
return &pb.ResolveResponse{Hash: h.String(), LastChecked: s.lastPull.UnixNano()}, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Service) getFile(hash, path string, notFoundOkay bool) (*object.File, error) {
|
||
|
if !reHash.MatchString(hash) {
|
||
|
return nil, status.Error(codes.InvalidArgument, "hash must be valid full git hash string")
|
||
|
}
|
||
|
if path == "" {
|
||
|
return nil, status.Error(codes.InvalidArgument, "path must be set")
|
||
|
}
|
||
|
|
||
|
path = pathNormalize(path)
|
||
|
if path == "" {
|
||
|
return nil, status.Error(codes.InvalidArgument, "path must be a valid unix or depot-style path")
|
||
|
}
|
||
|
|
||
|
if err := s.ensureRepo(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
c, err := s.repo.CommitObject(plumbing.NewHash(hash))
|
||
|
switch {
|
||
|
case err == plumbing.ErrObjectNotFound:
|
||
|
return nil, status.Error(codes.NotFound, "hash not found")
|
||
|
case err != nil:
|
||
|
return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
|
||
|
}
|
||
|
|
||
|
file, err := c.File(path)
|
||
|
switch {
|
||
|
case err == object.ErrFileNotFound && !notFoundOkay:
|
||
|
return nil, status.Error(codes.NotFound, "file not found")
|
||
|
case err == object.ErrFileNotFound && notFoundOkay:
|
||
|
return nil, nil
|
||
|
case err != nil:
|
||
|
return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
|
||
|
}
|
||
|
|
||
|
return file, nil
|
||
|
}
|
||
|
|
||
|
func (s *Service) Stat(ctx context.Context, req *pb.StatRequest) (*pb.StatResponse, error) {
|
||
|
s.mu.Lock()
|
||
|
defer s.mu.Unlock()
|
||
|
|
||
|
file, err := s.getFile(req.Hash, req.Path, true)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if file == nil {
|
||
|
return &pb.StatResponse{Type: pb.StatResponse_TYPE_NOT_PRESENT}, nil
|
||
|
}
|
||
|
|
||
|
switch {
|
||
|
case file.Mode == filemode.Dir:
|
||
|
return &pb.StatResponse{Type: pb.StatResponse_TYPE_DIRECTORY}, nil
|
||
|
case file.Mode.IsFile():
|
||
|
return &pb.StatResponse{Type: pb.StatResponse_TYPE_FILE}, nil
|
||
|
default:
|
||
|
return nil, status.Errorf(codes.Unimplemented, "unknown file type %o", file.Mode)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Service) Read(req *pb.ReadRequest, srv pb.DepotView_ReadServer) error {
|
||
|
s.mu.Lock()
|
||
|
defer s.mu.Unlock()
|
||
|
|
||
|
file, err := s.getFile(req.Hash, req.Path, false)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
reader, err := file.Reader()
|
||
|
if err != nil {
|
||
|
return status.Errorf(codes.Unavailable, "file read error: %v", err)
|
||
|
}
|
||
|
defer reader.Close()
|
||
|
|
||
|
ctx := srv.Context()
|
||
|
for {
|
||
|
if ctx.Err() != nil {
|
||
|
return ctx.Err()
|
||
|
}
|
||
|
|
||
|
// 1 MB read
|
||
|
chunk := make([]byte, 16*1024)
|
||
|
n, err := reader.Read(chunk)
|
||
|
switch {
|
||
|
case err == io.EOF:
|
||
|
n = 0
|
||
|
case err != nil:
|
||
|
return status.Errorf(codes.Unavailable, "file read error: %v", err)
|
||
|
}
|
||
|
|
||
|
err = srv.Send(&pb.ReadResponse{Data: chunk[:n]})
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if n == 0 {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func pathNormalize(path string) string {
|
||
|
leadingSlashes := 0
|
||
|
for _, c := range path {
|
||
|
if c != '/' {
|
||
|
break
|
||
|
}
|
||
|
leadingSlashes += 1
|
||
|
}
|
||
|
|
||
|
// Only foo/bar, /foo/bar, and //foo/bar paths allowed.
|
||
|
if leadingSlashes > 2 {
|
||
|
return ""
|
||
|
}
|
||
|
path = path[leadingSlashes:]
|
||
|
|
||
|
// No trailing slashes allowed.
|
||
|
if strings.HasSuffix(path, "/") {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
return path
|
||
|
}
|