From b90b167cfbce785088c5236960515bc460e062d8 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Wed, 4 Mar 2026 12:53:00 +0800 Subject: objectstore/loose: Split --- objectstore/loose/write_temp_object_file.go | 30 +++++ objectstore/loose/write_writer.go | 183 ---------------------------- objectstore/loose/write_writer_accept.go | 61 ++++++++++ objectstore/loose/write_writer_finalize.go | 113 +++++++++++++++++ 4 files changed, 204 insertions(+), 183 deletions(-) create mode 100644 objectstore/loose/write_temp_object_file.go create mode 100644 objectstore/loose/write_writer_accept.go create mode 100644 objectstore/loose/write_writer_finalize.go 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 +} -- cgit v1.3.1-10-gc9f91