mirror of
https://github.com/gogs/gogs.git
synced 2026-02-19 04:47:05 +01:00
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>
134 lines
3.3 KiB
Go
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
|
|
}
|