Skip to content

Commit

Permalink
Add basic repository lfs management
Browse files Browse the repository at this point in the history
  • Loading branch information
zeripath committed Jun 14, 2019
1 parent 2f39fc7 commit 786965e
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 8 deletions.
36 changes: 31 additions & 5 deletions models/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,47 @@ func (repo *Repository) GetLFSMetaObjectByOid(oid string) (*LFSMetaObject, error

// RemoveLFSMetaObjectByOid removes a LFSMetaObject entry from database by its OID.
// It may return ErrLFSObjectNotExist or a database error.
func (repo *Repository) RemoveLFSMetaObjectByOid(oid string) error {
func (repo *Repository) RemoveLFSMetaObjectByOid(oid string) (int64, error) {
if len(oid) == 0 {
return ErrLFSObjectNotExist
return 0, ErrLFSObjectNotExist
}

sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
return -1, err
}

m := &LFSMetaObject{Oid: oid, RepositoryID: repo.ID}
if _, err := sess.Delete(m); err != nil {
return err
return -1, err
}

return sess.Commit()
count, err := sess.Count(&LFSMetaObject{Oid: oid})
if err != nil {
return count, err
}

return count, sess.Commit()
}

// GetLFSMetaObjects returns all LFSMetaObjects associated with a repository
func (repo *Repository) GetLFSMetaObjects(page, pageSize int) ([]*LFSMetaObject, error) {
sess := x.NewSession()
defer sess.Close()

if page >= 0 && pageSize > 0 {
start := 0
if page > 0 {
start = (page - 1) * pageSize
}
sess.Limit(pageSize, start)
}
lfsObjects := make([]*LFSMetaObject, 0, pageSize)
return lfsObjects, sess.Find(&lfsObjects, &LFSMetaObject{RepositoryID: repo.ID})
}

// CountLFSMetaObjects returns a count of all LFSMetaObjects associated with a repository
func (repo *Repository) CountLFSMetaObjects() (int64, error) {
return x.Count(&LFSMetaObject{RepositoryID: repo.ID})
}
2 changes: 1 addition & 1 deletion modules/lfs/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func PutHandler(ctx *context.Context) {
if err := contentStore.Put(meta, ctx.Req.Body().ReadCloser()); err != nil {
ctx.Resp.WriteHeader(500)
fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err)
if err = repository.RemoveLFSMetaObjectByOid(rv.Oid); err != nil {
if _, err = repository.RemoveLFSMetaObjectByOid(rv.Oid); err != nil {
log.Error("RemoveLFSMetaObjectByOid: %v", err)
}
return
Expand Down
2 changes: 1 addition & 1 deletion modules/repofiles/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath}
if !contentStore.Exists(lfsMetaObject) {
if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil {
if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err)
}
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion modules/repofiles/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func cleanUpAfterFailure(infos *[]uploadInfo, t *TemporaryUploadRepository, orig
continue
}
if !info.lfsMetaObject.Existing {
if err := t.repo.RemoveLFSMetaObjectByOid(info.lfsMetaObject.Oid); err != nil {
if _, err := t.repo.RemoveLFSMetaObjectByOid(info.lfsMetaObject.Oid); err != nil {
original = fmt.Errorf("%v, %v", original, err)
}
}
Expand Down
4 changes: 4 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,10 @@ settings.unarchive.text = Un-Archiving the repo will restore its ability to rece
settings.unarchive.success = The repo was successfully un-archived.
settings.unarchive.error = An error occurred while trying to un-archive the repo. See the log for more details.
settings.update_avatar_success = The repository avatar has been updated.
settings.lfs=LFS
settings.lfs_filelist=LFS files stored in this repository
settings.lfs_delete=Delete LFS file with OID %s
settings.lfs_delete_warning=Deleting an LFS file may cause object does not exist errors on checkout. Are you sure?
diff.browse_source = Browse Source
diff.parent = parent
Expand Down
169 changes: 169 additions & 0 deletions routers/repo/lfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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/ioutil"
"os"
"path/filepath"
"strings"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
)

const (
tplSettingsLFS base.TplName = "repo/settings/lfs"
tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
)

// 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 {
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 = templates.ToUTF8WithFallback(append(buf, d...))

// Building code view blocks with line number on server side.
var fileContent string
if content, err := templates.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("LFSFileGet", 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")
}
7 changes: 7 additions & 0 deletions routers/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,15 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/delete", repo.DeleteDeployKey)
})

m.Group("/lfs", func() {
m.Get("", repo.LFSFiles)
m.Get("/show/:oid", repo.LFSFileGet)
m.Post("/delete/:oid", repo.LFSDelete)
})

}, func(ctx *context.Context) {
ctx.Data["PageIsSettings"] = true
ctx.Data["LFSStartServer"] = setting.LFS.StartServer
})
}, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.UnitTypes(), context.RepoRef())

Expand Down
85 changes: 85 additions & 0 deletions templates/repo/settings/lfs.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{{template "base/head" .}}
<div class="repository settings lfs">
{{template "repo/header" .}}
{{template "repo/settings/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
<table id="lfs-files-table" class="ui single line table">
<thead>
<tr>
<th colspan="4">{{.i18n.Tr "repo.settings.lfs_filelist"}}</th>
</tr>
</thead>
<tbody>
{{range .LFSFiles}}
<tr>
<td>
<span class="truncate">
<a href="{{$.Link}}/show/{{.Oid}}">
{{.Oid}}
</a>
</span>
</td>
<td>{{FileSize .Size}}</td>
<td>{{TimeSince .CreatedUnix.AsTime $.Lang}}</td>
<td>
<button class="ui basic show-modal icon button" data-modal="#delete-{{.Oid}}">
<i class="octicon octicon-trashcan btn-octicon btn-octicon-danger poping up" data-content="{{$.i18n.Tr "repo.editor.delete_this_file"}}" data-position="bottom center" data-variation="tiny inverted"></i>
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
{{template "base/paginate" .}}
{{range .LFSFiles}}
<div class="ui basic modal" id="delete-{{.Oid}}">
<div class="ui icon header">
{{$.i18n.Tr "repo.settings.lfs_delete" .Oid}}
</div>
<div class="content center">
<p>
{{$.i18n.Tr "repo.settings.lfs_delete_warning"}}
</p>
<form class="ui form" action="{{$.Link}}/delete/{{.Oid}}" method="post">
{{$.CsrfTokenHtml}}
<div class="center actions">
<div class="ui basic cancel inverted button">{{$.i18n.Tr "settings.cancel"}}</div>
<button class="ui basic inverted yellow button">{{$.i18n.Tr "modal.yes"}}</button>
</div>
</form>
</div>
</div>
{{end}}
<div class="ui attached segment lfs list">

</div>
<!--<div class="ui bottom attached segment">
<form class="ui form" id="repo-collab-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="inline field ui left">
<div id="search-user-box" class="ui search">
<div class="ui input">
<input class="prompt" name="collaborator" placeholder="{{.i18n.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" autofocus required>
</div>
</div>
</div>
<button class="ui green button">{{.i18n.Tr "repo.settings.add_collaborator"}}</button>
</form>
</div>
-->
</div>
</div>
<!--
<div class="ui small basic delete modal">
<div class="ui icon header">
<i class="trash icon"></i>
{{.i18n.Tr "repo.settings.collaborator_deletion"}}
</div>
<div class="content">
<p>{{.i18n.Tr "repo.settings.collaborator_deletion_desc"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
-->
{{template "base/footer" .}}
Loading

0 comments on commit 786965e

Please sign in to comment.