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 }