ref: b90b167cfbce785088c5236960515bc460e062d8
parent: 845cd640384ed25ce3c18ade9aae37de2ed4c5e0
author: Runxi Yu <me@runxiyu.org>
date: Wed Mar 4 07:53:00 EST 2026
objectstore/loose: Split
--- /dev/null
+++ b/objectstore/loose/write_temp_object_file.go
@@ -1,0 +1,30 @@
+package loose
+
+import (
+ "crypto/rand"
+ "errors"
+ "io/fs"
+ "os"
+ "path/filepath"
+)
+
+// createTempObjectFile creates a unique temporary object file within dir.
+// The returned path is relative to the objects root.
+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
+ }
+
+ return "", nil, errors.New("objectstore/loose: failed to create temporary object file")+}
--- a/objectstore/loose/write_writer.go
+++ b/objectstore/loose/write_writer.go
@@ -1,16 +1,11 @@
package loose
import (
- "bytes"
- "crypto/rand"
"errors"
"hash"
- "io/fs"
"os"
- "path/filepath"
"codeberg.org/lindenii/furgit/internal/zlib"
- "codeberg.org/lindenii/furgit/objectheader"
"codeberg.org/lindenii/furgit/objectid"
)
@@ -99,182 +94,4 @@
}
return len(src), nil
-}
-
-// Close flushes and closes the underlying zlib stream and temp file.
-// It is safe to call multiple times.
-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)
-}
-
-// finalize validates write completeness and atomically publishes the object.
-// Publication is no-clobber: it links tmpRelPath to the object path and treats
-// existing destination objects as success.
-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 {- 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)
-
- 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)
- }
- }()
-
- 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 {- 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
- }
-
- return writer.acceptContent(int64(len(src)))
-}
-
-// acceptContent validates and accounts content byte counts.
-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 {- _, err := writer.hash.Write(src)
- if err != nil {- return err
- }
-
- _, err = writer.zw.Write(src)
- if err != nil {- return err
- }
-
- return nil
-}
-
-// createTempObjectFile creates a unique temporary object file within dir.
-// The returned path is relative to the objects root.
-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
- }
-
- return "", nil, errors.New("objectstore/loose: failed to create temporary object file")}
--- /dev/null
+++ b/objectstore/loose/write_writer_accept.go
@@ -1,0 +1,61 @@
+package loose
+
+import (
+ "bytes"
+ "errors"
+
+ "codeberg.org/lindenii/furgit/objectheader"
+)
+
+// acceptFull validates and accounts raw full-object input.
+func (writer *streamWriter) acceptFull(src []byte) error {+ if !writer.headerDone {+ 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
+ }
+
+ return writer.acceptContent(int64(len(src)))
+}
+
+// acceptContent validates and accounts content byte counts.
+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 {+ _, err := writer.hash.Write(src)
+ if err != nil {+ return err
+ }
+
+ _, err = writer.zw.Write(src)
+ if err != nil {+ return err
+ }
+
+ return nil
+}
--- /dev/null
+++ b/objectstore/loose/write_writer_finalize.go
@@ -1,0 +1,113 @@
+package loose
+
+import (
+ "errors"
+ "io/fs"
+ "path/filepath"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// Close flushes and closes the underlying zlib stream and temp file.
+// It is safe to call multiple times.
+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)
+}
+
+// finalize validates write completeness and atomically publishes the object.
+// Publication is no-clobber: it links tmpRelPath to the object path and treats
+// existing destination objects as success.
+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 {+ 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)
+
+ 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)
+ }
+ }()
+
+ 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
+}
--
⑨