···2626// It is cleared once the (possibly long) transfer begins.
2727const handshakeTimeout = 30 * time.Second
28282929+// streamingStorer wraps a storage.Storer to hide its optional
3030+// storer.PackfileWriter capability. go-git's UpdateObjectStorage drains the
3131+// incoming pack into PackfileWriter via io.CopyBuffer, which only returns on
3232+// io.EOF — fine over HTTP (the request body has a natural EOF) but a deadlock
3333+// over git://, where the client holds the connection open waiting for the
3434+// server's report-status. With PackfileWriter hidden, UpdateObjectStorage
3535+// falls through to Parser.Parse, which knows the end of the pack from the
3636+// pack format itself and never waits for an EOF.
3737+//
3838+// Trade-off: Parser.Parse writes loose objects (one Rename → one S3 PUT each)
3939+// instead of one packfile, so large git:// pushes incur more S3 calls. HTTP
4040+// keeps the fast PackfileWriter path.
4141+type streamingStorer struct {
4242+ storage.Storer
4343+}
4444+2945// daemon serves the git:// (TCP) protocol out of a billy filesystem.
3046type daemon struct {
3147 fs billy.Filesystem
···119135 _, _ = pktline.WriteError(conn, fmt.Errorf("cannot open repository %q", req.Pathname))
120136 return fmt.Errorf("opening %q for push: %w", req.Pathname, err)
121137 }
122122- return transport.ReceivePack(ctx, st, r, conn, &transport.ReceivePackRequest{
138138+ return transport.ReceivePack(ctx, streamingStorer{Storer: st}, r, conn, &transport.ReceivePackRequest{
123139 GitProtocol: gitProtocol,
124140 })
125141
···33package s3fs
4455import (
66+ "bytes"
67 "context"
78 "errors"
89 "fmt"
···1011 "os"
1112 "path"
1213 "strings"
1414+ "time"
13151416 "github.com/aws/aws-sdk-go-v2/aws"
1517 "github.com/aws/aws-sdk-go-v2/service/s3"
···6668 return newS3DirFile(key, fs3.bucket, fs3.client), nil
6769 }
68707171+ // A TempFile that has not yet been renamed lives only in memory; serve
7272+ // reads from that buffer so go-git's PackWriter can read the pack back
7373+ // while it is still being written.
7474+ if buf, ok := fs3.lookupTemp(filename); ok {
7575+ return &tempReadFile{buf: buf, name: filename}, nil
7676+ }
7777+6978 f, err := newS3ReadFile(fs3.client, fs3.bucket, key, filename)
7079 if err == nil {
7180 return f, nil
···118127 return newDirInfo("/"), nil
119128 }
120129130130+ // A still-open TempFile lives only in memory; report its current size so
131131+ // callers that Stat the temp path before Rename see a consistent view.
132132+ if buf, ok := fs3.lookupTemp(filename); ok {
133133+ return newFileInfo(path.Base(filename), buf.size(), time.Now()), nil
134134+ }
135135+121136 ctx := context.TODO()
122137123138 head, err := fs3.client.HeadObject(ctx, &s3.HeadObjectInput{
···160175 return nil, &os.PathError{Op: "stat", Path: filename, Err: fs.ErrNotExist}
161176}
162177163163-// Rename renames (moves) oldpath to newpath. If newpath already exists and
164164-// is not a directory, Rename replaces it. OS-specific restrictions may
165165-// apply when oldpath and newpath are in different directories.
178178+// Rename renames (moves) oldpath to newpath. If oldpath refers to an
179179+// in-memory TempFile, its buffer is uploaded to S3 under newpath and the
180180+// registry entry is dropped — this is how PackWriter's "tmp_pack_… →
181181+// pack-<sha>.pack" promotion lands the final pack in the bucket. Otherwise
182182+// Rename uses Tigris's in-place RenameObject extension.
166183func (fs3 *S3FS) Rename(oldpath, newpath string) error {
167184 ctx := context.TODO() // TODO: Get user-supplied context?
168185169186 src := fs3.key(oldpath)
170187 dst := fs3.key(newpath)
171188189189+ if buf, ok := fs3.detachTemp(oldpath); ok {
190190+ data := buf.snapshot()
191191+ _, err := fs3.client.PutObject(ctx, &s3.PutObjectInput{
192192+ Bucket: &fs3.bucket,
193193+ Key: &dst,
194194+ Body: bytes.NewReader(data),
195195+ })
196196+ if err != nil {
197197+ return fmt.Errorf("failed to upload temp %q to %q: %w", oldpath, newpath, err)
198198+ }
199199+ return nil
200200+ }
201201+172202 // RenameObject is a Tigris extension that renames in place (no data copy),
173203 // so we don't need a separate CopyObject + DeleteObject. CopySource is
174204 // bucket-qualified; Key is the destination key.
···185215 return nil
186216}
187217188188-// Remove removes the named file or directory.
218218+// Remove removes the named file or directory. In-memory TempFile entries are
219219+// dropped from the registry without an S3 call.
189220func (fs3 *S3FS) Remove(filename string) error {
190190- // TODO: Validate the path?
191191- // ...
221221+ if _, ok := fs3.detachTemp(filename); ok {
222222+ return nil
223223+ }
192224193193- // Create a context
194225 ctx := context.TODO() // TODO: Get user-supplied context?
195226196227 key := fs3.key(filename)
···44 "fmt"
55 "path"
66 "strings"
77+ "sync"
7889 "github.com/go-git/go-billy/v6"
910 "github.com/tigrisdata/storage-go"
···1819 bucket string
1920 root string
2021 separator string
2222+2323+ // temps holds TempFile-backed buffers keyed by canonical S3 key, so a
2424+ // subsequent Open of the same path returns a reader over the same bytes
2525+ // the writer is still appending to. See tempfs.go.
2626+ tempMu sync.Mutex
2727+ temps map[string]*tempBuffer
2128}
22292330// NewS3FS creates a new S3FS Filesystem.
···3138 bucket: bucket,
3239 root: "",
3340 separator: DefaultSeparator,
4141+ temps: make(map[string]*tempBuffer),
3442 }, nil
3543}
3644
···1010 "github.com/go-git/go-billy/v6"
1111)
12121313-// TempFile creates a uniquely named, write-only file under dir whose name
1414-// begins with prefix. The object is uploaded to S3 when the returned file is
1515-// closed; until then nothing exists in the bucket. The caller is responsible
1616-// for renaming or removing it.
1717-//
1818-// Note: the returned file is write-only. S3 has no read-while-write temp file,
1919-// so callers that reopen the temp path for reading before Close (e.g. go-git's
2020-// streaming PackWriter) are not supported; use the loose-object path instead.
1313+// TempFile creates a uniquely named file under dir whose name begins with
1414+// prefix and returns a write handle to it. The bytes live in an in-memory
1515+// buffer registered against the filesystem; a subsequent Open of the same
1616+// path returns a reader over that same buffer (needed by go-git's streaming
1717+// PackWriter, which reads the temp pack back as it is written). The buffer
1818+// is uploaded to S3 only when the caller renames the path to its final
1919+// location; Remove discards it.
2120func (fs3 *S3FS) TempFile(dir, prefix string) (billy.File, error) {
2221 var b [16]byte
2322 if _, err := rand.Read(b[:]); err != nil {
···2524 }
26252726 name := fs3.Join(dir, prefix+hex.EncodeToString(b[:]))
2828- return newS3WriteFile(fs3.client, fs3.bucket, fs3.key(name), name)
2727+ buf := &tempBuffer{}
2828+ fs3.registerTemp(name, buf)
2929+ return &tempWriteFile{buf: buf, name: name}, nil
2930}