diff options
Diffstat (limited to 'objectstore/loose/write_bytes.go')
| -rw-r--r-- | objectstore/loose/write_bytes.go | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/objectstore/loose/write_bytes.go b/objectstore/loose/write_bytes.go new file mode 100644 index 00000000..fe2bafb9 --- /dev/null +++ b/objectstore/loose/write_bytes.go @@ -0,0 +1,123 @@ +package loose + +import ( + "compress/zlib" + "crypto/rand" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "codeberg.org/lindenii/furgit/objectheader" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objecttype" +) + +const tempObjectFilePrefix = "tmp_obj_" + +// WriteBytesFull writes a full serialized object as "type size\\x00content". +func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { + var zero objectid.ObjectID + + if _, _, err := parseRaw(raw); err != nil { + return zero, err + } + + id := store.algo.Sum(raw) + relPath, err := store.objectPath(id) + if err != nil { + return zero, err + } + if err := store.writeCompressedAtomic(relPath, raw); err != nil { + return zero, err + } + return id, nil +} + +// WriteBytesContent writes typed content bytes as a loose object. +func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { + var zero objectid.ObjectID + + header, ok := objectheader.Encode(ty, int64(len(content))) + if !ok { + return zero, fmt.Errorf("objectstore/loose: failed to encode object header for type %d", ty) + } + + raw := make([]byte, len(header)+len(content)) + copy(raw, header) + copy(raw[len(header):], content) + return store.WriteBytesFull(raw) +} + +// writeCompressedAtomic compresses raw and writes it to relPath atomically. +func (store *Store) writeCompressedAtomic(relPath string, raw []byte) error { + if _, err := store.root.Stat(relPath); err == nil { + return nil + } else if !errors.Is(err, fs.ErrNotExist) { + return err + } + + dir := filepath.Dir(relPath) + if err := store.root.MkdirAll(dir, 0o755); err != nil { + return err + } + + tmpRelPath, tmpFile, err := store.createTempObjectFile(dir) + if err != nil { + return err + } + + cleanup := true + defer func() { + if tmpFile != nil { + _ = tmpFile.Close() + } + if cleanup { + _ = store.root.Remove(tmpRelPath) + } + }() + + zw := zlib.NewWriter(tmpFile) + if _, err := zw.Write(raw); err != nil { + _ = zw.Close() + return err + } + if err := zw.Close(); err != nil { + return err + } + if err := tmpFile.Sync(); err != nil { + return err + } + if err := tmpFile.Close(); err != nil { + return err + } + tmpFile = nil + + if err := store.root.Rename(tmpRelPath, relPath); err != nil { + if errors.Is(err, fs.ErrExist) { + return nil + } + return err + } + + cleanup = false + return nil +} + +// createTempObjectFile creates a unique temporary object file within dir. +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") +} |
