123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- // 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/context"
- "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"
- )
-
- 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
- }
- 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.ParamsInt64("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.Params("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()
- isRepresentableAsText := st.IsRepresentableAsText()
-
- fileSize := meta.Size
- 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 isRepresentableAsText:
- if st.IsSvgImage() {
- ctx.Data["IsImageFile"] = true
- }
-
- if fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
-
- rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
-
- // Building code view blocks with line number on server side.
- 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(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
- }
- ctx.Data["FileContent"] = gotemplate.HTML(output.String())
-
- output.Reset()
- for i := 0; i < len(lines); i++ {
- output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
- }
- ctx.Data["LineNums"] = gotemplate.HTML(output.String())
-
- case st.IsPDF():
- ctx.Data["IsPDFFile"] = true
- case st.IsVideo():
- ctx.Data["IsVideoFile"] = true
- case st.IsAudio():
- ctx.Data["IsAudioFile"] = true
- case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
- ctx.Data["IsImageFile"] = true
- }
- ctx.HTML(http.StatusOK, tplSettingsLFSFile)
- }
-
- // LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
- func LFSDelete(ctx *context.Context) {
- if !setting.LFS.StartServer {
- ctx.NotFound("LFSDelete", nil)
- return
- }
- oid := ctx.Params("oid")
- p := lfs.Pointer{Oid: oid}
- if !p.IsValid() {
- ctx.NotFound("LFSDelete", nil)
- return
- }
-
- count, err := git_model.RemoveLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid)
- if err != nil {
- ctx.ServerError("LFSDelete", err)
- return
- }
- // FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here
- // Please note a similar condition happens in models/repo.go DeleteRepository
- if count == 0 {
- oidPath := path.Join(oid[0:2], oid[2:4], oid[4:])
- err = storage.LFS.Delete(oidPath)
- if err != nil {
- ctx.ServerError("LFSDelete", err)
- return
- }
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
- }
-
- // LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
- func LFSFileFind(ctx *context.Context) {
- if !setting.LFS.StartServer {
- ctx.NotFound("LFSFind", nil)
- return
- }
- oid := ctx.FormString("oid")
- size := ctx.FormInt64("size")
- if len(oid) == 0 || size == 0 {
- ctx.NotFound("LFSFind", nil)
- return
- }
- sha := ctx.FormString("sha")
- ctx.Data["Title"] = oid
- ctx.Data["PageIsSettingsLFS"] = true
- var hash git.SHA1
- if len(sha) == 0 {
- pointer := lfs.Pointer{Oid: oid, Size: size}
- hash = git.ComputeBlobHash([]byte(pointer.StringContent()))
- sha = hash.String()
- } else {
- hash = git.MustIDFromString(sha)
- }
- ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
- ctx.Data["Oid"] = oid
- ctx.Data["Size"] = size
- ctx.Data["SHA"] = sha
-
- results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, hash)
- if err != nil && err != io.EOF {
- log.Error("Failure in FindLFSFile: %v", err)
- ctx.ServerError("LFSFind: FindLFSFile.", err)
- return
- }
-
- ctx.Data["Results"] = results
- ctx.HTML(http.StatusOK, tplSettingsLFSFileFind)
- }
-
- // LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store
- func LFSPointerFiles(ctx *context.Context) {
- if !setting.LFS.StartServer {
- ctx.NotFound("LFSFileGet", nil)
- return
- }
- ctx.Data["PageIsSettingsLFS"] = true
- ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
-
- var err error
- err = func() error {
- pointerChan := make(chan lfs.PointerBlob)
- errChan := make(chan error, 1)
- go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
-
- numPointers := 0
- var numAssociated, numNoExist, numAssociatable int
-
- type pointerResult struct {
- SHA string
- Oid string
- Size int64
- InRepo bool
- Exists bool
- Accessible bool
- Associatable bool
- }
-
- results := []pointerResult{}
-
- contentStore := lfs.NewContentStore()
- repo := ctx.Repo.Repository
-
- for pointerBlob := range pointerChan {
- numPointers++
-
- result := pointerResult{
- SHA: pointerBlob.Hash,
- Oid: pointerBlob.Oid,
- Size: pointerBlob.Size,
- }
-
- if _, err := git_model.GetLFSMetaObjectByOid(ctx, repo.ID, pointerBlob.Oid); err != nil {
- if err != git_model.ErrLFSObjectNotExist {
- return err
- }
- } else {
- result.InRepo = true
- }
-
- result.Exists, err = contentStore.Exists(pointerBlob.Pointer)
- if err != nil {
- return err
- }
-
- if result.Exists {
- if !result.InRepo {
- // Can we fix?
- // OK well that's "simple"
- // - we need to check whether current user has access to a repo that has access to the file
- result.Associatable, err = git_model.LFSObjectAccessible(ctx, ctx.Doer, pointerBlob.Oid)
- if err != nil {
- return err
- }
- if !result.Associatable {
- associated, err := git_model.ExistsLFSObject(ctx, pointerBlob.Oid)
- if err != nil {
- return err
- }
- result.Associatable = !associated
- }
- }
- }
-
- result.Accessible = result.InRepo || result.Associatable
-
- if result.InRepo {
- numAssociated++
- }
- if !result.Exists {
- numNoExist++
- }
- if result.Associatable {
- numAssociatable++
- }
-
- results = append(results, result)
- }
-
- err, has := <-errChan
- if has {
- return err
- }
-
- ctx.Data["Pointers"] = results
- ctx.Data["NumPointers"] = numPointers
- ctx.Data["NumAssociated"] = numAssociated
- ctx.Data["NumAssociatable"] = numAssociatable
- ctx.Data["NumNoExist"] = numNoExist
- ctx.Data["NumNotAssociated"] = numPointers - numAssociated
-
- return nil
- }()
- if err != nil {
- ctx.ServerError("LFSPointerFiles", err)
- return
- }
-
- ctx.HTML(http.StatusOK, tplSettingsLFSPointers)
- }
-
- // LFSAutoAssociate auto associates accessible lfs files
- func LFSAutoAssociate(ctx *context.Context) {
- if !setting.LFS.StartServer {
- ctx.NotFound("LFSAutoAssociate", nil)
- return
- }
- oids := ctx.FormStrings("oid")
- metas := make([]*git_model.LFSMetaObject, len(oids))
- for i, oid := range oids {
- idx := strings.IndexRune(oid, ' ')
- if idx < 0 || idx+1 > len(oid) {
- ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s", oid))
- return
- }
- var err error
- metas[i] = &git_model.LFSMetaObject{}
- metas[i].Size, err = strconv.ParseInt(oid[idx+1:], 10, 64)
- if err != nil {
- ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s %w", oid, err))
- return
- }
- metas[i].Oid = oid[:idx]
- // metas[i].RepositoryID = ctx.Repo.Repository.ID
- }
- if err := git_model.LFSAutoAssociate(ctx, metas, ctx.Doer, ctx.Repo.Repository.ID); err != nil {
- ctx.ServerError("LFSAutoAssociate", err)
- return
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
- }
|