Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
559 lines
15 KiB
559 lines
15 KiB
// 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 ( |
|
"bytes" |
|
"fmt" |
|
gotemplate "html/template" |
|
"io" |
|
"net/http" |
|
"net/url" |
|
"path" |
|
"strconv" |
|
"strings" |
|
|
|
"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" |
|
"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 := models.CountLFSMetaObjects(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 := models.GetLFSMetaObjects(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 := models.CountLFSLockByRepoID(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 := models.GetLFSLockByRepoID(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 := models.CreateTemporaryPath("locks") |
|
if err != nil { |
|
log.Error("Failed to create temporary path: %v", err) |
|
ctx.ServerError("LFSLocks", err) |
|
return |
|
} |
|
defer func() { |
|
if err := models.RemoveTemporaryPath(tmpBasePath); err != nil { |
|
log.Error("LFSLocks: RemoveTemporaryPath: %v", err) |
|
} |
|
}() |
|
|
|
if err := git.Clone(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 (%v)", ctx.Repo.Repository.FullName(), err)) |
|
return |
|
} |
|
|
|
gitRepo, err := git.OpenRepository(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 %v", 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 (%v)", 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 |
|
} |
|
|
|
filemap := make(map[string]bool, len(filelist)) |
|
for _, name := range filelist { |
|
filemap[name] = true |
|
} |
|
|
|
linkable := make([]bool, len(lfsLocks)) |
|
for i, lock := range lfsLocks { |
|
linkable[i] = filemap[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 = path.Clean("/" + lockPath)[1:] |
|
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 := models.CreateLFSLock(ctx.Repo.Repository, &models.LFSLock{ |
|
Path: lockPath, |
|
OwnerID: ctx.User.ID, |
|
}) |
|
if err != nil { |
|
if models.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 := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.Repo.Repository, ctx.User, 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 := models.GetLFSMetaObjectByOid(ctx.Repo.Repository.ID, 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.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)) |
|
|
|
// Building code view blocks with line number on server side. |
|
escapedContent := &bytes.Buffer{} |
|
ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent) |
|
|
|
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 := models.RemoveLFSMetaObjectByOid(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 |
|
err := git.LoadGitVersion() |
|
if err != nil { |
|
log.Fatal("Error retrieving git version: %v", err) |
|
} |
|
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" |
|
|
|
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 := models.GetLFSMetaObjectByOid(repo.ID, pointerBlob.Oid); err != nil { |
|
if err != models.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 = models.LFSObjectAccessible(ctx.User, pointerBlob.Oid) |
|
if err != nil { |
|
return err |
|
} |
|
if !result.Associatable { |
|
associated, err := models.LFSObjectIsAssociated(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([]*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 = strconv.ParseInt(oid[idx+1:], 10, 64) |
|
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") |
|
}
|
|
|