Skip to content

Commit 7a5465f

Browse files
authored
LFS support to be stored on minio (#12518)
* LFS support to be stored on minio * Fix test * Fix lint * Fix lint * Fix check * Fix test * Update documents and add migration for LFS * Fix some bugs
1 parent e4b3f35 commit 7a5465f

18 files changed

+420
-200
lines changed

cmd/migrate_storage.go

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ func migrateAttachments(dstStorage storage.ObjectStorage) error {
8383
})
8484
}
8585

86+
func migrateLFS(dstStorage storage.ObjectStorage) error {
87+
return models.IterateLFS(func(mo *models.LFSMetaObject) error {
88+
_, err := storage.Copy(dstStorage, mo.RelativePath(), storage.LFS, mo.RelativePath())
89+
return err
90+
})
91+
}
92+
8693
func runMigrateStorage(ctx *cli.Context) error {
8794
if err := initDB(); err != nil {
8895
return err
@@ -103,45 +110,50 @@ func runMigrateStorage(ctx *cli.Context) error {
103110
return err
104111
}
105112

113+
var dstStorage storage.ObjectStorage
114+
var err error
115+
switch ctx.String("store") {
116+
case "local":
117+
p := ctx.String("path")
118+
if p == "" {
119+
log.Fatal("Path must be given when store is loal")
120+
return nil
121+
}
122+
dstStorage, err = storage.NewLocalStorage(p)
123+
case "minio":
124+
dstStorage, err = storage.NewMinioStorage(
125+
context.Background(),
126+
ctx.String("minio-endpoint"),
127+
ctx.String("minio-access-key-id"),
128+
ctx.String("minio-secret-access-key"),
129+
ctx.String("minio-bucket"),
130+
ctx.String("minio-location"),
131+
ctx.String("minio-base-path"),
132+
ctx.Bool("minio-use-ssl"),
133+
)
134+
default:
135+
return fmt.Errorf("Unsupported attachments store type: %s", ctx.String("store"))
136+
}
137+
138+
if err != nil {
139+
return err
140+
}
141+
106142
tp := ctx.String("type")
107143
switch tp {
108144
case "attachments":
109-
var dstStorage storage.ObjectStorage
110-
var err error
111-
switch ctx.String("store") {
112-
case "local":
113-
p := ctx.String("path")
114-
if p == "" {
115-
log.Fatal("Path must be given when store is loal")
116-
return nil
117-
}
118-
dstStorage, err = storage.NewLocalStorage(p)
119-
case "minio":
120-
dstStorage, err = storage.NewMinioStorage(
121-
context.Background(),
122-
ctx.String("minio-endpoint"),
123-
ctx.String("minio-access-key-id"),
124-
ctx.String("minio-secret-access-key"),
125-
ctx.String("minio-bucket"),
126-
ctx.String("minio-location"),
127-
ctx.String("minio-base-path"),
128-
ctx.Bool("minio-use-ssl"),
129-
)
130-
default:
131-
return fmt.Errorf("Unsupported attachments store type: %s", ctx.String("store"))
132-
}
133-
134-
if err != nil {
145+
if err := migrateAttachments(dstStorage); err != nil {
135146
return err
136147
}
137-
if err := migrateAttachments(dstStorage); err != nil {
148+
case "lfs":
149+
if err := migrateLFS(dstStorage); err != nil {
138150
return err
139151
}
140-
141-
log.Warn("All files have been copied to the new placement but old files are still on the orignial placement.")
142-
143-
return nil
152+
default:
153+
return fmt.Errorf("Unsupported storage: %s", ctx.String("type"))
144154
}
145155

156+
log.Warn("All files have been copied to the new placement but old files are still on the orignial placement.")
157+
146158
return nil
147159
}

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,23 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
206206
- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars.
207207
- `ENABLE_GZIP`: **false**: Enables application-level GZIP support.
208208
- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login\].
209+
209210
- `LFS_START_SERVER`: **false**: Enables git-lfs support.
210-
- `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files.
211+
- `LFS_STORE_TYPE`: **local**: Storage type for lfs, `local` for local disk or `minio` for s3 compatible object storage service.
212+
- `LFS_SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
213+
- `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files, only available when `LFS_STORE_TYPE` is `local`.
214+
- `LFS_MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `LFS_STORE_TYPE` is `minio`
215+
- `LFS_MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `LFS_STORE_TYPE` is `minio`
216+
- `LFS_MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `LFS_STORE_TYPE is` `minio`
217+
- `LFS_MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `LFS_STORE_TYPE` is `minio`
218+
- `LFS_MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `LFS_STORE_TYPE` is `minio`
219+
- `LFS_MINIO_BASE_PATH`: **lfs/**: Minio base path on the bucket only available when `LFS_STORE_TYPE` is `minio`
220+
- `LFS_MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `LFS_STORE_TYPE` is `minio`
211221
- `LFS_JWT_SECRET`: **\<empty\>**: LFS authentication secret, change this a unique string.
212222
- `LFS_HTTP_AUTH_EXPIRY`: **20m**: LFS authentication validity period in time.Duration, pushes taking longer than this may fail.
213223
- `LFS_MAX_FILE_SIZE`: **0**: Maximum allowed LFS file size in bytes (Set to 0 for no limit).
214224
- `LFS_LOCK_PAGING_NUM`: **50**: Maximum number of LFS Locks returned per page.
225+
215226
- `REDIRECT_OTHER_PORT`: **false**: If true and `PROTOCOL` is https, allows redirecting http requests on `PORT_TO_REDIRECT` to the https port Gitea listens on.
216227
- `PORT_TO_REDIRECT`: **80**: Port for the http redirection service to listen on. Used when `REDIRECT_OTHER_PORT` is true.
217228
- `ENABLE_LETSENCRYPT`: **false**: If enabled you must set `DOMAIN` to valid internet facing domain (ensure DNS is set and port 80 is accessible by letsencrypt validation server).

docs/content/doc/advanced/config-cheat-sheet.zh-cn.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,18 @@ menu:
6969
- `STATIC_CACHE_TIME`: **6h**: 静态资源文件,包括 `custom/`, `public/` 和所有上传的头像的浏览器缓存时间。
7070
- `ENABLE_GZIP`: 启用应用级别的 GZIP 压缩。
7171
- `LANDING_PAGE`: 未登录用户的默认页面,可选 `home``explore`
72+
7273
- `LFS_START_SERVER`: 是否启用 git-lfs 支持. 可以为 `true``false`, 默认是 `false`
74+
- `LFS_STORE_TYPE`: **local**: LFS 的存储类型,`local` 将存储到磁盘,`minio` 将存储到 s3 兼容的对象服务。
75+
- `LFS_SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。
7376
- `LFS_CONTENT_PATH`: 存放 lfs 命令上传的文件的地方,默认是 `data/lfs`
77+
- `LFS_MINIO_ENDPOINT`: **localhost:9000**: Minio 地址,仅当 `LFS_STORE_TYPE``minio` 时有效。
78+
- `LFS_MINIO_ACCESS_KEY_ID`: Minio accessKeyID,仅当 `LFS_STORE_TYPE``minio` 时有效。
79+
- `LFS_MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `LFS_STORE_TYPE``minio` 时有效。
80+
- `LFS_MINIO_BUCKET`: **gitea**: Minio bucket,仅当 `LFS_STORE_TYPE``minio` 时有效。
81+
- `LFS_MINIO_LOCATION`: **us-east-1**: Minio location ,仅当 `LFS_STORE_TYPE``minio` 时有效。
82+
- `LFS_MINIO_BASE_PATH`: **lfs/**: Minio base path ,仅当 `LFS_STORE_TYPE``minio` 时有效。
83+
- `LFS_MINIO_USE_SSL`: **false**: Minio 是否启用 ssl ,仅当 `LFS_STORE_TYPE``minio` 时有效。
7484
- `LFS_JWT_SECRET`: LFS 认证密钥,改成自己的。
7585

7686
## Database (`database`)

integrations/lfs_getobject_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"code.gitea.io/gitea/models"
1919
"code.gitea.io/gitea/modules/lfs"
2020
"code.gitea.io/gitea/modules/setting"
21+
"code.gitea.io/gitea/modules/storage"
2122

2223
"gitea.com/macaron/gzip"
2324
gzipp "github.com/klauspost/compress/gzip"
@@ -49,8 +50,10 @@ func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string
4950
lfsID++
5051
lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
5152
assert.NoError(t, err)
52-
contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath}
53-
if !contentStore.Exists(lfsMetaObject) {
53+
contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS}
54+
exist, err := contentStore.Exists(lfsMetaObject)
55+
assert.NoError(t, err)
56+
if !exist {
5457
err := contentStore.Put(lfsMetaObject, bytes.NewReader(*content))
5558
assert.NoError(t, err)
5659
}

integrations/mysql.ini.tmpl

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,23 @@ ROOT_URL = http://localhost:3001/
3636
DISABLE_SSH = false
3737
SSH_LISTEN_HOST = localhost
3838
SSH_PORT = 2201
39+
APP_DATA_PATH = integrations/gitea-integration-mysql/data
40+
BUILTIN_SSH_SERVER_USER = git
3941
START_SSH_SERVER = true
42+
OFFLINE_MODE = false
43+
4044
LFS_START_SERVER = true
4145
LFS_CONTENT_PATH = integrations/gitea-integration-mysql/datalfs-mysql
42-
OFFLINE_MODE = false
4346
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
44-
APP_DATA_PATH = integrations/gitea-integration-mysql/data
45-
BUILTIN_SSH_SERVER_USER = git
47+
LFS_STORE_TYPE = minio
48+
LFS_SERVE_DIRECT = false
49+
LFS_MINIO_ENDPOINT = minio:9000
50+
LFS_MINIO_ACCESS_KEY_ID = 123456
51+
LFS_MINIO_SECRET_ACCESS_KEY = 12345678
52+
LFS_MINIO_BUCKET = gitea
53+
LFS_MINIO_LOCATION = us-east-1
54+
LFS_MINIO_BASE_PATH = lfs/
55+
LFS_MINIO_USE_SSL = false
4656

4757
[attachment]
4858
STORE_TYPE = minio

models/lfs.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"errors"
1111
"fmt"
1212
"io"
13+
"path"
1314

1415
"code.gitea.io/gitea/modules/timeutil"
1516

@@ -26,6 +27,15 @@ type LFSMetaObject struct {
2627
CreatedUnix timeutil.TimeStamp `xorm:"created"`
2728
}
2829

30+
// RelativePath returns the relative path of the lfs object
31+
func (m *LFSMetaObject) RelativePath() string {
32+
if len(m.Oid) < 5 {
33+
return m.Oid
34+
}
35+
36+
return path.Join(m.Oid[0:2], m.Oid[2:4], m.Oid[4:])
37+
}
38+
2939
// Pointer returns the string representation of an LFS pointer file
3040
func (m *LFSMetaObject) Pointer() string {
3141
return fmt.Sprintf("%s\n%s%s\nsize %d\n", LFSMetaFileIdentifier, LFSMetaFileOidPrefix, m.Oid, m.Size)
@@ -202,3 +212,25 @@ func LFSAutoAssociate(metas []*LFSMetaObject, user *User, repoID int64) error {
202212

203213
return sess.Commit()
204214
}
215+
216+
// IterateLFS iterates lfs object
217+
func IterateLFS(f func(mo *LFSMetaObject) error) error {
218+
var start int
219+
const batchSize = 100
220+
for {
221+
var mos = make([]*LFSMetaObject, 0, batchSize)
222+
if err := x.Limit(batchSize, start).Find(&mos); err != nil {
223+
return err
224+
}
225+
if len(mos) == 0 {
226+
return nil
227+
}
228+
start += len(mos)
229+
230+
for _, mo := range mos {
231+
if err := f(mo); err != nil {
232+
return err
233+
}
234+
}
235+
}
236+
}

models/unit_tests.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func MainTest(m *testing.M, pathToGiteaRoot string) {
6969
}
7070

7171
setting.Attachment.Path = filepath.Join(setting.AppDataPath, "attachments")
72+
setting.LFS.ContentPath = filepath.Join(setting.AppDataPath, "lfs")
7273
if err = storage.Init(); err != nil {
7374
fatalTestError("storage.Init: %v\n", err)
7475
}

modules/lfs/content_store.go

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ import (
1010
"errors"
1111
"io"
1212
"os"
13-
"path/filepath"
1413

1514
"code.gitea.io/gitea/models"
1615
"code.gitea.io/gitea/modules/log"
17-
"code.gitea.io/gitea/modules/util"
16+
"code.gitea.io/gitea/modules/storage"
1817
)
1918

2019
var (
@@ -24,17 +23,15 @@ var (
2423

2524
// ContentStore provides a simple file system based storage.
2625
type ContentStore struct {
27-
BasePath string
26+
storage.ObjectStorage
2827
}
2928

3029
// Get takes a Meta object and retrieves the content from the store, returning
3130
// it as an io.Reader. If fromByte > 0, the reader starts from that byte
3231
func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadCloser, error) {
33-
path := filepath.Join(s.BasePath, transformKey(meta.Oid))
34-
35-
f, err := os.Open(path)
32+
f, err := s.Open(meta.RelativePath())
3633
if err != nil {
37-
log.Error("Whilst trying to read LFS OID[%s]: Unable to open %s Error: %v", meta.Oid, path, err)
34+
log.Error("Whilst trying to read LFS OID[%s]: Unable to open Error: %v", meta.Oid, err)
3835
return nil, err
3936
}
4037
if fromByte > 0 {
@@ -48,82 +45,55 @@ func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadC
4845

4946
// Put takes a Meta object and an io.Reader and writes the content to the store.
5047
func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error {
51-
path := filepath.Join(s.BasePath, transformKey(meta.Oid))
52-
tmpPath := path + ".tmp"
53-
54-
dir := filepath.Dir(path)
55-
if err := os.MkdirAll(dir, 0750); err != nil {
56-
log.Error("Whilst putting LFS OID[%s]: Unable to create the LFS directory: %s Error: %v", meta.Oid, dir, err)
57-
return err
58-
}
59-
60-
file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0640)
61-
if err != nil {
62-
log.Error("Whilst putting LFS OID[%s]: Unable to open temporary file for writing: %s Error: %v", tmpPath, err)
63-
return err
64-
}
65-
defer func() {
66-
if err := util.Remove(tmpPath); err != nil {
67-
log.Warn("Unable to remove temporary path: %s: Error: %v", tmpPath, err)
68-
}
69-
}()
70-
7148
hash := sha256.New()
72-
hw := io.MultiWriter(hash, file)
73-
74-
written, err := io.Copy(hw, r)
49+
rd := io.TeeReader(r, hash)
50+
p := meta.RelativePath()
51+
written, err := s.Save(p, rd)
7552
if err != nil {
76-
log.Error("Whilst putting LFS OID[%s]: Failed to copy to tmpPath: %s Error: %v", meta.Oid, tmpPath, err)
77-
file.Close()
53+
log.Error("Whilst putting LFS OID[%s]: Failed to copy to tmpPath: %s Error: %v", meta.Oid, p, err)
7854
return err
7955
}
80-
file.Close()
8156

8257
if written != meta.Size {
58+
if err := s.Delete(p); err != nil {
59+
log.Error("Cleaning the LFS OID[%s] failed: %v", meta.Oid, err)
60+
}
8361
return errSizeMismatch
8462
}
8563

8664
shaStr := hex.EncodeToString(hash.Sum(nil))
8765
if shaStr != meta.Oid {
66+
if err := s.Delete(p); err != nil {
67+
log.Error("Cleaning the LFS OID[%s] failed: %v", meta.Oid, err)
68+
}
8869
return errHashMismatch
8970
}
9071

91-
if err := os.Rename(tmpPath, path); err != nil {
92-
log.Error("Whilst putting LFS OID[%s]: Unable to move tmp file to final destination: %s Error: %v", meta.Oid, path, err)
93-
return err
94-
}
95-
9672
return nil
9773
}
9874

9975
// Exists returns true if the object exists in the content store.
100-
func (s *ContentStore) Exists(meta *models.LFSMetaObject) bool {
101-
path := filepath.Join(s.BasePath, transformKey(meta.Oid))
102-
if _, err := os.Stat(path); os.IsNotExist(err) {
103-
return false
76+
func (s *ContentStore) Exists(meta *models.LFSMetaObject) (bool, error) {
77+
_, err := s.ObjectStorage.Stat(meta.RelativePath())
78+
if err != nil {
79+
if os.IsNotExist(err) {
80+
return false, nil
81+
}
82+
return false, err
10483
}
105-
return true
84+
return true, nil
10685
}
10786

10887
// Verify returns true if the object exists in the content store and size is correct.
10988
func (s *ContentStore) Verify(meta *models.LFSMetaObject) (bool, error) {
110-
path := filepath.Join(s.BasePath, transformKey(meta.Oid))
111-
112-
fi, err := os.Stat(path)
113-
if os.IsNotExist(err) || err == nil && fi.Size() != meta.Size {
89+
p := meta.RelativePath()
90+
fi, err := s.ObjectStorage.Stat(p)
91+
if os.IsNotExist(err) || (err == nil && fi.Size() != meta.Size) {
11492
return false, nil
11593
} else if err != nil {
116-
log.Error("Unable stat file: %s for LFS OID[%s] Error: %v", path, meta.Oid, err)
94+
log.Error("Unable stat file: %s for LFS OID[%s] Error: %v", p, meta.Oid, err)
11795
return false, err
11896
}
11997

12098
return true, nil
12199
}
122-
123-
func transformKey(key string) string {
124-
if len(key) < 5 {
125-
return key
126-
}
127-
128-
return filepath.Join(key[0:2], key[2:4], key[4:])
129-
}

0 commit comments

Comments
 (0)