// Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package setting import ( "bytes" "fmt" gotemplate "html/template" "io" "net/http" "net/url" "path" "strconv" "strings" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/pipeline" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) const ( tplSettingsLFS base.TplName = "repo/settings/lfs" tplSettingsLFSLocks base.TplName = "repo/settings/lfs_locks" tplSettingsLFSFile base.TplName = "repo/settings/lfs_file" tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find" tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers" ) // LFSFiles shows a repository's LFS files func LFSFiles(ctx *context.Context) { if !setting.LFS.StartServer { ctx.NotFound("LFSFiles", nil) return } page := ctx.FormInt("page") if page <= 1 { page = 1 } total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("LFSFiles", err) return } ctx.Data["Total"] = total pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5) ctx.Data["Title"] = ctx.Tr("repo.settings.lfs") ctx.Data["PageIsSettingsLFS"] = true lfsMetaObjects, err := git_model.GetLFSMetaObjects(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum) if err != nil { ctx.ServerError("LFSFiles", err) return } ctx.Data["LFSFiles"] = lfsMetaObjects ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsLFS) } // LFSLocks shows a repository's LFS locks func LFSLocks(ctx *context.Context) { if !setting.LFS.StartServer { ctx.NotFound("LFSLocks", nil) return } ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" page := ctx.FormInt("page") if page <= 1 { page = 1 } total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("LFSLocks", err) return } ctx.Data["Total"] = total pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5) ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks") ctx.Data["PageIsSettingsLFS"] = true lfsLocks, err := git_model.GetLFSLockByRepoID(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum) if err != nil { ctx.ServerError("LFSLocks", err) return } if err := lfsLocks.LoadAttributes(ctx); err != nil { ctx.ServerError("LFSLocks", err) return } ctx.Data["LFSLocks"] = lfsLocks if len(lfsLocks) == 0 { ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsLFSLocks) return } // Clone base repo. tmpBasePath, err := repo_module.CreateTemporaryPath("locks") if err != nil { log.Error("Failed to create temporary path: %v", err) ctx.ServerError("LFSLocks", err) return } defer func() { if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { log.Error("LFSLocks: RemoveTemporaryPath: %v", err) } }() if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{ Bare: true, Shared: true, }); err != nil { log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err) ctx.ServerError("LFSLocks", fmt.Errorf("failed to clone repository: %s (%w)", ctx.Repo.Repository.FullName(), err)) return } gitRepo, err := git.OpenRepository(ctx, tmpBasePath) if err != nil { log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err) ctx.ServerError("LFSLocks", fmt.Errorf("failed to open new temporary repository in: %s %w", tmpBasePath, err)) return } defer gitRepo.Close() filenames := make([]string, len(lfsLocks)) for i, lock := range lfsLocks { filenames[i] = lock.Path } if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil { log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err) ctx.ServerError("LFSLocks", fmt.Errorf("unable to read the default branch to the index: %s (%w)", ctx.Repo.Repository.DefaultBranch, err)) return } name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ Attributes: []string{"lockable"}, Filenames: filenames, CachedOnly: true, }) if err != nil { log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err) ctx.ServerError("LFSLocks", err) return } lockables := make([]bool, len(lfsLocks)) for i, lock := range lfsLocks { attribute2info, has := name2attribute2info[lock.Path] if !has { continue } if attribute2info["lockable"] != "set" { continue } lockables[i] = true } ctx.Data["Lockables"] = lockables filelist, err := gitRepo.LsFiles(filenames...) if err != nil { log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err) ctx.ServerError("LFSLocks", err) return } fileset := make(container.Set[string], len(filelist)) fileset.AddMultiple(filelist...) linkable := make([]bool, len(lfsLocks)) for i, lock := range lfsLocks { linkable[i] = fileset.Contains(lock.Path) } ctx.Data["Linkable"] = linkable ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsLFSLocks) } // LFSLockFile locks a file func LFSLockFile(ctx *context.Context) { if !setting.LFS.StartServer { ctx.NotFound("LFSLocks", nil) return } originalPath := ctx.FormString("path") lockPath := originalPath if len(lockPath) == 0 { ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath)) ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") return } if lockPath[len(lockPath)-1] == '/' { ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath)) ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") return } lockPath = util.PathJoinRel(lockPath) if len(lockPath) == 0 { ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath)) ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") return } _, err := git_model.CreateLFSLock(ctx, ctx.Repo.Repository, &git_model.LFSLock{ Path: lockPath, OwnerID: ctx.Doer.ID, }) if err != nil { if git_model.IsErrLFSLockAlreadyExist(err) { ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath)) ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") return } ctx.ServerError("LFSLockFile", err) return } ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") } // LFSUnlock forcibly unlocks an LFS lock func LFSUnlock(ctx *context.Context) { if !setting.LFS.StartServer { ctx.NotFound("LFSUnlock", nil) return } _, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), ctx.Repo.Repository, ctx.Doer, true) if err != nil { ctx.ServerError("LFSUnlock", err) return } ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") } // LFSFileGet serves a single LFS file func LFSFileGet(ctx *context.Context) { if !setting.LFS.StartServer { ctx.NotFound("LFSFileGet", nil) return } ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" oid := ctx.PathParam("oid") p := lfs.Pointer{Oid: oid} if !p.IsValid() { ctx.NotFound("LFSFileGet", nil) return } ctx.Data["Title"] = oid ctx.Data["PageIsSettingsLFS"] = true meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid) if err != nil { if err == git_model.ErrLFSObjectNotExist { ctx.NotFound("LFSFileGet", nil) return } ctx.ServerError("LFSFileGet", err) return } ctx.Data["LFSFile"] = meta dataRc, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { ctx.ServerError("LFSFileGet", err) return } defer dataRc.Close() buf := make([]byte, 1024) n, err := util.ReadAtMost(dataRc, buf) if err != nil { ctx.ServerError("Data", err) return } buf = buf[:n] st := typesniffer.DetectContentType(buf) ctx.Data["IsTextFile"] = st.IsText() ctx.Data["FileSize"] = meta.Size 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") switch { case st.IsRepresentableAsText(): if meta.Size >= setting.UI.MaxDisplayFileSize { ctx.Data["IsFileTooLarge"] = true break } if st.IsSvgImage() { ctx.Data["IsImageFile"] = true } rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) // Building code view blocks with line number on server side. // FIXME: the logic is not right here: it first calls EscapeControlReader then calls HTMLEscapeString: double-escaping escapedContent := &bytes.Buffer{} ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale) var output bytes.Buffer lines := strings.Split(escapedContent.String(), "\n") // Remove blank line at the end of file if len(lines) > 0 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } for index, line := range lines { line = gotemplate.HTMLEscapeString(line) if index != len(lines)-1 { line += "\n" } output.WriteString(fmt.Sprintf(`