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  }