code.gitea.io/gitea@v1.21.7/routers/web/repo/setting/lfs.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package setting 5 6 import ( 7 "bytes" 8 "fmt" 9 gotemplate "html/template" 10 "io" 11 "net/http" 12 "net/url" 13 "path" 14 "strconv" 15 "strings" 16 17 git_model "code.gitea.io/gitea/models/git" 18 "code.gitea.io/gitea/modules/base" 19 "code.gitea.io/gitea/modules/charset" 20 "code.gitea.io/gitea/modules/container" 21 "code.gitea.io/gitea/modules/context" 22 "code.gitea.io/gitea/modules/git" 23 "code.gitea.io/gitea/modules/git/pipeline" 24 "code.gitea.io/gitea/modules/lfs" 25 "code.gitea.io/gitea/modules/log" 26 repo_module "code.gitea.io/gitea/modules/repository" 27 "code.gitea.io/gitea/modules/setting" 28 "code.gitea.io/gitea/modules/storage" 29 "code.gitea.io/gitea/modules/typesniffer" 30 "code.gitea.io/gitea/modules/util" 31 ) 32 33 const ( 34 tplSettingsLFS base.TplName = "repo/settings/lfs" 35 tplSettingsLFSLocks base.TplName = "repo/settings/lfs_locks" 36 tplSettingsLFSFile base.TplName = "repo/settings/lfs_file" 37 tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find" 38 tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers" 39 ) 40 41 // LFSFiles shows a repository's LFS files 42 func LFSFiles(ctx *context.Context) { 43 if !setting.LFS.StartServer { 44 ctx.NotFound("LFSFiles", nil) 45 return 46 } 47 page := ctx.FormInt("page") 48 if page <= 1 { 49 page = 1 50 } 51 total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID) 52 if err != nil { 53 ctx.ServerError("LFSFiles", err) 54 return 55 } 56 ctx.Data["Total"] = total 57 58 pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5) 59 ctx.Data["Title"] = ctx.Tr("repo.settings.lfs") 60 ctx.Data["PageIsSettingsLFS"] = true 61 lfsMetaObjects, err := git_model.GetLFSMetaObjects(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum) 62 if err != nil { 63 ctx.ServerError("LFSFiles", err) 64 return 65 } 66 ctx.Data["LFSFiles"] = lfsMetaObjects 67 ctx.Data["Page"] = pager 68 ctx.HTML(http.StatusOK, tplSettingsLFS) 69 } 70 71 // LFSLocks shows a repository's LFS locks 72 func LFSLocks(ctx *context.Context) { 73 if !setting.LFS.StartServer { 74 ctx.NotFound("LFSLocks", nil) 75 return 76 } 77 ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" 78 79 page := ctx.FormInt("page") 80 if page <= 1 { 81 page = 1 82 } 83 total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID) 84 if err != nil { 85 ctx.ServerError("LFSLocks", err) 86 return 87 } 88 ctx.Data["Total"] = total 89 90 pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5) 91 ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks") 92 ctx.Data["PageIsSettingsLFS"] = true 93 lfsLocks, err := git_model.GetLFSLockByRepoID(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum) 94 if err != nil { 95 ctx.ServerError("LFSLocks", err) 96 return 97 } 98 ctx.Data["LFSLocks"] = lfsLocks 99 100 if len(lfsLocks) == 0 { 101 ctx.Data["Page"] = pager 102 ctx.HTML(http.StatusOK, tplSettingsLFSLocks) 103 return 104 } 105 106 // Clone base repo. 107 tmpBasePath, err := repo_module.CreateTemporaryPath("locks") 108 if err != nil { 109 log.Error("Failed to create temporary path: %v", err) 110 ctx.ServerError("LFSLocks", err) 111 return 112 } 113 defer func() { 114 if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { 115 log.Error("LFSLocks: RemoveTemporaryPath: %v", err) 116 } 117 }() 118 119 if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{ 120 Bare: true, 121 Shared: true, 122 }); err != nil { 123 log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err) 124 ctx.ServerError("LFSLocks", fmt.Errorf("failed to clone repository: %s (%w)", ctx.Repo.Repository.FullName(), err)) 125 return 126 } 127 128 gitRepo, err := git.OpenRepository(ctx, tmpBasePath) 129 if err != nil { 130 log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err) 131 ctx.ServerError("LFSLocks", fmt.Errorf("failed to open new temporary repository in: %s %w", tmpBasePath, err)) 132 return 133 } 134 defer gitRepo.Close() 135 136 filenames := make([]string, len(lfsLocks)) 137 138 for i, lock := range lfsLocks { 139 filenames[i] = lock.Path 140 } 141 142 if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil { 143 log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err) 144 ctx.ServerError("LFSLocks", fmt.Errorf("unable to read the default branch to the index: %s (%w)", ctx.Repo.Repository.DefaultBranch, err)) 145 return 146 } 147 148 name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ 149 Attributes: []string{"lockable"}, 150 Filenames: filenames, 151 CachedOnly: true, 152 }) 153 if err != nil { 154 log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err) 155 ctx.ServerError("LFSLocks", err) 156 return 157 } 158 159 lockables := make([]bool, len(lfsLocks)) 160 for i, lock := range lfsLocks { 161 attribute2info, has := name2attribute2info[lock.Path] 162 if !has { 163 continue 164 } 165 if attribute2info["lockable"] != "set" { 166 continue 167 } 168 lockables[i] = true 169 } 170 ctx.Data["Lockables"] = lockables 171 172 filelist, err := gitRepo.LsFiles(filenames...) 173 if err != nil { 174 log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err) 175 ctx.ServerError("LFSLocks", err) 176 return 177 } 178 179 fileset := make(container.Set[string], len(filelist)) 180 fileset.AddMultiple(filelist...) 181 182 linkable := make([]bool, len(lfsLocks)) 183 for i, lock := range lfsLocks { 184 linkable[i] = fileset.Contains(lock.Path) 185 } 186 ctx.Data["Linkable"] = linkable 187 188 ctx.Data["Page"] = pager 189 ctx.HTML(http.StatusOK, tplSettingsLFSLocks) 190 } 191 192 // LFSLockFile locks a file 193 func LFSLockFile(ctx *context.Context) { 194 if !setting.LFS.StartServer { 195 ctx.NotFound("LFSLocks", nil) 196 return 197 } 198 originalPath := ctx.FormString("path") 199 lockPath := originalPath 200 if len(lockPath) == 0 { 201 ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath)) 202 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") 203 return 204 } 205 if lockPath[len(lockPath)-1] == '/' { 206 ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath)) 207 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") 208 return 209 } 210 lockPath = util.PathJoinRel(lockPath) 211 if len(lockPath) == 0 { 212 ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath)) 213 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") 214 return 215 } 216 217 _, err := git_model.CreateLFSLock(ctx, ctx.Repo.Repository, &git_model.LFSLock{ 218 Path: lockPath, 219 OwnerID: ctx.Doer.ID, 220 }) 221 if err != nil { 222 if git_model.IsErrLFSLockAlreadyExist(err) { 223 ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath)) 224 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") 225 return 226 } 227 ctx.ServerError("LFSLockFile", err) 228 return 229 } 230 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") 231 } 232 233 // LFSUnlock forcibly unlocks an LFS lock 234 func LFSUnlock(ctx *context.Context) { 235 if !setting.LFS.StartServer { 236 ctx.NotFound("LFSUnlock", nil) 237 return 238 } 239 _, err := git_model.DeleteLFSLockByID(ctx, ctx.ParamsInt64("lid"), ctx.Repo.Repository, ctx.Doer, true) 240 if err != nil { 241 ctx.ServerError("LFSUnlock", err) 242 return 243 } 244 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") 245 } 246 247 // LFSFileGet serves a single LFS file 248 func LFSFileGet(ctx *context.Context) { 249 if !setting.LFS.StartServer { 250 ctx.NotFound("LFSFileGet", nil) 251 return 252 } 253 ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" 254 oid := ctx.Params("oid") 255 256 p := lfs.Pointer{Oid: oid} 257 if !p.IsValid() { 258 ctx.NotFound("LFSFileGet", nil) 259 return 260 } 261 262 ctx.Data["Title"] = oid 263 ctx.Data["PageIsSettingsLFS"] = true 264 meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid) 265 if err != nil { 266 if err == git_model.ErrLFSObjectNotExist { 267 ctx.NotFound("LFSFileGet", nil) 268 return 269 } 270 ctx.ServerError("LFSFileGet", err) 271 return 272 } 273 ctx.Data["LFSFile"] = meta 274 dataRc, err := lfs.ReadMetaObject(meta.Pointer) 275 if err != nil { 276 ctx.ServerError("LFSFileGet", err) 277 return 278 } 279 defer dataRc.Close() 280 buf := make([]byte, 1024) 281 n, err := util.ReadAtMost(dataRc, buf) 282 if err != nil { 283 ctx.ServerError("Data", err) 284 return 285 } 286 buf = buf[:n] 287 288 st := typesniffer.DetectContentType(buf) 289 ctx.Data["IsTextFile"] = st.IsText() 290 isRepresentableAsText := st.IsRepresentableAsText() 291 292 fileSize := meta.Size 293 ctx.Data["FileSize"] = meta.Size 294 ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct") 295 switch { 296 case isRepresentableAsText: 297 if st.IsSvgImage() { 298 ctx.Data["IsImageFile"] = true 299 } 300 301 if fileSize >= setting.UI.MaxDisplayFileSize { 302 ctx.Data["IsFileTooLarge"] = true 303 break 304 } 305 306 rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) 307 308 // Building code view blocks with line number on server side. 309 escapedContent := &bytes.Buffer{} 310 ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale) 311 312 var output bytes.Buffer 313 lines := strings.Split(escapedContent.String(), "\n") 314 // Remove blank line at the end of file 315 if len(lines) > 0 && lines[len(lines)-1] == "" { 316 lines = lines[:len(lines)-1] 317 } 318 for index, line := range lines { 319 line = gotemplate.HTMLEscapeString(line) 320 if index != len(lines)-1 { 321 line += "\n" 322 } 323 output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line)) 324 } 325 ctx.Data["FileContent"] = gotemplate.HTML(output.String()) 326 327 output.Reset() 328 for i := 0; i < len(lines); i++ { 329 output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1)) 330 } 331 ctx.Data["LineNums"] = gotemplate.HTML(output.String()) 332 333 case st.IsPDF(): 334 ctx.Data["IsPDFFile"] = true 335 case st.IsVideo(): 336 ctx.Data["IsVideoFile"] = true 337 case st.IsAudio(): 338 ctx.Data["IsAudioFile"] = true 339 case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()): 340 ctx.Data["IsImageFile"] = true 341 } 342 ctx.HTML(http.StatusOK, tplSettingsLFSFile) 343 } 344 345 // LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it 346 func LFSDelete(ctx *context.Context) { 347 if !setting.LFS.StartServer { 348 ctx.NotFound("LFSDelete", nil) 349 return 350 } 351 oid := ctx.Params("oid") 352 p := lfs.Pointer{Oid: oid} 353 if !p.IsValid() { 354 ctx.NotFound("LFSDelete", nil) 355 return 356 } 357 358 count, err := git_model.RemoveLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid) 359 if err != nil { 360 ctx.ServerError("LFSDelete", err) 361 return 362 } 363 // FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here 364 // Please note a similar condition happens in models/repo.go DeleteRepository 365 if count == 0 { 366 oidPath := path.Join(oid[0:2], oid[2:4], oid[4:]) 367 err = storage.LFS.Delete(oidPath) 368 if err != nil { 369 ctx.ServerError("LFSDelete", err) 370 return 371 } 372 } 373 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs") 374 } 375 376 // LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha 377 func LFSFileFind(ctx *context.Context) { 378 if !setting.LFS.StartServer { 379 ctx.NotFound("LFSFind", nil) 380 return 381 } 382 oid := ctx.FormString("oid") 383 size := ctx.FormInt64("size") 384 if len(oid) == 0 || size == 0 { 385 ctx.NotFound("LFSFind", nil) 386 return 387 } 388 sha := ctx.FormString("sha") 389 ctx.Data["Title"] = oid 390 ctx.Data["PageIsSettingsLFS"] = true 391 var hash git.SHA1 392 if len(sha) == 0 { 393 pointer := lfs.Pointer{Oid: oid, Size: size} 394 hash = git.ComputeBlobHash([]byte(pointer.StringContent())) 395 sha = hash.String() 396 } else { 397 hash = git.MustIDFromString(sha) 398 } 399 ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" 400 ctx.Data["Oid"] = oid 401 ctx.Data["Size"] = size 402 ctx.Data["SHA"] = sha 403 404 results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, hash) 405 if err != nil && err != io.EOF { 406 log.Error("Failure in FindLFSFile: %v", err) 407 ctx.ServerError("LFSFind: FindLFSFile.", err) 408 return 409 } 410 411 ctx.Data["Results"] = results 412 ctx.HTML(http.StatusOK, tplSettingsLFSFileFind) 413 } 414 415 // LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store 416 func LFSPointerFiles(ctx *context.Context) { 417 if !setting.LFS.StartServer { 418 ctx.NotFound("LFSFileGet", nil) 419 return 420 } 421 ctx.Data["PageIsSettingsLFS"] = true 422 ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" 423 424 var err error 425 err = func() error { 426 pointerChan := make(chan lfs.PointerBlob) 427 errChan := make(chan error, 1) 428 go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan) 429 430 numPointers := 0 431 var numAssociated, numNoExist, numAssociatable int 432 433 type pointerResult struct { 434 SHA string 435 Oid string 436 Size int64 437 InRepo bool 438 Exists bool 439 Accessible bool 440 Associatable bool 441 } 442 443 results := []pointerResult{} 444 445 contentStore := lfs.NewContentStore() 446 repo := ctx.Repo.Repository 447 448 for pointerBlob := range pointerChan { 449 numPointers++ 450 451 result := pointerResult{ 452 SHA: pointerBlob.Hash, 453 Oid: pointerBlob.Oid, 454 Size: pointerBlob.Size, 455 } 456 457 if _, err := git_model.GetLFSMetaObjectByOid(ctx, repo.ID, pointerBlob.Oid); err != nil { 458 if err != git_model.ErrLFSObjectNotExist { 459 return err 460 } 461 } else { 462 result.InRepo = true 463 } 464 465 result.Exists, err = contentStore.Exists(pointerBlob.Pointer) 466 if err != nil { 467 return err 468 } 469 470 if result.Exists { 471 if !result.InRepo { 472 // Can we fix? 473 // OK well that's "simple" 474 // - we need to check whether current user has access to a repo that has access to the file 475 result.Associatable, err = git_model.LFSObjectAccessible(ctx, ctx.Doer, pointerBlob.Oid) 476 if err != nil { 477 return err 478 } 479 if !result.Associatable { 480 associated, err := git_model.ExistsLFSObject(ctx, pointerBlob.Oid) 481 if err != nil { 482 return err 483 } 484 result.Associatable = !associated 485 } 486 } 487 } 488 489 result.Accessible = result.InRepo || result.Associatable 490 491 if result.InRepo { 492 numAssociated++ 493 } 494 if !result.Exists { 495 numNoExist++ 496 } 497 if result.Associatable { 498 numAssociatable++ 499 } 500 501 results = append(results, result) 502 } 503 504 err, has := <-errChan 505 if has { 506 return err 507 } 508 509 ctx.Data["Pointers"] = results 510 ctx.Data["NumPointers"] = numPointers 511 ctx.Data["NumAssociated"] = numAssociated 512 ctx.Data["NumAssociatable"] = numAssociatable 513 ctx.Data["NumNoExist"] = numNoExist 514 ctx.Data["NumNotAssociated"] = numPointers - numAssociated 515 516 return nil 517 }() 518 if err != nil { 519 ctx.ServerError("LFSPointerFiles", err) 520 return 521 } 522 523 ctx.HTML(http.StatusOK, tplSettingsLFSPointers) 524 } 525 526 // LFSAutoAssociate auto associates accessible lfs files 527 func LFSAutoAssociate(ctx *context.Context) { 528 if !setting.LFS.StartServer { 529 ctx.NotFound("LFSAutoAssociate", nil) 530 return 531 } 532 oids := ctx.FormStrings("oid") 533 metas := make([]*git_model.LFSMetaObject, len(oids)) 534 for i, oid := range oids { 535 idx := strings.IndexRune(oid, ' ') 536 if idx < 0 || idx+1 > len(oid) { 537 ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s", oid)) 538 return 539 } 540 var err error 541 metas[i] = &git_model.LFSMetaObject{} 542 metas[i].Size, err = strconv.ParseInt(oid[idx+1:], 10, 64) 543 if err != nil { 544 ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s %w", oid, err)) 545 return 546 } 547 metas[i].Oid = oid[:idx] 548 // metas[i].RepositoryID = ctx.Repo.Repository.ID 549 } 550 if err := git_model.LFSAutoAssociate(ctx, metas, ctx.Doer, ctx.Repo.Repository.ID); err != nil { 551 ctx.ServerError("LFSAutoAssociate", err) 552 return 553 } 554 ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs") 555 }