|
- // Copyright 2019 The Gitea Authors. All rights reserved.
- // Use of this source code is governed by a MIT-style
- // license that can be found in the LICENSE file.
-
- package repo
-
- import (
- "bufio"
- "bytes"
- "fmt"
- gotemplate "html/template"
- "io"
- "io/ioutil"
- "os"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/charset"
- "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"
- "code.gitea.io/gitea/modules/setting"
-
- "github.com/mcuadros/go-version"
- "github.com/unknwon/com"
- gogit "gopkg.in/src-d/go-git.v4"
- "gopkg.in/src-d/go-git.v4/plumbing"
- "gopkg.in/src-d/go-git.v4/plumbing/object"
- )
-
- const (
- tplSettingsLFS base.TplName = "repo/settings/lfs"
- 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.QueryInt("page")
- if page <= 1 {
- page = 1
- }
- total, err := ctx.Repo.Repository.CountLFSMetaObjects()
- if err != nil {
- ctx.ServerError("LFSFiles", err)
- return
- }
-
- pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
- ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
- ctx.Data["PageIsSettingsLFS"] = true
- lfsMetaObjects, err := ctx.Repo.Repository.GetLFSMetaObjects(pager.Paginater.Current(), setting.UI.ExplorePagingNum)
- if err != nil {
- ctx.ServerError("LFSFiles", err)
- return
- }
- ctx.Data["LFSFiles"] = lfsMetaObjects
- ctx.Data["Page"] = pager
- ctx.HTML(200, tplSettingsLFS)
- }
-
- // 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")
- ctx.Data["Title"] = oid
- ctx.Data["PageIsSettingsLFS"] = true
- meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(oid)
- if err != nil {
- if err == models.ErrLFSObjectNotExist {
- ctx.NotFound("LFSFileGet", nil)
- return
- }
- ctx.ServerError("LFSFileGet", err)
- return
- }
- ctx.Data["LFSFile"] = meta
- dataRc, err := lfs.ReadMetaObject(meta)
- if err != nil {
- ctx.ServerError("LFSFileGet", err)
- return
- }
- defer dataRc.Close()
- buf := make([]byte, 1024)
- n, err := dataRc.Read(buf)
- if err != nil {
- ctx.ServerError("Data", err)
- return
- }
- buf = buf[:n]
-
- isTextFile := base.IsTextFile(buf)
- ctx.Data["IsTextFile"] = isTextFile
-
- fileSize := meta.Size
- ctx.Data["FileSize"] = meta.Size
- ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct")
- switch {
- case isTextFile:
- if fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
-
- d, _ := ioutil.ReadAll(dataRc)
- buf = charset.ToUTF8WithFallback(append(buf, d...))
-
- // Building code view blocks with line number on server side.
- var fileContent string
- if content, err := charset.ToUTF8WithErr(buf); err != nil {
- log.Error("ToUTF8WithErr: %v", err)
- fileContent = string(buf)
- } else {
- fileContent = content
- }
-
- var output bytes.Buffer
- lines := strings.Split(fileContent, "\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 base.IsPDFFile(buf):
- ctx.Data["IsPDFFile"] = true
- case base.IsVideoFile(buf):
- ctx.Data["IsVideoFile"] = true
- case base.IsAudioFile(buf):
- ctx.Data["IsAudioFile"] = true
- case base.IsImageFile(buf):
- ctx.Data["IsImageFile"] = true
- }
- ctx.HTML(200, 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")
- count, err := ctx.Repo.Repository.RemoveLFSMetaObjectByOid(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 := filepath.Join(oid[0:2], oid[2:4], oid[4:])
- err = os.Remove(filepath.Join(setting.LFS.ContentPath, oidPath))
- if err != nil {
- ctx.ServerError("LFSDelete", err)
- return
- }
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
- }
-
- type lfsResult struct {
- Name string
- SHA string
- Summary string
- When time.Time
- ParentHashes []plumbing.Hash
- BranchName string
- FullCommitName string
- }
-
- type lfsResultSlice []*lfsResult
-
- func (a lfsResultSlice) Len() int { return len(a) }
- func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
- func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
-
- // 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.Query("oid")
- size := ctx.QueryInt64("size")
- if len(oid) == 0 || size == 0 {
- ctx.NotFound("LFSFind", nil)
- return
- }
- sha := ctx.Query("sha")
- ctx.Data["Title"] = oid
- ctx.Data["PageIsSettingsLFS"] = true
- var hash plumbing.Hash
- if len(sha) == 0 {
- meta := models.LFSMetaObject{Oid: oid, Size: size}
- pointer := meta.Pointer()
- hash = plumbing.ComputeHash(plumbing.BlobObject, []byte(pointer))
- sha = hash.String()
- } else {
- hash = plumbing.NewHash(sha)
- }
- ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
- ctx.Data["Oid"] = oid
- ctx.Data["Size"] = size
- ctx.Data["SHA"] = sha
-
- resultsMap := map[string]*lfsResult{}
- results := make([]*lfsResult, 0)
-
- basePath := ctx.Repo.Repository.RepoPath()
- gogitRepo := ctx.Repo.GitRepo.GoGitRepo()
-
- commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
- Order: gogit.LogOrderCommitterTime,
- All: true,
- })
- if err != nil {
- log.Error("Failed to get GoGit CommitsIter: %v", err)
- ctx.ServerError("LFSFind: Iterate Commits", err)
- return
- }
-
- err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
- tree, err := gitCommit.Tree()
- if err != nil {
- return err
- }
- treeWalker := object.NewTreeWalker(tree, true, nil)
- defer treeWalker.Close()
- for {
- name, entry, err := treeWalker.Next()
- if err == io.EOF {
- break
- }
- if entry.Hash == hash {
- result := lfsResult{
- Name: name,
- SHA: gitCommit.Hash.String(),
- Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
- When: gitCommit.Author.When,
- ParentHashes: gitCommit.ParentHashes,
- }
- resultsMap[gitCommit.Hash.String()+":"+name] = &result
- }
- }
- return nil
- })
- if err != nil && err != io.EOF {
- log.Error("Failure in CommitIter.ForEach: %v", err)
- ctx.ServerError("LFSFind: IterateCommits ForEach", err)
- return
- }
-
- for _, result := range resultsMap {
- hasParent := false
- for _, parentHash := range result.ParentHashes {
- if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
- break
- }
- }
- if !hasParent {
- results = append(results, result)
- }
- }
-
- sort.Sort(lfsResultSlice(results))
-
- // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
- shasToNameReader, shasToNameWriter := io.Pipe()
- nameRevStdinReader, nameRevStdinWriter := io.Pipe()
- errChan := make(chan error, 1)
- wg := sync.WaitGroup{}
- wg.Add(3)
-
- go func() {
- defer wg.Done()
- scanner := bufio.NewScanner(nameRevStdinReader)
- i := 0
- for scanner.Scan() {
- line := scanner.Text()
- if len(line) == 0 {
- continue
- }
- result := results[i]
- result.FullCommitName = line
- result.BranchName = strings.Split(line, "~")[0]
- i++
- }
- }()
- go pipeline.NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath)
- go func() {
- defer wg.Done()
- defer shasToNameWriter.Close()
- for _, result := range results {
- i := 0
- if i < len(result.SHA) {
- n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
- if err != nil {
- errChan <- err
- break
- }
- i += n
- }
- n := 0
- for n < 1 {
- n, err = shasToNameWriter.Write([]byte{'\n'})
- if err != nil {
- errChan <- err
- break
- }
-
- }
-
- }
- }()
-
- wg.Wait()
-
- select {
- case err, has := <-errChan:
- if has {
- ctx.ServerError("LFSPointerFiles", err)
- }
- default:
- }
-
- ctx.Data["Results"] = results
- ctx.HTML(200, 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
- binVersion, err := git.BinVersion()
- if err != nil {
- log.Fatal("Error retrieving git version: %v", err)
- }
- ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
-
- basePath := ctx.Repo.Repository.RepoPath()
-
- pointerChan := make(chan pointerResult)
-
- catFileCheckReader, catFileCheckWriter := io.Pipe()
- shasToBatchReader, shasToBatchWriter := io.Pipe()
- catFileBatchReader, catFileBatchWriter := io.Pipe()
- errChan := make(chan error, 1)
- wg := sync.WaitGroup{}
- wg.Add(5)
-
- var numPointers, numAssociated, numNoExist, numAssociatable int
-
- go func() {
- defer wg.Done()
- pointers := make([]pointerResult, 0, 50)
- for pointer := range pointerChan {
- pointers = append(pointers, pointer)
- if pointer.InRepo {
- numAssociated++
- }
- if !pointer.Exists {
- numNoExist++
- }
- if !pointer.InRepo && pointer.Accessible {
- numAssociatable++
- }
- }
- numPointers = len(pointers)
- ctx.Data["Pointers"] = pointers
- ctx.Data["NumPointers"] = numPointers
- ctx.Data["NumAssociated"] = numAssociated
- ctx.Data["NumAssociatable"] = numAssociatable
- ctx.Data["NumNoExist"] = numNoExist
- ctx.Data["NumNotAssociated"] = numPointers - numAssociated
- }()
- go createPointerResultsFromCatFileBatch(catFileBatchReader, &wg, pointerChan, ctx.Repo.Repository, ctx.User)
- go pipeline.CatFileBatch(shasToBatchReader, catFileBatchWriter, &wg, basePath)
- go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
- if !version.Compare(binVersion, "2.6.0", ">=") {
- revListReader, revListWriter := io.Pipe()
- shasToCheckReader, shasToCheckWriter := io.Pipe()
- wg.Add(2)
- go pipeline.CatFileBatchCheck(shasToCheckReader, catFileCheckWriter, &wg, basePath)
- go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg)
- go pipeline.RevListAllObjects(revListWriter, &wg, basePath, errChan)
- } else {
- go pipeline.CatFileBatchCheckAllObjects(catFileCheckWriter, &wg, basePath, errChan)
- }
- wg.Wait()
-
- select {
- case err, has := <-errChan:
- if has {
- ctx.ServerError("LFSPointerFiles", err)
- }
- default:
- }
- ctx.HTML(200, tplSettingsLFSPointers)
- }
-
- type pointerResult struct {
- SHA string
- Oid string
- Size int64
- InRepo bool
- Exists bool
- Accessible bool
- }
-
- func createPointerResultsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pointerChan chan<- pointerResult, repo *models.Repository, user *models.User) {
- defer wg.Done()
- defer catFileBatchReader.Close()
- contentStore := lfs.ContentStore{BasePath: setting.LFS.ContentPath}
-
- bufferedReader := bufio.NewReader(catFileBatchReader)
- buf := make([]byte, 1025)
- for {
- // File descriptor line: sha
- sha, err := bufferedReader.ReadString(' ')
- if err != nil {
- _ = catFileBatchReader.CloseWithError(err)
- break
- }
- // Throw away the blob
- if _, err := bufferedReader.ReadString(' '); err != nil {
- _ = catFileBatchReader.CloseWithError(err)
- break
- }
- sizeStr, err := bufferedReader.ReadString('\n')
- if err != nil {
- _ = catFileBatchReader.CloseWithError(err)
- break
- }
- size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1])
- if err != nil {
- _ = catFileBatchReader.CloseWithError(err)
- break
- }
- pointerBuf := buf[:size+1]
- if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil {
- _ = catFileBatchReader.CloseWithError(err)
- break
- }
- pointerBuf = pointerBuf[:size]
- // Now we need to check if the pointerBuf is an LFS pointer
- pointer := lfs.IsPointerFile(&pointerBuf)
- if pointer == nil {
- continue
- }
-
- result := pointerResult{
- SHA: strings.TrimSpace(sha),
- Oid: pointer.Oid,
- Size: pointer.Size,
- }
-
- // Then we need to check that this pointer is in the db
- if _, err := repo.GetLFSMetaObjectByOid(pointer.Oid); err != nil {
- if err != models.ErrLFSObjectNotExist {
- _ = catFileBatchReader.CloseWithError(err)
- break
- }
- } else {
- result.InRepo = true
- }
-
- result.Exists = contentStore.Exists(pointer)
-
- 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.Accessible, err = models.LFSObjectAccessible(user, result.Oid)
- if err != nil {
- _ = catFileBatchReader.CloseWithError(err)
- break
- }
- } else {
- result.Accessible = true
- }
- }
- pointerChan <- result
- }
- close(pointerChan)
- }
-
- // LFSAutoAssociate auto associates accessible lfs files
- func LFSAutoAssociate(ctx *context.Context) {
- if !setting.LFS.StartServer {
- ctx.NotFound("LFSAutoAssociate", nil)
- return
- }
- oids := ctx.QueryStrings("oid")
- metas := make([]*models.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] = &models.LFSMetaObject{}
- metas[i].Size, err = com.StrTo(oid[idx+1:]).Int64()
- if err != nil {
- ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s %v", oid, err))
- return
- }
- metas[i].Oid = oid[:idx]
- //metas[i].RepositoryID = ctx.Repo.Repository.ID
- }
- if err := models.LFSAutoAssociate(metas, ctx.User, ctx.Repo.Repository.ID); err != nil {
- ctx.ServerError("LFSAutoAssociate", err)
- return
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
- }
|