code.gitea.io/gitea@v1.21.7/models/git/lfs.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package git
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"code.gitea.io/gitea/models/db"
    11  	"code.gitea.io/gitea/models/perm"
    12  	repo_model "code.gitea.io/gitea/models/repo"
    13  	"code.gitea.io/gitea/models/unit"
    14  	user_model "code.gitea.io/gitea/models/user"
    15  	"code.gitea.io/gitea/modules/lfs"
    16  	"code.gitea.io/gitea/modules/log"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	"code.gitea.io/gitea/modules/timeutil"
    19  	"code.gitea.io/gitea/modules/util"
    20  
    21  	"xorm.io/builder"
    22  )
    23  
    24  // ErrLFSLockNotExist represents a "LFSLockNotExist" kind of error.
    25  type ErrLFSLockNotExist struct {
    26  	ID     int64
    27  	RepoID int64
    28  	Path   string
    29  }
    30  
    31  // IsErrLFSLockNotExist checks if an error is a ErrLFSLockNotExist.
    32  func IsErrLFSLockNotExist(err error) bool {
    33  	_, ok := err.(ErrLFSLockNotExist)
    34  	return ok
    35  }
    36  
    37  func (err ErrLFSLockNotExist) Error() string {
    38  	return fmt.Sprintf("lfs lock does not exist [id: %d, rid: %d, path: %s]", err.ID, err.RepoID, err.Path)
    39  }
    40  
    41  func (err ErrLFSLockNotExist) Unwrap() error {
    42  	return util.ErrNotExist
    43  }
    44  
    45  // ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error.
    46  type ErrLFSUnauthorizedAction struct {
    47  	RepoID   int64
    48  	UserName string
    49  	Mode     perm.AccessMode
    50  }
    51  
    52  // IsErrLFSUnauthorizedAction checks if an error is a ErrLFSUnauthorizedAction.
    53  func IsErrLFSUnauthorizedAction(err error) bool {
    54  	_, ok := err.(ErrLFSUnauthorizedAction)
    55  	return ok
    56  }
    57  
    58  func (err ErrLFSUnauthorizedAction) Error() string {
    59  	if err.Mode == perm.AccessModeWrite {
    60  		return fmt.Sprintf("User %s doesn't have write access for lfs lock [rid: %d]", err.UserName, err.RepoID)
    61  	}
    62  	return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID)
    63  }
    64  
    65  func (err ErrLFSUnauthorizedAction) Unwrap() error {
    66  	return util.ErrPermissionDenied
    67  }
    68  
    69  // ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error.
    70  type ErrLFSLockAlreadyExist struct {
    71  	RepoID int64
    72  	Path   string
    73  }
    74  
    75  // IsErrLFSLockAlreadyExist checks if an error is a ErrLFSLockAlreadyExist.
    76  func IsErrLFSLockAlreadyExist(err error) bool {
    77  	_, ok := err.(ErrLFSLockAlreadyExist)
    78  	return ok
    79  }
    80  
    81  func (err ErrLFSLockAlreadyExist) Error() string {
    82  	return fmt.Sprintf("lfs lock already exists [rid: %d, path: %s]", err.RepoID, err.Path)
    83  }
    84  
    85  func (err ErrLFSLockAlreadyExist) Unwrap() error {
    86  	return util.ErrAlreadyExist
    87  }
    88  
    89  // ErrLFSFileLocked represents a "LFSFileLocked" kind of error.
    90  type ErrLFSFileLocked struct {
    91  	RepoID   int64
    92  	Path     string
    93  	UserName string
    94  }
    95  
    96  // IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked.
    97  func IsErrLFSFileLocked(err error) bool {
    98  	_, ok := err.(ErrLFSFileLocked)
    99  	return ok
   100  }
   101  
   102  func (err ErrLFSFileLocked) Error() string {
   103  	return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path)
   104  }
   105  
   106  func (err ErrLFSFileLocked) Unwrap() error {
   107  	return util.ErrPermissionDenied
   108  }
   109  
   110  // LFSMetaObject stores metadata for LFS tracked files.
   111  type LFSMetaObject struct {
   112  	ID           int64 `xorm:"pk autoincr"`
   113  	lfs.Pointer  `xorm:"extends"`
   114  	RepositoryID int64              `xorm:"UNIQUE(s) INDEX NOT NULL"`
   115  	Existing     bool               `xorm:"-"`
   116  	CreatedUnix  timeutil.TimeStamp `xorm:"created"`
   117  	UpdatedUnix  timeutil.TimeStamp `xorm:"INDEX updated"`
   118  }
   119  
   120  func init() {
   121  	db.RegisterModel(new(LFSMetaObject))
   122  }
   123  
   124  // LFSTokenResponse defines the JSON structure in which the JWT token is stored.
   125  // This structure is fetched via SSH and passed by the Git LFS client to the server
   126  // endpoint for authorization.
   127  type LFSTokenResponse struct {
   128  	Header map[string]string `json:"header"`
   129  	Href   string            `json:"href"`
   130  }
   131  
   132  // ErrLFSObjectNotExist is returned from lfs models functions in order
   133  // to differentiate between database and missing object errors.
   134  var ErrLFSObjectNotExist = db.ErrNotExist{Resource: "LFS Meta object"}
   135  
   136  // NewLFSMetaObject stores a given populated LFSMetaObject structure in the database
   137  // if it is not already present.
   138  func NewLFSMetaObject(ctx context.Context, m *LFSMetaObject) (*LFSMetaObject, error) {
   139  	var err error
   140  
   141  	ctx, committer, err := db.TxContext(ctx)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	defer committer.Close()
   146  
   147  	has, err := db.GetByBean(ctx, m)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	if has {
   153  		m.Existing = true
   154  		return m, committer.Commit()
   155  	}
   156  
   157  	if err = db.Insert(ctx, m); err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	return m, committer.Commit()
   162  }
   163  
   164  // GetLFSMetaObjectByOid selects a LFSMetaObject entry from database by its OID.
   165  // It may return ErrLFSObjectNotExist or a database error. If the error is nil,
   166  // the returned pointer is a valid LFSMetaObject.
   167  func GetLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (*LFSMetaObject, error) {
   168  	if len(oid) == 0 {
   169  		return nil, ErrLFSObjectNotExist
   170  	}
   171  
   172  	m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID}
   173  	has, err := db.GetEngine(ctx).Get(m)
   174  	if err != nil {
   175  		return nil, err
   176  	} else if !has {
   177  		return nil, ErrLFSObjectNotExist
   178  	}
   179  	return m, nil
   180  }
   181  
   182  // RemoveLFSMetaObjectByOid removes a LFSMetaObject entry from database by its OID.
   183  // It may return ErrLFSObjectNotExist or a database error.
   184  func RemoveLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (int64, error) {
   185  	return RemoveLFSMetaObjectByOidFn(ctx, repoID, oid, nil)
   186  }
   187  
   188  // RemoveLFSMetaObjectByOidFn removes a LFSMetaObject entry from database by its OID.
   189  // It may return ErrLFSObjectNotExist or a database error. It will run Fn with the current count within the transaction
   190  func RemoveLFSMetaObjectByOidFn(ctx context.Context, repoID int64, oid string, fn func(count int64) error) (int64, error) {
   191  	if len(oid) == 0 {
   192  		return 0, ErrLFSObjectNotExist
   193  	}
   194  
   195  	ctx, committer, err := db.TxContext(ctx)
   196  	if err != nil {
   197  		return 0, err
   198  	}
   199  	defer committer.Close()
   200  
   201  	m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID}
   202  	if _, err := db.DeleteByBean(ctx, m); err != nil {
   203  		return -1, err
   204  	}
   205  
   206  	count, err := db.CountByBean(ctx, &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
   207  	if err != nil {
   208  		return count, err
   209  	}
   210  
   211  	if fn != nil {
   212  		if err := fn(count); err != nil {
   213  			return count, err
   214  		}
   215  	}
   216  
   217  	return count, committer.Commit()
   218  }
   219  
   220  // GetLFSMetaObjects returns all LFSMetaObjects associated with a repository
   221  func GetLFSMetaObjects(ctx context.Context, repoID int64, page, pageSize int) ([]*LFSMetaObject, error) {
   222  	sess := db.GetEngine(ctx)
   223  
   224  	if page >= 0 && pageSize > 0 {
   225  		start := 0
   226  		if page > 0 {
   227  			start = (page - 1) * pageSize
   228  		}
   229  		sess.Limit(pageSize, start)
   230  	}
   231  	lfsObjects := make([]*LFSMetaObject, 0, pageSize)
   232  	return lfsObjects, sess.Find(&lfsObjects, &LFSMetaObject{RepositoryID: repoID})
   233  }
   234  
   235  // CountLFSMetaObjects returns a count of all LFSMetaObjects associated with a repository
   236  func CountLFSMetaObjects(ctx context.Context, repoID int64) (int64, error) {
   237  	return db.GetEngine(ctx).Count(&LFSMetaObject{RepositoryID: repoID})
   238  }
   239  
   240  // LFSObjectAccessible checks if a provided Oid is accessible to the user
   241  func LFSObjectAccessible(ctx context.Context, user *user_model.User, oid string) (bool, error) {
   242  	if user.IsAdmin {
   243  		count, err := db.GetEngine(ctx).Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
   244  		return count > 0, err
   245  	}
   246  	cond := repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)
   247  	count, err := db.GetEngine(ctx).Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
   248  	return count > 0, err
   249  }
   250  
   251  // ExistsLFSObject checks if a provided Oid exists within the DB
   252  func ExistsLFSObject(ctx context.Context, oid string) (bool, error) {
   253  	return db.GetEngine(ctx).Exist(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
   254  }
   255  
   256  // LFSAutoAssociate auto associates accessible LFSMetaObjects
   257  func LFSAutoAssociate(ctx context.Context, metas []*LFSMetaObject, user *user_model.User, repoID int64) error {
   258  	ctx, committer, err := db.TxContext(ctx)
   259  	if err != nil {
   260  		return err
   261  	}
   262  	defer committer.Close()
   263  
   264  	sess := db.GetEngine(ctx)
   265  
   266  	oids := make([]any, len(metas))
   267  	oidMap := make(map[string]*LFSMetaObject, len(metas))
   268  	for i, meta := range metas {
   269  		oids[i] = meta.Oid
   270  		oidMap[meta.Oid] = meta
   271  	}
   272  
   273  	if !user.IsAdmin {
   274  		newMetas := make([]*LFSMetaObject, 0, len(metas))
   275  		cond := builder.In(
   276  			"`lfs_meta_object`.repository_id",
   277  			builder.Select("`repository`.id").From("repository").Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)),
   278  		)
   279  		err = sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas)
   280  		if err != nil {
   281  			return err
   282  		}
   283  		if len(newMetas) != len(oidMap) {
   284  			return fmt.Errorf("unable collect all LFS objects from database, expected %d, actually %d", len(oidMap), len(newMetas))
   285  		}
   286  		for i := range newMetas {
   287  			newMetas[i].Size = oidMap[newMetas[i].Oid].Size
   288  			newMetas[i].RepositoryID = repoID
   289  		}
   290  		if err = db.Insert(ctx, newMetas); err != nil {
   291  			return err
   292  		}
   293  	} else {
   294  		// admin can associate any LFS object to any repository, and we do not care about errors (eg: duplicated unique key),
   295  		// even if error occurs, it won't hurt users and won't make things worse
   296  		for i := range metas {
   297  			p := lfs.Pointer{Oid: metas[i].Oid, Size: metas[i].Size}
   298  			_, err = sess.Insert(&LFSMetaObject{
   299  				Pointer:      p,
   300  				RepositoryID: repoID,
   301  			})
   302  			if err != nil {
   303  				log.Warn("failed to insert LFS meta object %-v for repo_id: %d into database, err=%v", p, repoID, err)
   304  			}
   305  		}
   306  	}
   307  	return committer.Commit()
   308  }
   309  
   310  // CopyLFS copies LFS data from one repo to another
   311  func CopyLFS(ctx context.Context, newRepo, oldRepo *repo_model.Repository) error {
   312  	var lfsObjects []*LFSMetaObject
   313  	if err := db.GetEngine(ctx).Where("repository_id=?", oldRepo.ID).Find(&lfsObjects); err != nil {
   314  		return err
   315  	}
   316  
   317  	for _, v := range lfsObjects {
   318  		v.ID = 0
   319  		v.RepositoryID = newRepo.ID
   320  		if err := db.Insert(ctx, v); err != nil {
   321  			return err
   322  		}
   323  	}
   324  
   325  	return nil
   326  }
   327  
   328  // GetRepoLFSSize return a repository's lfs files size
   329  func GetRepoLFSSize(ctx context.Context, repoID int64) (int64, error) {
   330  	lfsSize, err := db.GetEngine(ctx).Where("repository_id = ?", repoID).SumInt(new(LFSMetaObject), "size")
   331  	if err != nil {
   332  		return 0, fmt.Errorf("updateSize: GetLFSMetaObjects: %w", err)
   333  	}
   334  	return lfsSize, nil
   335  }
   336  
   337  // IterateRepositoryIDsWithLFSMetaObjects iterates across the repositories that have LFSMetaObjects
   338  func IterateRepositoryIDsWithLFSMetaObjects(ctx context.Context, f func(ctx context.Context, repoID, count int64) error) error {
   339  	batchSize := setting.Database.IterateBufferSize
   340  	sess := db.GetEngine(ctx)
   341  	id := int64(0)
   342  	type RepositoryCount struct {
   343  		RepositoryID int64
   344  		Count        int64
   345  	}
   346  	for {
   347  		counts := make([]*RepositoryCount, 0, batchSize)
   348  		sess.Select("repository_id, COUNT(id) AS count").
   349  			Table("lfs_meta_object").
   350  			Where("repository_id > ?", id).
   351  			GroupBy("repository_id").
   352  			OrderBy("repository_id ASC")
   353  
   354  		if err := sess.Limit(batchSize, 0).Find(&counts); err != nil {
   355  			return err
   356  		}
   357  		if len(counts) == 0 {
   358  			return nil
   359  		}
   360  
   361  		for _, count := range counts {
   362  			if err := f(ctx, count.RepositoryID, count.Count); err != nil {
   363  				return err
   364  			}
   365  		}
   366  		id = counts[len(counts)-1].RepositoryID
   367  	}
   368  }
   369  
   370  // IterateLFSMetaObjectsForRepoOptions provides options for IterateLFSMetaObjectsForRepo
   371  type IterateLFSMetaObjectsForRepoOptions struct {
   372  	OlderThan                 timeutil.TimeStamp
   373  	UpdatedLessRecentlyThan   timeutil.TimeStamp
   374  	OrderByUpdated            bool
   375  	LoopFunctionAlwaysUpdates bool
   376  }
   377  
   378  // IterateLFSMetaObjectsForRepo provides a iterator for LFSMetaObjects per Repo
   379  func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(context.Context, *LFSMetaObject, int64) error, opts *IterateLFSMetaObjectsForRepoOptions) error {
   380  	var start int
   381  	batchSize := setting.Database.IterateBufferSize
   382  	engine := db.GetEngine(ctx)
   383  	type CountLFSMetaObject struct {
   384  		Count         int64
   385  		LFSMetaObject `xorm:"extends"`
   386  	}
   387  
   388  	id := int64(0)
   389  
   390  	for {
   391  		beans := make([]*CountLFSMetaObject, 0, batchSize)
   392  		sess := engine.Table("lfs_meta_object").Select("`lfs_meta_object`.*, COUNT(`l1`.oid) AS `count`").
   393  			Join("INNER", "`lfs_meta_object` AS l1", "`lfs_meta_object`.oid = `l1`.oid").
   394  			Where("`lfs_meta_object`.repository_id = ?", repoID)
   395  		if !opts.OlderThan.IsZero() {
   396  			sess.And("`lfs_meta_object`.created_unix < ?", opts.OlderThan)
   397  		}
   398  		if !opts.UpdatedLessRecentlyThan.IsZero() {
   399  			sess.And("`lfs_meta_object`.updated_unix < ?", opts.UpdatedLessRecentlyThan)
   400  		}
   401  		sess.GroupBy("`lfs_meta_object`.id")
   402  		if opts.OrderByUpdated {
   403  			sess.OrderBy("`lfs_meta_object`.updated_unix ASC")
   404  		} else {
   405  			sess.And("`lfs_meta_object`.id > ?", id)
   406  			sess.OrderBy("`lfs_meta_object`.id ASC")
   407  		}
   408  		if err := sess.Limit(batchSize, start).Find(&beans); err != nil {
   409  			return err
   410  		}
   411  		if len(beans) == 0 {
   412  			return nil
   413  		}
   414  		if !opts.LoopFunctionAlwaysUpdates {
   415  			start += len(beans)
   416  		}
   417  
   418  		for _, bean := range beans {
   419  			if err := f(ctx, &bean.LFSMetaObject, bean.Count); err != nil {
   420  				return err
   421  			}
   422  		}
   423  		id = beans[len(beans)-1].ID
   424  	}
   425  }
   426  
   427  // MarkLFSMetaObject updates the updated time for the provided LFSMetaObject
   428  func MarkLFSMetaObject(ctx context.Context, id int64) error {
   429  	obj := &LFSMetaObject{
   430  		UpdatedUnix: timeutil.TimeStampNow(),
   431  	}
   432  	count, err := db.GetEngine(ctx).ID(id).Update(obj)
   433  	if count != 1 {
   434  		log.Error("Unexpectedly updated %d LFSMetaObjects with ID: %d", count, id)
   435  	}
   436  	return err
   437  }