Skip to content

Commit 786965e

Browse files
committed
Add basic repository lfs management
1 parent 2f39fc7 commit 786965e

File tree

10 files changed

+370
-8
lines changed

10 files changed

+370
-8
lines changed

models/lfs.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,47 @@ func (repo *Repository) GetLFSMetaObjectByOid(oid string) (*LFSMetaObject, error
106106

107107
// RemoveLFSMetaObjectByOid removes a LFSMetaObject entry from database by its OID.
108108
// It may return ErrLFSObjectNotExist or a database error.
109-
func (repo *Repository) RemoveLFSMetaObjectByOid(oid string) error {
109+
func (repo *Repository) RemoveLFSMetaObjectByOid(oid string) (int64, error) {
110110
if len(oid) == 0 {
111-
return ErrLFSObjectNotExist
111+
return 0, ErrLFSObjectNotExist
112112
}
113113

114114
sess := x.NewSession()
115115
defer sess.Close()
116116
if err := sess.Begin(); err != nil {
117-
return err
117+
return -1, err
118118
}
119119

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

125-
return sess.Commit()
125+
count, err := sess.Count(&LFSMetaObject{Oid: oid})
126+
if err != nil {
127+
return count, err
128+
}
129+
130+
return count, sess.Commit()
131+
}
132+
133+
// GetLFSMetaObjects returns all LFSMetaObjects associated with a repository
134+
func (repo *Repository) GetLFSMetaObjects(page, pageSize int) ([]*LFSMetaObject, error) {
135+
sess := x.NewSession()
136+
defer sess.Close()
137+
138+
if page >= 0 && pageSize > 0 {
139+
start := 0
140+
if page > 0 {
141+
start = (page - 1) * pageSize
142+
}
143+
sess.Limit(pageSize, start)
144+
}
145+
lfsObjects := make([]*LFSMetaObject, 0, pageSize)
146+
return lfsObjects, sess.Find(&lfsObjects, &LFSMetaObject{RepositoryID: repo.ID})
147+
}
148+
149+
// CountLFSMetaObjects returns a count of all LFSMetaObjects associated with a repository
150+
func (repo *Repository) CountLFSMetaObjects() (int64, error) {
151+
return x.Count(&LFSMetaObject{RepositoryID: repo.ID})
126152
}

modules/lfs/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ func PutHandler(ctx *context.Context) {
330330
if err := contentStore.Put(meta, ctx.Req.Body().ReadCloser()); err != nil {
331331
ctx.Resp.WriteHeader(500)
332332
fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err)
333-
if err = repository.RemoveLFSMetaObjectByOid(rv.Oid); err != nil {
333+
if _, err = repository.RemoveLFSMetaObjectByOid(rv.Oid); err != nil {
334334
log.Error("RemoveLFSMetaObjectByOid: %v", err)
335335
}
336336
return

modules/repofiles/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
374374
contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath}
375375
if !contentStore.Exists(lfsMetaObject) {
376376
if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil {
377-
if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
377+
if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
378378
return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err)
379379
}
380380
return nil, err

modules/repofiles/upload.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func cleanUpAfterFailure(infos *[]uploadInfo, t *TemporaryUploadRepository, orig
3737
continue
3838
}
3939
if !info.lfsMetaObject.Existing {
40-
if err := t.repo.RemoveLFSMetaObjectByOid(info.lfsMetaObject.Oid); err != nil {
40+
if _, err := t.repo.RemoveLFSMetaObjectByOid(info.lfsMetaObject.Oid); err != nil {
4141
original = fmt.Errorf("%v, %v", original, err)
4242
}
4343
}

options/locale/locale_en-US.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,10 @@ settings.unarchive.text = Un-Archiving the repo will restore its ability to rece
13161316
settings.unarchive.success = The repo was successfully un-archived.
13171317
settings.unarchive.error = An error occurred while trying to un-archive the repo. See the log for more details.
13181318
settings.update_avatar_success = The repository avatar has been updated.
1319+
settings.lfs=LFS
1320+
settings.lfs_filelist=LFS files stored in this repository
1321+
settings.lfs_delete=Delete LFS file with OID %s
1322+
settings.lfs_delete_warning=Deleting an LFS file may cause object does not exist errors on checkout. Are you sure?
13191323
13201324
diff.browse_source = Browse Source
13211325
diff.parent = parent

routers/repo/lfs.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package repo
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
gotemplate "html/template"
11+
"io/ioutil"
12+
"os"
13+
"path/filepath"
14+
"strings"
15+
16+
"code.gitea.io/gitea/modules/base"
17+
"code.gitea.io/gitea/modules/context"
18+
"code.gitea.io/gitea/modules/lfs"
19+
"code.gitea.io/gitea/modules/log"
20+
"code.gitea.io/gitea/modules/setting"
21+
"code.gitea.io/gitea/modules/templates"
22+
)
23+
24+
const (
25+
tplSettingsLFS base.TplName = "repo/settings/lfs"
26+
tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
27+
)
28+
29+
// LFSFiles shows a repository's LFS files
30+
func LFSFiles(ctx *context.Context) {
31+
if !setting.LFS.StartServer {
32+
ctx.NotFound("LFSFiles", nil)
33+
return
34+
}
35+
page := ctx.QueryInt("page")
36+
if page <= 1 {
37+
page = 1
38+
}
39+
total, err := ctx.Repo.Repository.CountLFSMetaObjects()
40+
if err != nil {
41+
ctx.ServerError("LFSFiles", err)
42+
return
43+
}
44+
45+
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
46+
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
47+
ctx.Data["PageIsSettingsLFS"] = true
48+
lfsMetaObjects, err := ctx.Repo.Repository.GetLFSMetaObjects(pager.Paginater.Current(), setting.UI.ExplorePagingNum)
49+
if err != nil {
50+
ctx.ServerError("LFSFiles", err)
51+
return
52+
}
53+
ctx.Data["LFSFiles"] = lfsMetaObjects
54+
ctx.Data["Page"] = pager
55+
ctx.HTML(200, tplSettingsLFS)
56+
}
57+
58+
// LFSFileGet serves a single LFS file
59+
func LFSFileGet(ctx *context.Context) {
60+
if !setting.LFS.StartServer {
61+
ctx.NotFound("LFSFileGet", nil)
62+
return
63+
}
64+
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
65+
oid := ctx.Params("oid")
66+
ctx.Data["Title"] = oid
67+
ctx.Data["PageIsSettingsLFS"] = true
68+
meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(oid)
69+
if err != nil {
70+
ctx.ServerError("LFSFileGet", err)
71+
return
72+
}
73+
ctx.Data["LFSFile"] = meta
74+
dataRc, err := lfs.ReadMetaObject(meta)
75+
if err != nil {
76+
ctx.ServerError("LFSFileGet", err)
77+
return
78+
}
79+
defer dataRc.Close()
80+
buf := make([]byte, 1024)
81+
n, err := dataRc.Read(buf)
82+
if err != nil {
83+
ctx.ServerError("Data", err)
84+
return
85+
}
86+
buf = buf[:n]
87+
88+
isTextFile := base.IsTextFile(buf)
89+
ctx.Data["IsTextFile"] = isTextFile
90+
91+
fileSize := meta.Size
92+
ctx.Data["FileSize"] = meta.Size
93+
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct")
94+
switch {
95+
case isTextFile:
96+
if fileSize >= setting.UI.MaxDisplayFileSize {
97+
ctx.Data["IsFileTooLarge"] = true
98+
break
99+
}
100+
101+
d, _ := ioutil.ReadAll(dataRc)
102+
buf = templates.ToUTF8WithFallback(append(buf, d...))
103+
104+
// Building code view blocks with line number on server side.
105+
var fileContent string
106+
if content, err := templates.ToUTF8WithErr(buf); err != nil {
107+
log.Error("ToUTF8WithErr: %v", err)
108+
fileContent = string(buf)
109+
} else {
110+
fileContent = content
111+
}
112+
113+
var output bytes.Buffer
114+
lines := strings.Split(fileContent, "\n")
115+
//Remove blank line at the end of file
116+
if len(lines) > 0 && lines[len(lines)-1] == "" {
117+
lines = lines[:len(lines)-1]
118+
}
119+
for index, line := range lines {
120+
line = gotemplate.HTMLEscapeString(line)
121+
if index != len(lines)-1 {
122+
line += "\n"
123+
}
124+
output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
125+
}
126+
ctx.Data["FileContent"] = gotemplate.HTML(output.String())
127+
128+
output.Reset()
129+
for i := 0; i < len(lines); i++ {
130+
output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
131+
}
132+
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
133+
134+
case base.IsPDFFile(buf):
135+
ctx.Data["IsPDFFile"] = true
136+
case base.IsVideoFile(buf):
137+
ctx.Data["IsVideoFile"] = true
138+
case base.IsAudioFile(buf):
139+
ctx.Data["IsAudioFile"] = true
140+
case base.IsImageFile(buf):
141+
ctx.Data["IsImageFile"] = true
142+
}
143+
ctx.HTML(200, tplSettingsLFSFile)
144+
}
145+
146+
// LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
147+
func LFSDelete(ctx *context.Context) {
148+
if !setting.LFS.StartServer {
149+
ctx.NotFound("LFSFileGet", nil)
150+
return
151+
}
152+
oid := ctx.Params("oid")
153+
count, err := ctx.Repo.Repository.RemoveLFSMetaObjectByOid(oid)
154+
if err != nil {
155+
ctx.ServerError("LFSDelete", err)
156+
return
157+
}
158+
// FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here
159+
// Please note a similar condition happens in models/repo.go DeleteRepository
160+
if count == 0 {
161+
oidPath := filepath.Join(oid[0:2], oid[2:4], oid[4:])
162+
err = os.Remove(filepath.Join(setting.LFS.ContentPath, oidPath))
163+
if err != nil {
164+
ctx.ServerError("LFSDelete", err)
165+
return
166+
}
167+
}
168+
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
169+
}

routers/routes/routes.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,15 @@ func RegisterRoutes(m *macaron.Macaron) {
662662
m.Post("/delete", repo.DeleteDeployKey)
663663
})
664664

665+
m.Group("/lfs", func() {
666+
m.Get("", repo.LFSFiles)
667+
m.Get("/show/:oid", repo.LFSFileGet)
668+
m.Post("/delete/:oid", repo.LFSDelete)
669+
})
670+
665671
}, func(ctx *context.Context) {
666672
ctx.Data["PageIsSettings"] = true
673+
ctx.Data["LFSStartServer"] = setting.LFS.StartServer
667674
})
668675
}, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.UnitTypes(), context.RepoRef())
669676

templates/repo/settings/lfs.tmpl

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{{template "base/head" .}}
2+
<div class="repository settings lfs">
3+
{{template "repo/header" .}}
4+
{{template "repo/settings/navbar" .}}
5+
<div class="ui container">
6+
{{template "base/alert" .}}
7+
<table id="lfs-files-table" class="ui single line table">
8+
<thead>
9+
<tr>
10+
<th colspan="4">{{.i18n.Tr "repo.settings.lfs_filelist"}}</th>
11+
</tr>
12+
</thead>
13+
<tbody>
14+
{{range .LFSFiles}}
15+
<tr>
16+
<td>
17+
<span class="truncate">
18+
<a href="{{$.Link}}/show/{{.Oid}}">
19+
{{.Oid}}
20+
</a>
21+
</span>
22+
</td>
23+
<td>{{FileSize .Size}}</td>
24+
<td>{{TimeSince .CreatedUnix.AsTime $.Lang}}</td>
25+
<td>
26+
<button class="ui basic show-modal icon button" data-modal="#delete-{{.Oid}}">
27+
<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>
28+
</button>
29+
</td>
30+
</tr>
31+
{{end}}
32+
</tbody>
33+
</table>
34+
{{template "base/paginate" .}}
35+
{{range .LFSFiles}}
36+
<div class="ui basic modal" id="delete-{{.Oid}}">
37+
<div class="ui icon header">
38+
{{$.i18n.Tr "repo.settings.lfs_delete" .Oid}}
39+
</div>
40+
<div class="content center">
41+
<p>
42+
{{$.i18n.Tr "repo.settings.lfs_delete_warning"}}
43+
</p>
44+
<form class="ui form" action="{{$.Link}}/delete/{{.Oid}}" method="post">
45+
{{$.CsrfTokenHtml}}
46+
<div class="center actions">
47+
<div class="ui basic cancel inverted button">{{$.i18n.Tr "settings.cancel"}}</div>
48+
<button class="ui basic inverted yellow button">{{$.i18n.Tr "modal.yes"}}</button>
49+
</div>
50+
</form>
51+
</div>
52+
</div>
53+
{{end}}
54+
<div class="ui attached segment lfs list">
55+
56+
</div>
57+
<!--<div class="ui bottom attached segment">
58+
<form class="ui form" id="repo-collab-form" action="{{.Link}}" method="post">
59+
{{.CsrfTokenHtml}}
60+
<div class="inline field ui left">
61+
<div id="search-user-box" class="ui search">
62+
<div class="ui input">
63+
<input class="prompt" name="collaborator" placeholder="{{.i18n.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" autofocus required>
64+
</div>
65+
</div>
66+
</div>
67+
<button class="ui green button">{{.i18n.Tr "repo.settings.add_collaborator"}}</button>
68+
</form>
69+
</div>
70+
-->
71+
</div>
72+
</div>
73+
<!--
74+
<div class="ui small basic delete modal">
75+
<div class="ui icon header">
76+
<i class="trash icon"></i>
77+
{{.i18n.Tr "repo.settings.collaborator_deletion"}}
78+
</div>
79+
<div class="content">
80+
<p>{{.i18n.Tr "repo.settings.collaborator_deletion_desc"}}</p>
81+
</div>
82+
{{template "base/delete_modal_actions" .}}
83+
</div>
84+
-->
85+
{{template "base/footer" .}}

0 commit comments

Comments
 (0)