1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
|
package standalone
import (
"bufio"
"encoding/json"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/git-lfs/git-lfs/v3/config"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/lfs"
"github.com/git-lfs/git-lfs/v3/lfsapi"
"github.com/git-lfs/git-lfs/v3/subprocess"
"github.com/git-lfs/git-lfs/v3/tools"
"github.com/git-lfs/git-lfs/v3/tr"
"github.com/rubyist/tracerx"
)
// inputMessage represents a message from Git LFS to the standalone transfer
// agent. Not all fields will be filled in on all requests.
type inputMessage struct {
Event string `json:"event"`
Operation string `json:"operation"`
Remote string `json:"remote"`
Oid string `json:"oid"`
Size int64 `json:"size"`
Path string `json:"path"`
}
// errorMessage represents an optional error message that may occur in a
// completion response.
type errorMessage struct {
Message string `json:"message"`
}
// outputErrorMessage represents an error message that may occur during startup.
type outputErrorMessage struct {
Error errorMessage `json:"error"`
}
// completeMessage represents a completion response.
type completeMessage struct {
Event string `json:"event"`
Oid string `json:"oid"`
Path string `json:"path,omitempty"`
Error *errorMessage `json:"error,omitempty"`
}
type fileHandler struct {
remotePath string
remoteConfig *config.Configuration
output *os.File
config *config.Configuration
tempdir string
}
// fileUrlFromRemote looks up the URL depending on the remote. The remote can be
// a literal URL or the name of a remote.
//
// In this situation, we only accept file URLs.
func fileUrlFromRemote(cfg *config.Configuration, name string, direction string) (*url.URL, error) {
if strings.HasPrefix(name, "file://") {
if url, err := url.Parse(name); err == nil {
return url, nil
}
}
apiClient, err := lfsapi.NewClient(cfg)
if err != nil {
return nil, err
}
for _, remote := range cfg.Remotes() {
if remote != name {
continue
}
remoteEndpoint := apiClient.Endpoints.Endpoint(direction, remote)
if !strings.HasPrefix(remoteEndpoint.Url, "file://") {
return nil, nil
}
return url.Parse(remoteEndpoint.Url)
}
return nil, nil
}
// gitDirAtPath finds the .git directory corresponding to the given path, which
// may be the .git directory itself, the working tree, or the root of a bare
// repository.
//
// We filter out the GIT_DIR environment variable to ensure we get the expected
// result, and we change directories to ensure that we can make use of
// filepath.Abs. Using --absolute-git-dir instead of --git-dir is not an option
// because we support Git versions that don't have --absolute-git-dir.
func gitDirAtPath(path string) (string, error) {
// Filter out all the GIT_* environment variables.
env := os.Environ()
n := 0
for _, val := range env {
if !strings.HasPrefix(val, "GIT_") {
env[n] = val
n++
}
}
env = env[:n]
// Trim any trailing .git path segment.
if filepath.Base(path) == ".git" {
path = filepath.Dir(path)
}
curdir, err := os.Getwd()
if err != nil {
return "", err
}
err = os.Chdir(path)
if err != nil {
return "", err
}
cmd, err := subprocess.ExecCommand("git", "rev-parse", "--git-dir")
if err != nil {
return "", errors.Wrap(err, tr.Tr.Get("failed to find `git rev-parse --git-dir`"))
}
cmd.Cmd.Env = env
out, err := cmd.Output()
if err != nil {
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
return "", errors.New(tr.Tr.Get("failed to call `git rev-parse --git-dir`: %s", string(err.Stderr)))
}
return "", errors.Wrap(err, tr.Tr.Get("failed to call `git rev-parse --git-dir`"))
}
gitdir, err := tools.TranslateCygwinPath(strings.TrimRight(string(out), "\n"))
if err != nil {
return "", errors.Wrap(err, tr.Tr.Get("unable to translate path"))
}
gitdir, err = filepath.Abs(gitdir)
if err != nil {
return "", errors.Wrap(err, tr.Tr.Get("unable to canonicalize path"))
}
err = os.Chdir(curdir)
if err != nil {
return "", err
}
return tools.CanonicalizeSystemPath(gitdir)
}
func fixUrlPath(path string) string {
if runtime.GOOS != "windows" {
return path
}
// When parsing a file URL, Go produces a path starting with a slash. If
// it looks like there's a Windows drive letter at the beginning, strip
// off the beginning slash. If this is a Unix-style path from a
// Cygwin-like environment, we'll canonicalize it later.
re := regexp.MustCompile("/[A-Za-z]:/")
if re.MatchString(path) {
return path[1:]
}
return path
}
// newHandler creates a new handler for the protocol.
func newHandler(cfg *config.Configuration, output *os.File, msg *inputMessage) (*fileHandler, error) {
url, err := fileUrlFromRemote(cfg, msg.Remote, msg.Operation)
if err != nil {
return nil, err
}
if url == nil {
return nil, errors.New(tr.Tr.Get("no valid file:// URLs found"))
}
path, err := tools.TranslateCygwinPath(fixUrlPath(url.Path))
if err != nil {
return nil, err
}
gitdir, err := gitDirAtPath(path)
if err != nil {
return nil, err
}
tempdir, err := os.MkdirTemp(cfg.TempDir(), "lfs-standalone-file-*")
if err != nil {
return nil, err
}
tracerx.Printf("using %q as remote git directory", gitdir)
return &fileHandler{
remotePath: path,
remoteConfig: config.NewIn(gitdir, gitdir),
output: output,
config: cfg,
tempdir: tempdir,
}, nil
}
// dispatch dispatches the event depending on the message type.
func (h *fileHandler) dispatch(msg *inputMessage) bool {
switch msg.Event {
case "init":
fmt.Fprintln(h.output, "{}")
case "upload":
h.respond(h.upload(msg.Oid, msg.Size, msg.Path))
case "download":
h.respond(h.download(msg.Oid, msg.Size))
case "terminate":
return false
default:
standaloneFailure(tr.Tr.Get("unknown event %q", msg.Event), nil)
}
return true
}
// respond sends a response to an upload or download command, using the return
// values from those functions.
func (h *fileHandler) respond(oid string, path string, err error) {
response := &completeMessage{
Event: "complete",
Oid: oid,
Path: path,
}
if err != nil {
response.Error = &errorMessage{Message: err.Error()}
}
json.NewEncoder(h.output).Encode(response)
}
// upload performs the upload action for the given OID, size, and path. It
// returns arguments suitable for the respond method.
func (h *fileHandler) upload(oid string, size int64, path string) (string, string, error) {
if h.remoteConfig.LFSObjectExists(oid, size) {
// Already there, nothing to do.
return oid, "", nil
}
dest, err := h.remoteConfig.Filesystem().ObjectPath(oid)
if err != nil {
return oid, "", err
}
return oid, "", lfs.LinkOrCopy(h.remoteConfig, path, dest)
}
// download performs the download action for the given OID and size. It returns
// arguments suitable for the respond method.
func (h *fileHandler) download(oid string, size int64) (string, string, error) {
if !h.remoteConfig.LFSObjectExists(oid, size) {
tracerx.Printf("missing object in %q (%s)", h.remotePath, oid)
return oid, "", errors.New(tr.Tr.Get("remote missing object %s", oid))
}
src, err := h.remoteConfig.Filesystem().ObjectPath(oid)
if err != nil {
return oid, "", err
}
tmp, err := os.CreateTemp(h.tempdir, "download")
if err != nil {
return oid, "", err
}
tmp.Close()
os.Remove(tmp.Name())
path := tmp.Name()
return oid, path, lfs.LinkOrCopy(h.config, src, path)
}
// standaloneFailure reports a fatal error.
func standaloneFailure(msg string, err error) {
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
os.Exit(2)
}
// ProcessStandaloneData is the primary endpoint for processing data with a
// standalone transfer agent. It reads input from the specified input file and
// produces output to the specified output file.
func ProcessStandaloneData(cfg *config.Configuration, input *os.File, output *os.File) error {
var handler *fileHandler
scanner := bufio.NewScanner(input)
for scanner.Scan() {
var msg inputMessage
if err := json.NewDecoder(strings.NewReader(scanner.Text())).Decode(&msg); err != nil {
return errors.Wrap(err, tr.Tr.Get("error decoding JSON"))
}
if handler == nil {
var err error
handler, err = newHandler(cfg, output, &msg)
if err != nil {
err := errors.Wrap(err, tr.Tr.Get("error creating handler"))
errMsg := outputErrorMessage{
Error: errorMessage{
Message: err.Error(),
},
}
json.NewEncoder(output).Encode(errMsg)
return err
}
}
if !handler.dispatch(&msg) {
break
}
}
if handler != nil {
os.RemoveAll(handler.tempdir)
}
if err := scanner.Err(); err != nil {
return errors.Wrap(err, tr.Tr.Get("error reading input"))
}
return nil
}
|