diff options
| author | 2026-03-04 08:26:56 +0800 | |
|---|---|---|
| committer | 2026-03-04 08:59:53 +0800 | |
| commit | ab7501be34032fb9e5c48726a68ae90a917af9eb (patch) | |
| tree | 20d005647569befea8133e953c3270e8fd2a2a5b /objectstore/loose | |
| parent | *: gofumpt (diff) | |
| signature | No signature | |
*: Lint
Diffstat (limited to 'objectstore/loose')
| -rw-r--r-- | objectstore/loose/helpers_test.go | 15 | ||||
| -rw-r--r-- | objectstore/loose/parse.go | 6 | ||||
| -rw-r--r-- | objectstore/loose/paths.go | 5 | ||||
| -rw-r--r-- | objectstore/loose/read_bytes.go | 5 | ||||
| -rw-r--r-- | objectstore/loose/read_header.go | 3 | ||||
| -rw-r--r-- | objectstore/loose/read_reader.go | 8 | ||||
| -rw-r--r-- | objectstore/loose/read_size.go | 1 | ||||
| -rw-r--r-- | objectstore/loose/read_test.go | 43 | ||||
| -rw-r--r-- | objectstore/loose/store.go | 1 | ||||
| -rw-r--r-- | objectstore/loose/write_reader.go | 17 | ||||
| -rw-r--r-- | objectstore/loose/write_test.go | 20 | ||||
| -rw-r--r-- | objectstore/loose/write_writer.go | 57 |
12 files changed, 155 insertions, 26 deletions
diff --git a/objectstore/loose/helpers_test.go b/objectstore/loose/helpers_test.go index 972059e0..4b0bb60e 100644 --- a/objectstore/loose/helpers_test.go +++ b/objectstore/loose/helpers_test.go @@ -15,30 +15,39 @@ import ( func openLooseStore(t *testing.T, repoPath string, algo objectid.Algorithm) *loose.Store { t.Helper() + objectsPath := filepath.Join(repoPath, "objects") + root, err := os.OpenRoot(objectsPath) if err != nil { t.Fatalf("OpenRoot(%q): %v", objectsPath, err) } + t.Cleanup(func() { _ = root.Close() }) store, err := loose.New(root, algo) if err != nil { t.Fatalf("loose.New: %v", err) } + return store } func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte { t.Helper() + data, err := io.ReadAll(reader) if err != nil { _ = reader.Close() + t.Fatalf("ReadAll: %v", err) } - if err := reader.Close(); err != nil { + + err = reader.Close() + if err != nil { t.Fatalf("Close: %v", err) } + return data } @@ -46,11 +55,14 @@ func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.Obj t.Helper() typeName := testRepo.Run(t, "cat-file", "-t", id.String()) + ty, ok := objecttype.ParseName(typeName) if !ok { t.Fatalf("ParseName(%q) failed", typeName) } + body := testRepo.CatFile(t, typeName, id) + header, ok := objectheader.Encode(ty, int64(len(body))) if !ok { t.Fatalf("objectheader.Encode failed") @@ -59,5 +71,6 @@ func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.Obj raw := make([]byte, len(header)+len(body)) copy(raw, header) copy(raw[len(header):], body) + return ty, body, raw } diff --git a/objectstore/loose/parse.go b/objectstore/loose/parse.go index 54bb2375..e88d7c6c 100644 --- a/objectstore/loose/parse.go +++ b/objectstore/loose/parse.go @@ -17,7 +17,9 @@ func decodeAll(file *os.File) ([]byte, error) { if err != nil { return nil, err } + defer func() { _ = zr.Close() }() + return io.ReadAll(zr) } @@ -27,10 +29,12 @@ func parseRaw(raw []byte) (objecttype.Type, []byte, error) { if !ok { return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: malformed object header") } + content := raw[headerLen:] if int64(len(content)) != size { return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: object header size/content mismatch") } + return ty, content, nil } @@ -41,9 +45,11 @@ func readHeader(br *bufio.Reader) ([]byte, objecttype.Type, int64, error) { if err != nil { return nil, objecttype.TypeInvalid, 0, err } + ty, size, _, ok := objectheader.Parse(header) if !ok { return nil, objecttype.TypeInvalid, 0, errors.New("objectstore/loose: malformed object header") } + return header, ty, size, nil } diff --git a/objectstore/loose/paths.go b/objectstore/loose/paths.go index 04730bd3..e8020d72 100644 --- a/objectstore/loose/paths.go +++ b/objectstore/loose/paths.go @@ -16,7 +16,9 @@ func (store *Store) objectPath(id objectid.ObjectID) (string, error) { if id.Algorithm() != store.algo { return "", fmt.Errorf("objectstore/loose: object id algorithm mismatch: got %s want %s", id.Algorithm(), store.algo) } + hex := id.String() + return filepath.Join(hex[:2], hex[2:]), nil } @@ -27,12 +29,15 @@ func (store *Store) openObject(id objectid.ObjectID) (*os.File, error) { if err != nil { return nil, err } + file, err := store.root.Open(relPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, objectstore.ErrObjectNotFound } + return nil, err } + return file, nil } diff --git a/objectstore/loose/read_bytes.go b/objectstore/loose/read_bytes.go index 2f7c24bc..78e1009e 100644 --- a/objectstore/loose/read_bytes.go +++ b/objectstore/loose/read_bytes.go @@ -12,16 +12,19 @@ func (store *Store) readBytesParsed(id objectid.ObjectID) ([]byte, objecttype.Ty if err != nil { return nil, objecttype.TypeInvalid, nil, err } + defer func() { _ = file.Close() }() raw, err := decodeAll(file) if err != nil { return nil, objecttype.TypeInvalid, nil, err } + ty, content, err := parseRaw(raw) if err != nil { return nil, objecttype.TypeInvalid, nil, err } + return raw, ty, content, nil } @@ -31,6 +34,7 @@ func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { if err != nil { return nil, err } + return raw, nil } @@ -40,5 +44,6 @@ func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []b if err != nil { return objecttype.TypeInvalid, nil, err } + return ty, content, nil } diff --git a/objectstore/loose/read_header.go b/objectstore/loose/read_header.go index ce76600e..abfb1a02 100644 --- a/objectstore/loose/read_header.go +++ b/objectstore/loose/read_header.go @@ -14,17 +14,20 @@ func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, er if err != nil { return objecttype.TypeInvalid, 0, err } + defer func() { _ = file.Close() }() zr, err := zlib.NewReader(file) if err != nil { return objecttype.TypeInvalid, 0, err } + defer func() { _ = zr.Close() }() _, ty, size, err := readHeader(bufio.NewReader(zr)) if err != nil { return objecttype.TypeInvalid, 0, err } + return ty, size, nil } diff --git a/objectstore/loose/read_reader.go b/objectstore/loose/read_reader.go index 6a377ba3..a0a51cc1 100644 --- a/objectstore/loose/read_reader.go +++ b/objectstore/loose/read_reader.go @@ -29,6 +29,7 @@ func (reader *objectReader) Read(dst []byte) (int, error) { func (reader *objectReader) Close() error { errZlib := reader.zr.Close() errFile := reader.file.Close() + return errors.Join(errZlib, errFile) } @@ -39,11 +40,14 @@ func (store *Store) openInflated(id objectid.ObjectID) (*os.File, io.ReadCloser, if err != nil { return nil, nil, err } + zr, err := zlib.NewReader(file) if err != nil { _ = file.Close() + return nil, nil, err } + return file, zr, nil } @@ -56,10 +60,12 @@ func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) } br := bufio.NewReader(zr) + header, _, size, err := readHeader(br) if err != nil { _ = zr.Close() _ = file.Close() + return nil, err } @@ -82,10 +88,12 @@ func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, in } br := bufio.NewReader(zr) + _, ty, size, err := readHeader(br) if err != nil { _ = zr.Close() _ = file.Close() + return objecttype.TypeInvalid, 0, nil, err } diff --git a/objectstore/loose/read_size.go b/objectstore/loose/read_size.go index 45f1f0fe..2a1eaec9 100644 --- a/objectstore/loose/read_size.go +++ b/objectstore/loose/read_size.go @@ -5,5 +5,6 @@ import "codeberg.org/lindenii/furgit/objectid" // ReadSize reads an object's declared content length. func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { _, size, err := store.ReadHeader(id) + return size, err } diff --git a/objectstore/loose/read_test.go b/objectstore/loose/read_test.go index d8166c9e..1efc1682 100644 --- a/objectstore/loose/read_test.go +++ b/objectstore/loose/read_test.go @@ -41,6 +41,7 @@ func TestLooseStoreReadAgainstGit(t *testing.T) { if err != nil { t.Fatalf("ReadBytesFull: %v", err) } + if !bytes.Equal(gotRaw, wantRaw) { t.Fatalf("ReadBytesFull mismatch") } @@ -49,9 +50,11 @@ func TestLooseStoreReadAgainstGit(t *testing.T) { if err != nil { t.Fatalf("ReadBytesContent: %v", err) } + if gotType != wantType { t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) } + if !bytes.Equal(gotBody, wantBody) { t.Fatalf("ReadBytesContent body mismatch") } @@ -60,9 +63,11 @@ func TestLooseStoreReadAgainstGit(t *testing.T) { if err != nil { t.Fatalf("ReadHeader: %v", err) } + if headType != wantType { t.Fatalf("ReadHeader type = %v, want %v", headType, wantType) } + if headSize != int64(len(wantBody)) { t.Fatalf("ReadHeader size = %d, want %d", headSize, len(wantBody)) } @@ -71,7 +76,9 @@ func TestLooseStoreReadAgainstGit(t *testing.T) { if err != nil { t.Fatalf("ReadReaderFull: %v", err) } - if got := mustReadAllAndClose(t, fullReader); !bytes.Equal(got, wantRaw) { + + got := mustReadAllAndClose(t, fullReader) + if !bytes.Equal(got, wantRaw) { t.Fatalf("ReadReaderFull stream mismatch") } @@ -79,13 +86,17 @@ func TestLooseStoreReadAgainstGit(t *testing.T) { if err != nil { t.Fatalf("ReadReaderContent: %v", err) } + if contentType != wantType { t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) } + if contentSize != int64(len(wantBody)) { t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) } - if got := mustReadAllAndClose(t, contentReader); !bytes.Equal(got, wantBody) { + + got = mustReadAllAndClose(t, contentReader) + if !bytes.Equal(got, wantBody) { t.Fatalf("ReadReaderContent stream mismatch") } }) @@ -104,19 +115,28 @@ func TestLooseStoreErrors(t *testing.T) { t.Fatalf("ParseHex(notFoundID): %v", err) } - if _, err := store.ReadBytesFull(notFoundID); !errors.Is(err, objectstore.ErrObjectNotFound) { + _, err = store.ReadBytesFull(notFoundID) + if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadBytesFull not-found error = %v", err) } - if _, _, err := store.ReadBytesContent(notFoundID); !errors.Is(err, objectstore.ErrObjectNotFound) { + + _, _, err = store.ReadBytesContent(notFoundID) + if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadBytesContent not-found error = %v", err) } - if _, err := store.ReadReaderFull(notFoundID); !errors.Is(err, objectstore.ErrObjectNotFound) { + + _, err = store.ReadReaderFull(notFoundID) + if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadReaderFull not-found error = %v", err) } - if _, _, _, err := store.ReadReaderContent(notFoundID); !errors.Is(err, objectstore.ErrObjectNotFound) { + + _, _, _, err = store.ReadReaderContent(notFoundID) + if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadReaderContent not-found error = %v", err) } - if _, _, err := store.ReadHeader(notFoundID); !errors.Is(err, objectstore.ErrObjectNotFound) { + + _, _, err = store.ReadHeader(notFoundID) + if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadHeader not-found error = %v", err) } @@ -126,12 +146,14 @@ func TestLooseStoreErrors(t *testing.T) { } else { otherAlgo = objectid.AlgorithmSHA1 } + otherID, err := objectid.ParseHex(otherAlgo, strings.Repeat("1", otherAlgo.HexLen())) if err != nil { t.Fatalf("ParseHex(otherID): %v", err) } - if _, err := store.ReadBytesFull(otherID); err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { + _, err = store.ReadBytesFull(otherID) + if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) } }) @@ -139,13 +161,16 @@ func TestLooseStoreErrors(t *testing.T) { func TestLooseStoreNewValidation(t *testing.T) { t.Parallel() + root, err := os.OpenRoot(t.TempDir()) if err != nil { t.Fatalf("OpenRoot: %v", err) } + defer func() { _ = root.Close() }() - if _, err := loose.New(root, objectid.AlgorithmUnknown); err == nil { + _, err = loose.New(root, objectid.AlgorithmUnknown) + if err == nil { t.Fatalf("loose.New(root, unknown) expected error") } } diff --git a/objectstore/loose/store.go b/objectstore/loose/store.go index 05459a6c..c3ae989c 100644 --- a/objectstore/loose/store.go +++ b/objectstore/loose/store.go @@ -24,6 +24,7 @@ func New(root *os.Root, algo objectid.Algorithm) (*Store, error) { if algo.Size() == 0 { return nil, objectid.ErrInvalidAlgorithm } + return &Store{ root: root, algo: algo, diff --git a/objectstore/loose/write_reader.go b/objectstore/loose/write_reader.go index b2329f02..9dbf3818 100644 --- a/objectstore/loose/write_reader.go +++ b/objectstore/loose/write_reader.go @@ -27,12 +27,15 @@ func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Re if err != nil { return objectid.ObjectID{}, err } + writer.headerDone = true writer.expectedContentLeft = size - if err := writer.writeRawChunk(header); err != nil { + err = writer.writeRawChunk(header) + if err != nil { _ = writer.Close() _ = store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err } @@ -46,25 +49,33 @@ func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { if err != nil { return objectid.ObjectID{}, err } + return writeReaderIntoStreamWriter(writer, src) } // writeReaderIntoStreamWriter copies src into writer and publishes the object. func writeReaderIntoStreamWriter(writer *streamWriter, src io.Reader) (objectid.ObjectID, error) { - if _, err := io.Copy(writer, src); err != nil { + _, err := io.Copy(writer, src) + if err != nil { _ = writer.Close() _ = writer.store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err } - if err := writer.Close(); err != nil { + + err = writer.Close() + if err != nil { _ = writer.store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err } id, err := writer.finalize() if err != nil { _ = writer.store.root.Remove(writer.tmpRelPath) + return objectid.ObjectID{}, err } + return id, nil } diff --git a/objectstore/loose/write_test.go b/objectstore/loose/write_test.go index cceabe5a..5604c5b0 100644 --- a/objectstore/loose/write_test.go +++ b/objectstore/loose/write_test.go @@ -18,6 +18,7 @@ func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) { content := []byte("written-by-content-reader\n") expectedHex := testRepo.RunInput(t, content, "hash-object", "-t", "blob", "--stdin") + expectedID, err := objectid.ParseHex(algo, expectedHex) if err != nil { t.Fatalf("ParseHex(expected): %v", err) @@ -27,6 +28,7 @@ func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) { if err != nil { t.Fatalf("WriteReaderContent: %v", err) } + if writtenID != expectedID { t.Fatalf("WriteReaderContent id = %s, want %s", writtenID, expectedID) } @@ -41,6 +43,7 @@ func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) { if err != nil { t.Fatalf("WriteReaderContent second: %v", err) } + if writtenID2 != expectedID { t.Fatalf("WriteReaderContent second id = %s, want %s", writtenID2, expectedID) } @@ -54,19 +57,23 @@ func TestLooseStoreWriteReaderFullAgainstGit(t *testing.T) { store := openLooseStore(t, testRepo.Dir(), algo) body := []byte("full-reader-body\n") + header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body))) if !ok { t.Fatalf("objectheader.Encode failed") } + raw := make([]byte, len(header)+len(body)) copy(raw, header) copy(raw[len(header):], body) wantID := algo.Sum(raw) + gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) if err != nil { t.Fatalf("WriteReaderFull: %v", err) } + if gotID != wantID { t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) } @@ -86,7 +93,8 @@ func TestLooseStoreReaderValidationErrors(t *testing.T) { testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - if _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))); err == nil { + _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) + if err == nil { t.Fatalf("expected error after overflow") } }) @@ -96,7 +104,8 @@ func TestLooseStoreReaderValidationErrors(t *testing.T) { testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - if _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))); err == nil { + _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) + if err == nil { t.Fatalf("expected error for short content") } }) @@ -106,7 +115,8 @@ func TestLooseStoreReaderValidationErrors(t *testing.T) { testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo.Dir(), algo) - if _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))); err == nil { + _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) + if err == nil { t.Fatalf("expected error for malformed header") } }) @@ -117,7 +127,9 @@ func TestLooseStoreReaderValidationErrors(t *testing.T) { store := openLooseStore(t, testRepo.Dir(), algo) raw := []byte("blob 1\x00hello") - if _, err := store.WriteReaderFull(bytes.NewReader(raw)); err == nil { + + _, err := store.WriteReaderFull(bytes.NewReader(raw)) + if err == nil { t.Fatalf("expected error after mismatch") } }) diff --git a/objectstore/loose/write_writer.go b/objectstore/loose/write_writer.go index c075f2ba..a0f24f2b 100644 --- a/objectstore/loose/write_writer.go +++ b/objectstore/loose/write_writer.go @@ -76,23 +76,28 @@ func (writer *streamWriter) Write(src []byte) (int, error) { if writer.finalized { return 0, errors.New("objectstore/loose: write after finalize") } + if writer.closed { return 0, errors.New("objectstore/loose: write after close") } if writer.fullMode { - if err := writer.acceptFull(src); err != nil { + err := writer.acceptFull(src) + if err != nil { return 0, err } } else { - if err := writer.acceptContent(int64(len(src))); err != nil { + err := writer.acceptContent(int64(len(src))) + if err != nil { return 0, err } } - if err := writer.writeRawChunk(src); err != nil { + err := writer.writeRawChunk(src) + if err != nil { return 0, err } + return len(src), nil } @@ -102,12 +107,14 @@ func (writer *streamWriter) Close() error { if writer.closed { return nil } + writer.closed = true errZlib := writer.zw.Close() errSync := writer.file.Sync() errFile := writer.file.Close() writer.file = nil + return errors.Join(errZlib, errSync, errFile) } @@ -118,84 +125,107 @@ func (writer *streamWriter) finalize() (objectid.ObjectID, error) { if writer.finalized { return writer.finalID, writer.finalErr } + writer.finalized = true var zero objectid.ObjectID if !writer.closed { - if err := writer.Close(); err != nil { + err := writer.Close() + if err != nil { writer.finalErr = err + return zero, err } } if writer.fullMode && !writer.headerDone { writer.finalErr = errors.New("objectstore/loose: missing full object header") + return zero, writer.finalErr } + if writer.expectedContentLeft != 0 { writer.finalErr = errors.New("objectstore/loose: object content shorter than declared size") + return zero, writer.finalErr } idBytes := writer.hash.Sum(nil) + id, err := objectid.FromBytes(writer.store.algo, idBytes) if err != nil { writer.finalErr = err + return zero, err } relPath, err := writer.store.objectPath(id) if err != nil { writer.finalErr = err + return zero, err } dir := filepath.Dir(relPath) - if err := writer.store.root.MkdirAll(dir, 0o755); err != nil { + + err = writer.store.root.MkdirAll(dir, 0o755) + if err != nil { writer.finalErr = err + return zero, err } cleanup := true + defer func() { if cleanup { _ = writer.store.root.Remove(writer.tmpRelPath) } }() - if err := writer.store.root.Link(writer.tmpRelPath, relPath); err != nil { + err = writer.store.root.Link(writer.tmpRelPath, relPath) + if err != nil { if errors.Is(err, fs.ErrExist) { writer.finalID = id cleanup = false _ = writer.store.root.Remove(writer.tmpRelPath) + return id, nil } + writer.finalErr = err + return zero, err } writer.finalID = id cleanup = false + return id, nil } // acceptFull validates and accounts raw full-object input. func (writer *streamWriter) acceptFull(src []byte) error { if !writer.headerDone { - if nul := bytes.IndexByte(src, 0); nul >= 0 { + nul := bytes.IndexByte(src, 0) + if nul >= 0 { headerChunkLen := nul + 1 writer.headerBuf = append(writer.headerBuf, src[:headerChunkLen]...) + _, size, _, ok := objectheader.Parse(writer.headerBuf) if !ok { return errors.New("objectstore/loose: malformed object header") } + writer.headerDone = true writer.expectedContentLeft = size + return writer.acceptContent(int64(len(src) - headerChunkLen)) } writer.headerBuf = append(writer.headerBuf, src...) + return nil } @@ -207,18 +237,24 @@ func (writer *streamWriter) acceptContent(n int64) error { if n > writer.expectedContentLeft { return errors.New("objectstore/loose: object content exceeds declared size") } + writer.expectedContentLeft -= n + return nil } // writeRawChunk forwards raw bytes to the hash and deflate pipeline. func (writer *streamWriter) writeRawChunk(src []byte) error { - if _, err := writer.hash.Write(src); err != nil { + _, err := writer.hash.Write(src) + if err != nil { return err } - if _, err := writer.zw.Write(src); err != nil { + + _, err = writer.zw.Write(src) + if err != nil { return err } + return nil } @@ -227,13 +263,16 @@ func (writer *streamWriter) writeRawChunk(src []byte) error { func (store *Store) createTempObjectFile(dir string) (string, *os.File, error) { for range 16 { relPath := filepath.Join(dir, tempObjectFilePrefix+rand.Text()) + file, err := store.root.OpenFile(relPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err == nil { return relPath, file, nil } + if errors.Is(err, fs.ErrExist) { continue } + return "", nil, err } |
