aboutsummaryrefslogtreecommitdiff
path: root/objectstore
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-04 12:53:00 +0800
committerGravatar Runxi Yu2026-03-04 12:53:00 +0800
commitb90b167cfbce785088c5236960515bc460e062d8 (patch)
treebcec9ce9f7fe4a63b546c0433002017d05f6a5be /objectstore
parentrefstore/chain: Split (diff)
signatureNo signature
objectstore/loose: Split
Diffstat (limited to 'objectstore')
-rw-r--r--objectstore/loose/write_temp_object_file.go30
-rw-r--r--objectstore/loose/write_writer.go183
-rw-r--r--objectstore/loose/write_writer_accept.go61
-rw-r--r--objectstore/loose/write_writer_finalize.go113
4 files changed, 204 insertions, 183 deletions
diff --git a/objectstore/loose/write_temp_object_file.go b/objectstore/loose/write_temp_object_file.go
new file mode 100644
index 00000000..1a78db48
--- /dev/null
+++ b/objectstore/loose/write_temp_object_file.go
@@ -0,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")
+}
diff --git a/objectstore/loose/write_writer.go b/objectstore/loose/write_writer.go
index a0f24f2b..f76c882e 100644
--- 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"
)
@@ -100,181 +95,3 @@ func (writer *streamWriter) Write(src []byte) (int, error) {
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")
-}
diff --git a/objectstore/loose/write_writer_accept.go b/objectstore/loose/write_writer_accept.go
new file mode 100644
index 00000000..707232ba
--- /dev/null
+++ b/objectstore/loose/write_writer_accept.go
@@ -0,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
+}
diff --git a/objectstore/loose/write_writer_finalize.go b/objectstore/loose/write_writer_finalize.go
new file mode 100644
index 00000000..0dcae98a
--- /dev/null
+++ b/objectstore/loose/write_writer_finalize.go
@@ -0,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
+}