code.gitea.io/gitea@v1.22.3/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, repoID int64, p lfs.Pointer) (*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 m, exist, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": p.Oid}) 148 if err != nil { 149 return nil, err 150 } else if exist { 151 m.Existing = true 152 return m, committer.Commit() 153 } 154 155 m = &LFSMetaObject{Pointer: p, RepositoryID: repoID} 156 if err = db.Insert(ctx, m); err != nil { 157 return nil, err 158 } 159 160 return m, committer.Commit() 161 } 162 163 // GetLFSMetaObjectByOid selects a LFSMetaObject entry from database by its OID. 164 // It may return ErrLFSObjectNotExist or a database error. If the error is nil, 165 // the returned pointer is a valid LFSMetaObject. 166 func GetLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (*LFSMetaObject, error) { 167 if len(oid) == 0 { 168 return nil, ErrLFSObjectNotExist 169 } 170 171 m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID} 172 has, err := db.GetEngine(ctx).Get(m) 173 if err != nil { 174 return nil, err 175 } else if !has { 176 return nil, ErrLFSObjectNotExist 177 } 178 return m, nil 179 } 180 181 // RemoveLFSMetaObjectByOid removes a LFSMetaObject entry from database by its OID. 182 // It may return ErrLFSObjectNotExist or a database error. 183 func RemoveLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (int64, error) { 184 return RemoveLFSMetaObjectByOidFn(ctx, repoID, oid, nil) 185 } 186 187 // RemoveLFSMetaObjectByOidFn removes a LFSMetaObject entry from database by its OID. 188 // It may return ErrLFSObjectNotExist or a database error. It will run Fn with the current count within the transaction 189 func RemoveLFSMetaObjectByOidFn(ctx context.Context, repoID int64, oid string, fn func(count int64) error) (int64, error) { 190 if len(oid) == 0 { 191 return 0, ErrLFSObjectNotExist 192 } 193 194 ctx, committer, err := db.TxContext(ctx) 195 if err != nil { 196 return 0, err 197 } 198 defer committer.Close() 199 200 m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID} 201 if _, err := db.DeleteByBean(ctx, m); err != nil { 202 return -1, err 203 } 204 205 count, err := db.CountByBean(ctx, &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}) 206 if err != nil { 207 return count, err 208 } 209 210 if fn != nil { 211 if err := fn(count); err != nil { 212 return count, err 213 } 214 } 215 216 return count, committer.Commit() 217 } 218 219 // GetLFSMetaObjects returns all LFSMetaObjects associated with a repository 220 func GetLFSMetaObjects(ctx context.Context, repoID int64, page, pageSize int) ([]*LFSMetaObject, error) { 221 sess := db.GetEngine(ctx) 222 223 if page >= 0 && pageSize > 0 { 224 start := 0 225 if page > 0 { 226 start = (page - 1) * pageSize 227 } 228 sess.Limit(pageSize, start) 229 } 230 lfsObjects := make([]*LFSMetaObject, 0, pageSize) 231 return lfsObjects, sess.Find(&lfsObjects, &LFSMetaObject{RepositoryID: repoID}) 232 } 233 234 // CountLFSMetaObjects returns a count of all LFSMetaObjects associated with a repository 235 func CountLFSMetaObjects(ctx context.Context, repoID int64) (int64, error) { 236 return db.GetEngine(ctx).Count(&LFSMetaObject{RepositoryID: repoID}) 237 } 238 239 // LFSObjectAccessible checks if a provided Oid is accessible to the user 240 func LFSObjectAccessible(ctx context.Context, user *user_model.User, oid string) (bool, error) { 241 if user.IsAdmin { 242 count, err := db.GetEngine(ctx).Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}) 243 return count > 0, err 244 } 245 cond := repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid) 246 count, err := db.GetEngine(ctx).Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}) 247 return count > 0, err 248 } 249 250 // ExistsLFSObject checks if a provided Oid exists within the DB 251 func ExistsLFSObject(ctx context.Context, oid string) (bool, error) { 252 return db.GetEngine(ctx).Exist(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}) 253 } 254 255 // LFSAutoAssociate auto associates accessible LFSMetaObjects 256 func LFSAutoAssociate(ctx context.Context, metas []*LFSMetaObject, user *user_model.User, repoID int64) error { 257 ctx, committer, err := db.TxContext(ctx) 258 if err != nil { 259 return err 260 } 261 defer committer.Close() 262 263 sess := db.GetEngine(ctx) 264 265 oids := make([]any, len(metas)) 266 oidMap := make(map[string]*LFSMetaObject, len(metas)) 267 for i, meta := range metas { 268 oids[i] = meta.Oid 269 oidMap[meta.Oid] = meta 270 } 271 272 if !user.IsAdmin { 273 newMetas := make([]*LFSMetaObject, 0, len(metas)) 274 cond := builder.In( 275 "`lfs_meta_object`.repository_id", 276 builder.Select("`repository`.id").From("repository").Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)), 277 ) 278 err = sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas) 279 if err != nil { 280 return err 281 } 282 if len(newMetas) != len(oidMap) { 283 return fmt.Errorf("unable collect all LFS objects from database, expected %d, actually %d", len(oidMap), len(newMetas)) 284 } 285 for i := range newMetas { 286 newMetas[i].Size = oidMap[newMetas[i].Oid].Size 287 newMetas[i].RepositoryID = repoID 288 } 289 if err = db.Insert(ctx, newMetas); err != nil { 290 return err 291 } 292 } else { 293 // admin can associate any LFS object to any repository, and we do not care about errors (eg: duplicated unique key), 294 // even if error occurs, it won't hurt users and won't make things worse 295 for i := range metas { 296 p := lfs.Pointer{Oid: metas[i].Oid, Size: metas[i].Size} 297 _, err = sess.Insert(&LFSMetaObject{ 298 Pointer: p, 299 RepositoryID: repoID, 300 }) 301 if err != nil { 302 log.Warn("failed to insert LFS meta object %-v for repo_id: %d into database, err=%v", p, repoID, err) 303 } 304 } 305 } 306 return committer.Commit() 307 } 308 309 // CopyLFS copies LFS data from one repo to another 310 func CopyLFS(ctx context.Context, newRepo, oldRepo *repo_model.Repository) error { 311 var lfsObjects []*LFSMetaObject 312 if err := db.GetEngine(ctx).Where("repository_id=?", oldRepo.ID).Find(&lfsObjects); err != nil { 313 return err 314 } 315 316 for _, v := range lfsObjects { 317 v.ID = 0 318 v.RepositoryID = newRepo.ID 319 if err := db.Insert(ctx, v); err != nil { 320 return err 321 } 322 } 323 324 return nil 325 } 326 327 // GetRepoLFSSize return a repository's lfs files size 328 func GetRepoLFSSize(ctx context.Context, repoID int64) (int64, error) { 329 lfsSize, err := db.GetEngine(ctx).Where("repository_id = ?", repoID).SumInt(new(LFSMetaObject), "size") 330 if err != nil { 331 return 0, fmt.Errorf("updateSize: GetLFSMetaObjects: %w", err) 332 } 333 return lfsSize, nil 334 } 335 336 // IterateRepositoryIDsWithLFSMetaObjects iterates across the repositories that have LFSMetaObjects 337 func IterateRepositoryIDsWithLFSMetaObjects(ctx context.Context, f func(ctx context.Context, repoID, count int64) error) error { 338 batchSize := setting.Database.IterateBufferSize 339 sess := db.GetEngine(ctx) 340 id := int64(0) 341 type RepositoryCount struct { 342 RepositoryID int64 343 Count int64 344 } 345 for { 346 counts := make([]*RepositoryCount, 0, batchSize) 347 sess.Select("repository_id, COUNT(id) AS count"). 348 Table("lfs_meta_object"). 349 Where("repository_id > ?", id). 350 GroupBy("repository_id"). 351 OrderBy("repository_id ASC") 352 353 if err := sess.Limit(batchSize, 0).Find(&counts); err != nil { 354 return err 355 } 356 if len(counts) == 0 { 357 return nil 358 } 359 360 for _, count := range counts { 361 if err := f(ctx, count.RepositoryID, count.Count); err != nil { 362 return err 363 } 364 } 365 id = counts[len(counts)-1].RepositoryID 366 } 367 } 368 369 // IterateLFSMetaObjectsForRepoOptions provides options for IterateLFSMetaObjectsForRepo 370 type IterateLFSMetaObjectsForRepoOptions struct { 371 OlderThan timeutil.TimeStamp 372 UpdatedLessRecentlyThan timeutil.TimeStamp 373 OrderByUpdated bool 374 LoopFunctionAlwaysUpdates bool 375 } 376 377 // IterateLFSMetaObjectsForRepo provides a iterator for LFSMetaObjects per Repo 378 func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(context.Context, *LFSMetaObject, int64) error, opts *IterateLFSMetaObjectsForRepoOptions) error { 379 var start int 380 batchSize := setting.Database.IterateBufferSize 381 engine := db.GetEngine(ctx) 382 type CountLFSMetaObject struct { 383 Count int64 384 LFSMetaObject `xorm:"extends"` 385 } 386 387 id := int64(0) 388 389 for { 390 beans := make([]*CountLFSMetaObject, 0, batchSize) 391 sess := engine.Table("lfs_meta_object").Select("`lfs_meta_object`.*, COUNT(`l1`.oid) AS `count`"). 392 Join("INNER", "`lfs_meta_object` AS l1", "`lfs_meta_object`.oid = `l1`.oid"). 393 Where("`lfs_meta_object`.repository_id = ?", repoID) 394 if !opts.OlderThan.IsZero() { 395 sess.And("`lfs_meta_object`.created_unix < ?", opts.OlderThan) 396 } 397 if !opts.UpdatedLessRecentlyThan.IsZero() { 398 sess.And("`lfs_meta_object`.updated_unix < ?", opts.UpdatedLessRecentlyThan) 399 } 400 sess.GroupBy("`lfs_meta_object`.id") 401 if opts.OrderByUpdated { 402 sess.OrderBy("`lfs_meta_object`.updated_unix ASC") 403 } else { 404 sess.And("`lfs_meta_object`.id > ?", id) 405 sess.OrderBy("`lfs_meta_object`.id ASC") 406 } 407 if err := sess.Limit(batchSize, start).Find(&beans); err != nil { 408 return err 409 } 410 if len(beans) == 0 { 411 return nil 412 } 413 if !opts.LoopFunctionAlwaysUpdates { 414 start += len(beans) 415 } 416 417 for _, bean := range beans { 418 if err := f(ctx, &bean.LFSMetaObject, bean.Count); err != nil { 419 return err 420 } 421 } 422 id = beans[len(beans)-1].ID 423 } 424 } 425 426 // MarkLFSMetaObject updates the updated time for the provided LFSMetaObject 427 func MarkLFSMetaObject(ctx context.Context, id int64) error { 428 obj := &LFSMetaObject{ 429 UpdatedUnix: timeutil.TimeStampNow(), 430 } 431 count, err := db.GetEngine(ctx).ID(id).Update(obj) 432 if count != 1 { 433 log.Error("Unexpectedly updated %d LFSMetaObjects with ID: %d", count, id) 434 } 435 return err 436 }