Files
Gogs/internal/lfsutil/storage.go
ᴊᴏᴇ ᴄʜᴇɴ 81ee883644 lfs: verify content hash and prevent object overwrite (#8166)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2026-02-08 17:14:12 -05:00

134 lines
3.3 KiB
Go

package lfsutil
import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
"path/filepath"
"github.com/cockroachdb/errors"
"gogs.io/gogs/internal/osutil"
)
var (
ErrObjectNotExist = errors.New("object does not exist")
ErrOIDMismatch = errors.New("content hash does not match OID")
)
// Storager is an storage backend for uploading and downloading LFS objects.
type Storager interface {
// Storage returns the name of the storage backend.
Storage() Storage
// Upload reads content from the io.ReadCloser and uploads as given oid.
// The reader is closed once upload is finished. ErrInvalidOID is returned
// if the given oid is not valid.
Upload(oid OID, rc io.ReadCloser) (int64, error)
// Download streams content of given oid to the io.Writer. It is caller's
// responsibility the close the writer when needed. ErrObjectNotExist is
// returned if the given oid does not exist.
Download(oid OID, w io.Writer) error
}
// Storage is the storage type of an LFS object.
type Storage string
const (
StorageLocal Storage = "local"
)
var _ Storager = (*LocalStorage)(nil)
// LocalStorage is a LFS storage backend on local file system.
type LocalStorage struct {
// The root path for storing LFS objects.
Root string
// The path for storing temporary files during upload verification.
TempDir string
}
func (*LocalStorage) Storage() Storage {
return StorageLocal
}
func (s *LocalStorage) storagePath(oid OID) string {
if len(oid) < 2 {
return ""
}
return filepath.Join(s.Root, string(oid[0]), string(oid[1]), string(oid))
}
func (s *LocalStorage) Upload(oid OID, rc io.ReadCloser) (int64, error) {
if !ValidOID(oid) {
return 0, ErrInvalidOID
}
fpath := s.storagePath(oid)
dir := filepath.Dir(fpath)
defer rc.Close()
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return 0, errors.Wrap(err, "create directories")
}
// If the object file already exists, skip the upload and return the
// existing file's size.
if fi, err := os.Stat(fpath); err == nil {
_, _ = io.Copy(io.Discard, rc)
return fi.Size(), nil
}
// Write to a temp file and verify the content hash before publishing.
// This ensures the final path always contains a complete, hash-verified
// file, even when concurrent uploads of the same OID race.
if err := os.MkdirAll(s.TempDir, os.ModePerm); err != nil {
return 0, errors.Wrap(err, "create temp directory")
}
tmp, err := os.CreateTemp(s.TempDir, "upload-*")
if err != nil {
return 0, errors.Wrap(err, "create temp file")
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
hash := sha256.New()
written, err := io.Copy(tmp, io.TeeReader(rc, hash))
if closeErr := tmp.Close(); err == nil && closeErr != nil {
err = closeErr
}
if err != nil {
return 0, errors.Wrap(err, "write object file")
}
if computed := hex.EncodeToString(hash.Sum(nil)); computed != string(oid) {
return 0, ErrOIDMismatch
}
if err := os.Rename(tmpPath, fpath); err != nil && !os.IsExist(err) {
return 0, errors.Wrap(err, "publish object file")
}
return written, nil
}
func (s *LocalStorage) Download(oid OID, w io.Writer) error {
fpath := s.storagePath(oid)
if !osutil.IsFile(fpath) {
return ErrObjectNotExist
}
r, err := os.Open(fpath)
if err != nil {
return errors.Wrap(err, "open file")
}
defer r.Close()
_, err = io.Copy(w, r)
if err != nil {
return errors.Wrap(err, "copy file")
}
return nil
}