Files
forge/services/packages/container/blob_uploader.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

143 lines
3.2 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"context"
"errors"
"io"
"os"
packages_model "github.com/hanzoai/git/models/packages"
packages_module "github.com/hanzoai/git/modules/packages"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/tempdir"
)
var (
// errWriteAfterRead occurs if Write is called after a read operation
errWriteAfterRead = errors.New("write is unsupported after a read operation")
// errOffsetMismatch occurs if the file offset is different than the model
errOffsetMismatch = errors.New("offset mismatch between file and model")
)
// BlobUploader handles chunked blob uploads
type BlobUploader struct {
*packages_model.PackageBlobUpload
*packages_module.MultiHasher
file *os.File
reading bool
}
func uploadPathTempDir() *tempdir.TempDir {
return setting.AppDataTempDir("package-upload")
}
func buildFilePath(uploadPath *tempdir.TempDir, id string) string {
return uploadPath.JoinPath(id)
}
// NewBlobUploader creates a new blob uploader for the given id
func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
model, err := packages_model.GetBlobUploadByID(ctx, id)
if err != nil {
return nil, err
}
hash := packages_module.NewMultiHasher()
if len(model.HashStateBytes) != 0 {
if err := hash.UnmarshalBinary(model.HashStateBytes); err != nil {
return nil, err
}
}
uploadPath := uploadPathTempDir()
_, err = uploadPath.MkdirAllSub("")
if err != nil {
return nil, err
}
f, err := os.OpenFile(buildFilePath(uploadPath, model.ID), os.O_RDWR|os.O_CREATE, 0o666)
if err != nil {
return nil, err
}
return &BlobUploader{
PackageBlobUpload: model,
MultiHasher: hash,
file: f,
reading: false,
}, nil
}
// Close implements io.Closer
func (u *BlobUploader) Close() error {
return u.file.Close()
}
// Append appends a chunk of data and updates the model
func (u *BlobUploader) Append(ctx context.Context, r io.Reader) error {
if u.reading {
return errWriteAfterRead
}
offset, err := u.file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if offset != u.BytesReceived {
return errOffsetMismatch
}
n, err := io.Copy(io.MultiWriter(u.file, u.MultiHasher), r)
if err != nil {
return err
}
// fast path if nothing was written
if n == 0 {
return nil
}
u.BytesReceived += n
u.HashStateBytes, err = u.MultiHasher.MarshalBinary()
if err != nil {
return err
}
return packages_model.UpdateBlobUpload(ctx, u.PackageBlobUpload)
}
func (u *BlobUploader) Size() int64 {
return u.BytesReceived
}
// Read implements io.Reader
func (u *BlobUploader) Read(p []byte) (int, error) {
if !u.reading {
_, err := u.file.Seek(0, io.SeekStart)
if err != nil {
return 0, err
}
u.reading = true
}
return u.file.Read(p)
}
// RemoveBlobUploadByID Remove deletes the data and the model of a blob upload
func RemoveBlobUploadByID(ctx context.Context, id string) error {
if err := packages_model.DeleteBlobUploadByID(ctx, id); err != nil {
return err
}
err := os.Remove(buildFilePath(uploadPathTempDir(), id))
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}