mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Add default storage configurations (#12813)
Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
		@@ -98,3 +98,6 @@ issues:
 | 
			
		||||
    - path: models/update.go
 | 
			
		||||
      linters:
 | 
			
		||||
        - unused
 | 
			
		||||
    - path: cmd/dump.go
 | 
			
		||||
      linters:
 | 
			
		||||
        - dupl
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								cmd/dump.go
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								cmd/dump.go
									
									
									
									
									
								
							@@ -18,6 +18,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"gitea.com/macaron/session"
 | 
			
		||||
@@ -57,6 +58,8 @@ func addRecursive(w archiver.Writer, dirPath string, absPath string, verbose boo
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Could not open directory %s: %s", absPath, err)
 | 
			
		||||
	}
 | 
			
		||||
	defer dir.Close()
 | 
			
		||||
 | 
			
		||||
	files, err := dir.Readdir(0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Unable to list files in %s: %s", absPath, err)
 | 
			
		||||
@@ -197,6 +200,10 @@ func runDump(ctx *cli.Context) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := storage.Init(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if file == nil {
 | 
			
		||||
		file, err = os.Create(fileName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -231,11 +238,21 @@ func runDump(ctx *cli.Context) error {
 | 
			
		||||
			fatal("Failed to include repositories: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := os.Stat(setting.LFS.ContentPath); !os.IsNotExist(err) {
 | 
			
		||||
			log.Info("Dumping lfs... %s", setting.LFS.ContentPath)
 | 
			
		||||
			if err := addRecursive(w, "lfs", setting.LFS.ContentPath, verbose); err != nil {
 | 
			
		||||
				fatal("Failed to include lfs: %v", err)
 | 
			
		||||
		if err := storage.LFS.IterateObjects(func(objPath string, object storage.Object) error {
 | 
			
		||||
			info, err := object.Stat()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return w.Write(archiver.File{
 | 
			
		||||
				FileInfo: archiver.FileInfo{
 | 
			
		||||
					FileInfo:   info,
 | 
			
		||||
					CustomName: path.Join("data", "lfs", objPath),
 | 
			
		||||
				},
 | 
			
		||||
				ReadCloser: object,
 | 
			
		||||
			})
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			fatal("Failed to dump LFS objects: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -302,13 +319,31 @@ func runDump(ctx *cli.Context) error {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		excludes = append(excludes, setting.RepoRootPath)
 | 
			
		||||
		excludes = append(excludes, setting.LFS.ContentPath)
 | 
			
		||||
		excludes = append(excludes, setting.LFS.Path)
 | 
			
		||||
		excludes = append(excludes, setting.Attachment.Path)
 | 
			
		||||
		excludes = append(excludes, setting.LogRootPath)
 | 
			
		||||
		if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
 | 
			
		||||
			fatal("Failed to include data directory: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := storage.Attachments.IterateObjects(func(objPath string, object storage.Object) error {
 | 
			
		||||
		info, err := object.Stat()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return w.Write(archiver.File{
 | 
			
		||||
			FileInfo: archiver.FileInfo{
 | 
			
		||||
				FileInfo:   info,
 | 
			
		||||
				CustomName: path.Join("data", "attachments", objPath),
 | 
			
		||||
			},
 | 
			
		||||
			ReadCloser: object,
 | 
			
		||||
		})
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fatal("Failed to dump attachments: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
 | 
			
		||||
	// ensuring that it's clear the dump is skipped whether the directory's initialized
 | 
			
		||||
	// yet or not.
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ package cmd
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/models/migrations"
 | 
			
		||||
@@ -30,8 +31,8 @@ var CmdMigrateStorage = cli.Command{
 | 
			
		||||
			Usage: "Kinds of files to migrate, currently only 'attachments' is supported",
 | 
			
		||||
		},
 | 
			
		||||
		cli.StringFlag{
 | 
			
		||||
			Name:  "store, s",
 | 
			
		||||
			Value: "local",
 | 
			
		||||
			Name:  "storage, s",
 | 
			
		||||
			Value: setting.LocalStorageType,
 | 
			
		||||
			Usage: "New storage type, local or minio",
 | 
			
		||||
		},
 | 
			
		||||
		cli.StringFlag{
 | 
			
		||||
@@ -112,15 +113,15 @@ func runMigrateStorage(ctx *cli.Context) error {
 | 
			
		||||
 | 
			
		||||
	var dstStorage storage.ObjectStorage
 | 
			
		||||
	var err error
 | 
			
		||||
	switch ctx.String("store") {
 | 
			
		||||
	case "local":
 | 
			
		||||
	switch strings.ToLower(ctx.String("storage")) {
 | 
			
		||||
	case setting.LocalStorageType:
 | 
			
		||||
		p := ctx.String("path")
 | 
			
		||||
		if p == "" {
 | 
			
		||||
			log.Fatal("Path must be given when store is loal")
 | 
			
		||||
			log.Fatal("Path must be given when storage is loal")
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		dstStorage, err = storage.NewLocalStorage(p)
 | 
			
		||||
	case "minio":
 | 
			
		||||
	case setting.MinioStorageType:
 | 
			
		||||
		dstStorage, err = storage.NewMinioStorage(
 | 
			
		||||
			context.Background(),
 | 
			
		||||
			ctx.String("minio-endpoint"),
 | 
			
		||||
@@ -132,14 +133,14 @@ func runMigrateStorage(ctx *cli.Context) error {
 | 
			
		||||
			ctx.Bool("minio-use-ssl"),
 | 
			
		||||
		)
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("Unsupported attachments store type: %s", ctx.String("store"))
 | 
			
		||||
		return fmt.Errorf("Unsupported attachments storage type: %s", ctx.String("storage"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tp := ctx.String("type")
 | 
			
		||||
	tp := strings.ToLower(ctx.String("type"))
 | 
			
		||||
	switch tp {
 | 
			
		||||
	case "attachments":
 | 
			
		||||
		if err := migrateAttachments(dstStorage); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -778,25 +778,25 @@ MAX_SIZE = 4
 | 
			
		||||
MAX_FILES = 5
 | 
			
		||||
; Storage type for attachments, `local` for local disk or `minio` for s3 compatible
 | 
			
		||||
; object storage service, default is `local`.
 | 
			
		||||
STORE_TYPE = local
 | 
			
		||||
STORAGE_TYPE = local
 | 
			
		||||
; Allows the storage driver to redirect to authenticated URLs to serve files directly
 | 
			
		||||
; Currently, only `minio` is supported.
 | 
			
		||||
SERVE_DIRECT = false
 | 
			
		||||
; Path for attachments. Defaults to `data/attachments` only available when STORE_TYPE is `local`
 | 
			
		||||
; Path for attachments. Defaults to `data/attachments` only available when STORAGE_TYPE is `local`
 | 
			
		||||
PATH = data/attachments
 | 
			
		||||
; Minio endpoint to connect only available when STORE_TYPE is `minio`
 | 
			
		||||
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_ENDPOINT = localhost:9000
 | 
			
		||||
; Minio accessKeyID to connect only available when STORE_TYPE is `minio`
 | 
			
		||||
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_ACCESS_KEY_ID =
 | 
			
		||||
; Minio secretAccessKey to connect only available when STORE_TYPE is `minio`
 | 
			
		||||
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_SECRET_ACCESS_KEY =
 | 
			
		||||
; Minio bucket to store the attachments only available when STORE_TYPE is `minio`
 | 
			
		||||
; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_BUCKET = gitea
 | 
			
		||||
; Minio location to create bucket only available when STORE_TYPE is `minio`
 | 
			
		||||
; Minio location to create bucket only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_LOCATION = us-east-1
 | 
			
		||||
; Minio base path on the bucket only available when STORE_TYPE is `minio`
 | 
			
		||||
; Minio base path on the bucket only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_BASE_PATH = attachments/
 | 
			
		||||
; Minio enabled ssl only available when STORE_TYPE is `minio`
 | 
			
		||||
; Minio enabled ssl only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_USE_SSL = false
 | 
			
		||||
 | 
			
		||||
[time]
 | 
			
		||||
@@ -1161,3 +1161,28 @@ QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0"
 | 
			
		||||
MAX_ATTEMPTS = 3
 | 
			
		||||
; Backoff time per http/https request retry (seconds)
 | 
			
		||||
RETRY_BACKOFF = 3
 | 
			
		||||
 | 
			
		||||
; default storage for attachments, lfs and avatars
 | 
			
		||||
[storage]
 | 
			
		||||
; storage type
 | 
			
		||||
STORAGE_TYPE = local
 | 
			
		||||
 | 
			
		||||
; lfs storage will override storage
 | 
			
		||||
[lfs]
 | 
			
		||||
STORAGE_TYPE = local
 | 
			
		||||
 | 
			
		||||
; customize storage
 | 
			
		||||
;[storage.my_minio]
 | 
			
		||||
;STORAGE_TYPE = minio
 | 
			
		||||
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
;MINIO_ENDPOINT = localhost:9000
 | 
			
		||||
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
;MINIO_ACCESS_KEY_ID =
 | 
			
		||||
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
;MINIO_SECRET_ACCESS_KEY =
 | 
			
		||||
; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
 | 
			
		||||
;MINIO_BUCKET = gitea
 | 
			
		||||
; Minio location to create bucket only available when STORAGE_TYPE is `minio`
 | 
			
		||||
;MINIO_LOCATION = us-east-1
 | 
			
		||||
; Minio enabled ssl only available when STORAGE_TYPE is `minio`
 | 
			
		||||
;MINIO_USE_SSL = false
 | 
			
		||||
 
 | 
			
		||||
@@ -220,16 +220,6 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
			
		||||
- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login\].
 | 
			
		||||
 | 
			
		||||
- `LFS_START_SERVER`: **false**: Enables git-lfs support.
 | 
			
		||||
- `LFS_STORE_TYPE`: **local**: Storage type for lfs, `local` for local disk or `minio` for s3 compatible object storage service.
 | 
			
		||||
- `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.
 | 
			
		||||
- `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files, only available when `LFS_STORE_TYPE` is `local`.
 | 
			
		||||
- `LFS_MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `LFS_STORE_TYPE` is `minio`
 | 
			
		||||
- `LFS_MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `LFS_STORE_TYPE` is `minio`
 | 
			
		||||
- `LFS_MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `LFS_STORE_TYPE is` `minio`
 | 
			
		||||
- `LFS_MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `LFS_STORE_TYPE` is `minio`
 | 
			
		||||
- `LFS_MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `LFS_STORE_TYPE` is `minio`
 | 
			
		||||
- `LFS_MINIO_BASE_PATH`: **lfs/**: Minio base path on the bucket only available when `LFS_STORE_TYPE` is `minio`
 | 
			
		||||
- `LFS_MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `LFS_STORE_TYPE` is `minio`
 | 
			
		||||
- `LFS_JWT_SECRET`: **\<empty\>**: LFS authentication secret, change this a unique string.
 | 
			
		||||
- `LFS_HTTP_AUTH_EXPIRY`: **20m**: LFS authentication validity period in time.Duration, pushes taking longer than this may fail.
 | 
			
		||||
- `LFS_MAX_FILE_SIZE`: **0**: Maximum allowed LFS file size in bytes (Set to 0 for no limit).
 | 
			
		||||
@@ -501,16 +491,16 @@ relation to port exhaustion.
 | 
			
		||||
   Use `*/*` for all types.
 | 
			
		||||
- `MAX_SIZE`: **4**: Maximum size (MB).
 | 
			
		||||
- `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
 | 
			
		||||
- `STORE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local`.
 | 
			
		||||
- `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
 | 
			
		||||
- `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.
 | 
			
		||||
- `PATH`: **data/attachments**: Path to store attachments only available when STORE_TYPE is `local`
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when STORE_TYPE is `minio`
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORE_TYPE is `minio`
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when STORE_TYPE is `minio`
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the attachments only available when STORE_TYPE is `minio`
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when STORE_TYPE is `minio`
 | 
			
		||||
- `MINIO_BASE_PATH`: **attachments/**: Minio base path on the bucket only available when STORE_TYPE is `minio`
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when STORE_TYPE is `minio`
 | 
			
		||||
- `PATH`: **data/attachments**: Path to store attachments only available when STORAGE_TYPE is `local`
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when STORAGE_TYPE is `minio`
 | 
			
		||||
- `MINIO_BASE_PATH`: **attachments/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio`
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when STORAGE_TYPE is `minio`
 | 
			
		||||
 | 
			
		||||
## Log (`log`)
 | 
			
		||||
 | 
			
		||||
@@ -714,6 +704,56 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
 | 
			
		||||
- `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations.
 | 
			
		||||
- `RETRY_BACKOFF`: **3**: Backoff time per http/https request retry (seconds)
 | 
			
		||||
 | 
			
		||||
## LFS (`lfs`)
 | 
			
		||||
 | 
			
		||||
Storage configuration for lfs data. It will be derived from default `[storage]` or
 | 
			
		||||
`[storage.xxx]` when set `STORAGE_TYPE` to `xxx`. When derived, the default of `PATH` 
 | 
			
		||||
is `data/lfs` and the default of `MINIO_BASE_PATH` is `lfs/`.
 | 
			
		||||
 | 
			
		||||
- `STORAGE_TYPE`: **local**: Storage type for lfs, `local` for local disk or `minio` for s3 compatible object storage service or other name defined with `[storage.xxx]`
 | 
			
		||||
- `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.
 | 
			
		||||
- `CONTENT_PATH`: **./data/lfs**: Where to store LFS files, only available when `STORAGE_TYPE` is `local`.
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_BASE_PATH`: **lfs/**: Minio base path on the bucket only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
 | 
			
		||||
## Storage (`storage`)
 | 
			
		||||
 | 
			
		||||
Default storage configuration for attachments, lfs, avatars and etc.
 | 
			
		||||
 | 
			
		||||
- `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.
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the data only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
 | 
			
		||||
 | 
			
		||||
And you can also define a customize storage like below:
 | 
			
		||||
 | 
			
		||||
```ini
 | 
			
		||||
[storage.my_minio]
 | 
			
		||||
STORAGE_TYPE = minio
 | 
			
		||||
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_ENDPOINT = localhost:9000
 | 
			
		||||
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_ACCESS_KEY_ID =
 | 
			
		||||
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_SECRET_ACCESS_KEY =
 | 
			
		||||
; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_BUCKET = gitea
 | 
			
		||||
; Minio location to create bucket only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_LOCATION = us-east-1
 | 
			
		||||
; Minio enabled ssl only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_USE_SSL = false
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
And used by `[attachment]`, `[lfs]` and etc. as `STORAGE_TYPE`.
 | 
			
		||||
 | 
			
		||||
## Other (`other`)
 | 
			
		||||
 | 
			
		||||
- `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer.
 | 
			
		||||
 
 | 
			
		||||
@@ -72,16 +72,6 @@ menu:
 | 
			
		||||
- `LANDING_PAGE`: 未登录用户的默认页面,可选 `home` 或 `explore`。
 | 
			
		||||
 | 
			
		||||
- `LFS_START_SERVER`: 是否启用 git-lfs 支持. 可以为 `true` 或 `false`, 默认是 `false`。
 | 
			
		||||
- `LFS_STORE_TYPE`: **local**: LFS 的存储类型,`local` 将存储到磁盘,`minio` 将存储到 s3 兼容的对象服务。
 | 
			
		||||
- `LFS_SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。
 | 
			
		||||
- `LFS_CONTENT_PATH`: 存放 lfs 命令上传的文件的地方,默认是 `data/lfs`。
 | 
			
		||||
- `LFS_MINIO_ENDPOINT`: **localhost:9000**: Minio 地址,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `LFS_MINIO_ACCESS_KEY_ID`: Minio accessKeyID,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `LFS_MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `LFS_MINIO_BUCKET`: **gitea**: Minio bucket,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `LFS_MINIO_LOCATION`: **us-east-1**: Minio location ,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `LFS_MINIO_BASE_PATH`: **lfs/**: Minio base path ,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `LFS_MINIO_USE_SSL`: **false**: Minio 是否启用 ssl ,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `LFS_JWT_SECRET`: LFS 认证密钥,改成自己的。
 | 
			
		||||
 | 
			
		||||
## Database (`database`)
 | 
			
		||||
@@ -198,15 +188,15 @@ menu:
 | 
			
		||||
- `ALLOWED_TYPES`: 允许上传的附件类型。比如:`image/jpeg|image/png`,用 `*/*` 表示允许任何类型。
 | 
			
		||||
- `MAX_SIZE`: 附件最大限制,单位 MB,比如: `4`。
 | 
			
		||||
- `MAX_FILES`: 一次最多上传的附件数量,比如: `5`。
 | 
			
		||||
- `STORE_TYPE`: **local**: 附件存储类型,`local` 将存储到本地文件夹, `minio` 将存储到 s3 兼容的对象存储服务中。
 | 
			
		||||
- `PATH`: **data/attachments**: 附件存储路径,仅当 `STORE_TYPE` 为 `local` 时有效。
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio 终端,仅当 `STORE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID ,仅当 `STORE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the attachments,仅当 `STORE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket,仅当 `STORE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_BASE_PATH`: **attachments/**: Minio base path on the bucket,仅当 `STORE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio enabled ssl,仅当 `STORE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `STORAGE_TYPE`: **local**: 附件存储类型,`local` 将存储到本地文件夹, `minio` 将存储到 s3 兼容的对象存储服务中。
 | 
			
		||||
- `PATH`: **data/attachments**: 附件存储路径,仅当 `STORAGE_TYPE` 为 `local` 时有效。
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio 终端,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID ,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the attachments,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_BASE_PATH`: **attachments/**: Minio base path on the bucket,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio enabled ssl,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
 | 
			
		||||
关于 `ALLOWED_TYPES`, 在 (*)unix 系统中可以使用`file -I <filename>` 来快速获得对应的 `MIME type`。
 | 
			
		||||
 | 
			
		||||
@@ -310,6 +300,55 @@ IS_INPUT_FILE = false
 | 
			
		||||
- `MAX_ATTEMPTS`: **3**: 在迁移过程中的 http/https 请求重试次数。
 | 
			
		||||
- `RETRY_BACKOFF`: **3**: 等待下一次重试的时间,单位秒。
 | 
			
		||||
 | 
			
		||||
## LFS (`lfs`)
 | 
			
		||||
 | 
			
		||||
LFS 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配置将从 `[storage]` 继承。如果不为 `local` 或者 `minio` 而为 `xxx`, 则从 `[storage.xxx]` 继承。当继承时, `PATH` 默认为 `data/lfs`,`MINIO_BASE_PATH` 默认为 `lfs/`。
 | 
			
		||||
 | 
			
		||||
- `STORAGE_TYPE`: **local**: LFS 的存储类型,`local` 将存储到磁盘,`minio` 将存储到 s3 兼容的对象服务。
 | 
			
		||||
- `SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。
 | 
			
		||||
- `CONTENT_PATH`: 存放 lfs 命令上传的文件的地方,默认是 `data/lfs`。
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio 地址,仅当 `LFS_STORAGE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID,仅当 `LFS_STORAGE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `LFS_STORAGE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket,仅当 `LFS_STORAGE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location ,仅当 `LFS_STORAGE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `MINIO_BASE_PATH`: **lfs/**: Minio base path ,仅当 `LFS_STORAGE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio 是否启用 ssl ,仅当 `LFS_STORAGE_TYPE` 为 `minio` 时有效。
 | 
			
		||||
 | 
			
		||||
## Storage (`storage`)
 | 
			
		||||
 | 
			
		||||
Attachments, lfs, avatars and etc 的默认存储配置。
 | 
			
		||||
 | 
			
		||||
- `STORAGE_TYPE`: **local**: 附件存储类型,`local` 将存储到本地文件夹, `minio` 将存储到 s3 兼容的对象存储服务中。
 | 
			
		||||
- `SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。
 | 
			
		||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio 终端,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID ,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the attachments,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
- `MINIO_USE_SSL`: **false**: Minio enabled ssl,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
 | 
			
		||||
 | 
			
		||||
你也可以自定义一个存储的名字如下:
 | 
			
		||||
 | 
			
		||||
```ini
 | 
			
		||||
[storage.my_minio]
 | 
			
		||||
STORAGE_TYPE = minio
 | 
			
		||||
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_ENDPOINT = localhost:9000
 | 
			
		||||
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_ACCESS_KEY_ID =
 | 
			
		||||
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_SECRET_ACCESS_KEY =
 | 
			
		||||
; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_BUCKET = gitea
 | 
			
		||||
; Minio location to create bucket only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_LOCATION = us-east-1
 | 
			
		||||
; Minio enabled ssl only available when STORAGE_TYPE is `minio`
 | 
			
		||||
MINIO_USE_SSL = false
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后你在 `[attachment]`, `[lfs]` 等中可以把这个名字用作 `STORAGE_TYPE` 的值。
 | 
			
		||||
 | 
			
		||||
## Other (`other`)
 | 
			
		||||
 | 
			
		||||
- `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,10 @@ func RemoveAllWithNotice(title, path string) {
 | 
			
		||||
// RemoveStorageWithNotice removes a file from the storage and
 | 
			
		||||
// creates a system notice when error occurs.
 | 
			
		||||
func RemoveStorageWithNotice(bucket storage.ObjectStorage, title, path string) {
 | 
			
		||||
	removeStorageWithNotice(x, bucket, title, path)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func removeStorageWithNotice(e Engine, bucket storage.ObjectStorage, title, path string) {
 | 
			
		||||
	if err := bucket.Delete(path); err != nil {
 | 
			
		||||
		desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
 | 
			
		||||
		log.Warn(title+" [%s]: %v", path, err)
 | 
			
		||||
 
 | 
			
		||||
@@ -1748,8 +1748,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		oidPath := filepath.Join(setting.LFS.ContentPath, v.Oid[0:2], v.Oid[2:4], v.Oid[4:len(v.Oid)])
 | 
			
		||||
		removeAllWithNotice(sess, "Delete orphaned LFS file", oidPath)
 | 
			
		||||
		removeStorageWithNotice(sess, storage.LFS, "Delete orphaned LFS file", v.RelativePath())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := sess.Delete(&LFSMetaObject{RepositoryID: repoID}); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -67,9 +67,11 @@ func MainTest(m *testing.M, pathToGiteaRoot string) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fatalTestError("url.Parse: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
	setting.Attachment.Storage.Type = setting.LocalStorageType
 | 
			
		||||
	setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
 | 
			
		||||
 | 
			
		||||
	setting.Attachment.Path = filepath.Join(setting.AppDataPath, "attachments")
 | 
			
		||||
	setting.LFS.ContentPath = filepath.Join(setting.AppDataPath, "lfs")
 | 
			
		||||
	setting.LFS.Storage.Type = setting.LocalStorageType
 | 
			
		||||
	setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs")
 | 
			
		||||
	if err = storage.Init(); err != nil {
 | 
			
		||||
		fatalTestError("storage.Init: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,42 +5,25 @@
 | 
			
		||||
package setting
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// Attachment settings
 | 
			
		||||
	Attachment = struct {
 | 
			
		||||
		StoreType   string
 | 
			
		||||
		Path        string
 | 
			
		||||
		ServeDirect bool
 | 
			
		||||
		Minio       struct {
 | 
			
		||||
			Endpoint        string
 | 
			
		||||
			AccessKeyID     string
 | 
			
		||||
			SecretAccessKey string
 | 
			
		||||
			UseSSL          bool
 | 
			
		||||
			Bucket          string
 | 
			
		||||
			Location        string
 | 
			
		||||
			BasePath        string
 | 
			
		||||
		}
 | 
			
		||||
		Storage
 | 
			
		||||
		AllowedTypes string
 | 
			
		||||
		MaxSize      int64
 | 
			
		||||
		MaxFiles     int
 | 
			
		||||
		Enabled      bool
 | 
			
		||||
	}{
 | 
			
		||||
		StoreType:   "local",
 | 
			
		||||
		Storage: Storage{
 | 
			
		||||
			Type:        LocalStorageType,
 | 
			
		||||
			ServeDirect: false,
 | 
			
		||||
		Minio: struct {
 | 
			
		||||
			Endpoint        string
 | 
			
		||||
			AccessKeyID     string
 | 
			
		||||
			SecretAccessKey string
 | 
			
		||||
			UseSSL          bool
 | 
			
		||||
			Bucket          string
 | 
			
		||||
			Location        string
 | 
			
		||||
			BasePath        string
 | 
			
		||||
		}{},
 | 
			
		||||
		},
 | 
			
		||||
		AllowedTypes: "image/jpeg,image/png,application/zip,application/gzip",
 | 
			
		||||
		MaxSize:      4,
 | 
			
		||||
		MaxFiles:     5,
 | 
			
		||||
@@ -50,22 +33,36 @@ var (
 | 
			
		||||
 | 
			
		||||
func newAttachmentService() {
 | 
			
		||||
	sec := Cfg.Section("attachment")
 | 
			
		||||
	Attachment.StoreType = sec.Key("STORE_TYPE").MustString("local")
 | 
			
		||||
	Attachment.ServeDirect = sec.Key("SERVE_DIRECT").MustBool(false)
 | 
			
		||||
	switch Attachment.StoreType {
 | 
			
		||||
	case "local":
 | 
			
		||||
		Attachment.Path = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments"))
 | 
			
		||||
		if !filepath.IsAbs(Attachment.Path) {
 | 
			
		||||
			Attachment.Path = path.Join(AppWorkPath, Attachment.Path)
 | 
			
		||||
	Attachment.Storage.Type = sec.Key("STORAGE_TYPE").MustString("")
 | 
			
		||||
	if Attachment.Storage.Type == "" {
 | 
			
		||||
		Attachment.Storage.Type = "default"
 | 
			
		||||
	}
 | 
			
		||||
	case "minio":
 | 
			
		||||
		Attachment.Minio.Endpoint = sec.Key("MINIO_ENDPOINT").MustString("localhost:9000")
 | 
			
		||||
		Attachment.Minio.AccessKeyID = sec.Key("MINIO_ACCESS_KEY_ID").MustString("")
 | 
			
		||||
		Attachment.Minio.SecretAccessKey = sec.Key("MINIO_SECRET_ACCESS_KEY").MustString("")
 | 
			
		||||
		Attachment.Minio.Bucket = sec.Key("MINIO_BUCKET").MustString("gitea")
 | 
			
		||||
		Attachment.Minio.Location = sec.Key("MINIO_LOCATION").MustString("us-east-1")
 | 
			
		||||
 | 
			
		||||
	if Attachment.Storage.Type != LocalStorageType && Attachment.Storage.Type != MinioStorageType {
 | 
			
		||||
		storage, ok := storages[Attachment.Storage.Type]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			log.Fatal("Failed to get attachment storage type: %s", Attachment.Storage.Type)
 | 
			
		||||
		}
 | 
			
		||||
		Attachment.Storage = storage
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Override
 | 
			
		||||
	Attachment.ServeDirect = sec.Key("SERVE_DIRECT").MustBool(Attachment.ServeDirect)
 | 
			
		||||
 | 
			
		||||
	switch Attachment.Storage.Type {
 | 
			
		||||
	case LocalStorageType:
 | 
			
		||||
		Attachment.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "attachments"))
 | 
			
		||||
		if !filepath.IsAbs(Attachment.Path) {
 | 
			
		||||
			Attachment.Path = filepath.Join(AppWorkPath, Attachment.Path)
 | 
			
		||||
		}
 | 
			
		||||
	case MinioStorageType:
 | 
			
		||||
		Attachment.Minio.Endpoint = sec.Key("MINIO_ENDPOINT").MustString(Attachment.Minio.Endpoint)
 | 
			
		||||
		Attachment.Minio.AccessKeyID = sec.Key("MINIO_ACCESS_KEY_ID").MustString(Attachment.Minio.AccessKeyID)
 | 
			
		||||
		Attachment.Minio.SecretAccessKey = sec.Key("MINIO_SECRET_ACCESS_KEY").MustString(Attachment.Minio.SecretAccessKey)
 | 
			
		||||
		Attachment.Minio.Bucket = sec.Key("MINIO_BUCKET").MustString(Attachment.Minio.Bucket)
 | 
			
		||||
		Attachment.Minio.Location = sec.Key("MINIO_LOCATION").MustString(Attachment.Minio.Location)
 | 
			
		||||
		Attachment.Minio.UseSSL = sec.Key("MINIO_USE_SSL").MustBool(Attachment.Minio.UseSSL)
 | 
			
		||||
		Attachment.Minio.BasePath = sec.Key("MINIO_BASE_PATH").MustString("attachments/")
 | 
			
		||||
		Attachment.Minio.UseSSL = sec.Key("MINIO_USE_SSL").MustBool(false)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Attachment.AllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png,application/zip,application/gzip"), "|", ",", -1)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,27 +21,14 @@ import (
 | 
			
		||||
// LFS represents the configuration for Git LFS
 | 
			
		||||
var LFS = struct {
 | 
			
		||||
	StartServer     bool          `ini:"LFS_START_SERVER"`
 | 
			
		||||
	ContentPath     string        `ini:"LFS_CONTENT_PATH"`
 | 
			
		||||
	JWTSecretBase64 string        `ini:"LFS_JWT_SECRET"`
 | 
			
		||||
	JWTSecretBytes  []byte        `ini:"-"`
 | 
			
		||||
	HTTPAuthExpiry  time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
 | 
			
		||||
	MaxFileSize     int64         `ini:"LFS_MAX_FILE_SIZE"`
 | 
			
		||||
	LocksPagingNum  int           `ini:"LFS_LOCKS_PAGING_NUM"`
 | 
			
		||||
 | 
			
		||||
	StoreType   string
 | 
			
		||||
	ServeDirect bool
 | 
			
		||||
	Minio       struct {
 | 
			
		||||
		Endpoint        string
 | 
			
		||||
		AccessKeyID     string
 | 
			
		||||
		SecretAccessKey string
 | 
			
		||||
		UseSSL          bool
 | 
			
		||||
		Bucket          string
 | 
			
		||||
		Location        string
 | 
			
		||||
		BasePath        string
 | 
			
		||||
	}
 | 
			
		||||
}{
 | 
			
		||||
	StoreType: "local",
 | 
			
		||||
}
 | 
			
		||||
	Storage
 | 
			
		||||
}{}
 | 
			
		||||
 | 
			
		||||
func newLFSService() {
 | 
			
		||||
	sec := Cfg.Section("server")
 | 
			
		||||
@@ -49,10 +36,41 @@ func newLFSService() {
 | 
			
		||||
		log.Fatal("Failed to map LFS settings: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	LFS.ContentPath = sec.Key("LFS_CONTENT_PATH").MustString(filepath.Join(AppDataPath, "lfs"))
 | 
			
		||||
	if !filepath.IsAbs(LFS.ContentPath) {
 | 
			
		||||
		LFS.ContentPath = filepath.Join(AppWorkPath, LFS.ContentPath)
 | 
			
		||||
	lfsSec := Cfg.Section("lfs")
 | 
			
		||||
	LFS.Storage.Type = lfsSec.Key("STORAGE_TYPE").MustString("")
 | 
			
		||||
	if LFS.Storage.Type == "" {
 | 
			
		||||
		LFS.Storage.Type = "default"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if LFS.Storage.Type != LocalStorageType && LFS.Storage.Type != MinioStorageType {
 | 
			
		||||
		storage, ok := storages[LFS.Storage.Type]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			log.Fatal("Failed to get lfs storage type: %s", LFS.Storage.Type)
 | 
			
		||||
		}
 | 
			
		||||
		LFS.Storage = storage
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Override
 | 
			
		||||
	LFS.ServeDirect = lfsSec.Key("SERVE_DIRECT").MustBool(LFS.ServeDirect)
 | 
			
		||||
	switch LFS.Storage.Type {
 | 
			
		||||
	case LocalStorageType:
 | 
			
		||||
		// keep compatible
 | 
			
		||||
		LFS.Path = sec.Key("LFS_CONTENT_PATH").MustString(filepath.Join(AppDataPath, "lfs"))
 | 
			
		||||
		LFS.Path = lfsSec.Key("PATH").MustString(LFS.Path)
 | 
			
		||||
		if !filepath.IsAbs(LFS.Path) {
 | 
			
		||||
			LFS.Path = filepath.Join(AppWorkPath, LFS.Path)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	case MinioStorageType:
 | 
			
		||||
		LFS.Minio.Endpoint = lfsSec.Key("MINIO_ENDPOINT").MustString(LFS.Minio.Endpoint)
 | 
			
		||||
		LFS.Minio.AccessKeyID = lfsSec.Key("MINIO_ACCESS_KEY_ID").MustString(LFS.Minio.AccessKeyID)
 | 
			
		||||
		LFS.Minio.SecretAccessKey = lfsSec.Key("MINIO_SECRET_ACCESS_KEY").MustString(LFS.Minio.SecretAccessKey)
 | 
			
		||||
		LFS.Minio.Bucket = lfsSec.Key("MINIO_BUCKET").MustString(LFS.Minio.Bucket)
 | 
			
		||||
		LFS.Minio.Location = lfsSec.Key("MINIO_LOCATION").MustString(LFS.Minio.Location)
 | 
			
		||||
		LFS.Minio.UseSSL = lfsSec.Key("MINIO_USE_SSL").MustBool(LFS.Minio.UseSSL)
 | 
			
		||||
		LFS.Minio.BasePath = lfsSec.Key("MINIO_BASE_PATH").MustString("lfs/")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if LFS.LocksPagingNum == 0 {
 | 
			
		||||
		LFS.LocksPagingNum = 50
 | 
			
		||||
	}
 | 
			
		||||
@@ -92,14 +110,6 @@ func newLFSService() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ensureLFSDirectory() {
 | 
			
		||||
	if LFS.StartServer {
 | 
			
		||||
		if err := os.MkdirAll(LFS.ContentPath, 0700); err != nil {
 | 
			
		||||
			log.Fatal("Failed to create '%s': %v", LFS.ContentPath, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckLFSVersion will check lfs version, if not satisfied, then disable it.
 | 
			
		||||
func CheckLFSVersion() {
 | 
			
		||||
	if LFS.StartServer {
 | 
			
		||||
 
 | 
			
		||||
@@ -691,8 +691,6 @@ func NewContext() {
 | 
			
		||||
	SSH.CreateAuthorizedKeysFile = sec.Key("SSH_CREATE_AUTHORIZED_KEYS_FILE").MustBool(true)
 | 
			
		||||
	SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false)
 | 
			
		||||
 | 
			
		||||
	newLFSService()
 | 
			
		||||
 | 
			
		||||
	if err = Cfg.Section("oauth2").MapTo(&OAuth2); err != nil {
 | 
			
		||||
		log.Fatal("Failed to OAuth2 settings: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -761,7 +759,9 @@ func NewContext() {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newStorageService()
 | 
			
		||||
	newAttachmentService()
 | 
			
		||||
	newLFSService()
 | 
			
		||||
 | 
			
		||||
	timeFormatKey := Cfg.Section("time").Key("FORMAT").MustString("")
 | 
			
		||||
	if timeFormatKey != "" {
 | 
			
		||||
@@ -1017,7 +1017,6 @@ func NewServices() {
 | 
			
		||||
	InitDBConfig()
 | 
			
		||||
	newService()
 | 
			
		||||
	NewLogServices(false)
 | 
			
		||||
	ensureLFSDirectory()
 | 
			
		||||
	newCacheService()
 | 
			
		||||
	newSessionService()
 | 
			
		||||
	newCORSService()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								modules/setting/storage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								modules/setting/storage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
// Copyright 2020 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 setting
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	ini "gopkg.in/ini.v1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// enumerate all storage types
 | 
			
		||||
const (
 | 
			
		||||
	LocalStorageType = "local"
 | 
			
		||||
	MinioStorageType = "minio"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Storage represents configuration of storages
 | 
			
		||||
type Storage struct {
 | 
			
		||||
	Type        string
 | 
			
		||||
	Path        string
 | 
			
		||||
	ServeDirect bool
 | 
			
		||||
	Minio       struct {
 | 
			
		||||
		Endpoint        string
 | 
			
		||||
		AccessKeyID     string
 | 
			
		||||
		SecretAccessKey string
 | 
			
		||||
		UseSSL          bool
 | 
			
		||||
		Bucket          string
 | 
			
		||||
		Location        string
 | 
			
		||||
		BasePath        string
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	storages = make(map[string]Storage)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getStorage(sec *ini.Section) Storage {
 | 
			
		||||
	var storage Storage
 | 
			
		||||
	storage.Type = sec.Key("STORAGE_TYPE").MustString(LocalStorageType)
 | 
			
		||||
	storage.ServeDirect = sec.Key("SERVE_DIRECT").MustBool(false)
 | 
			
		||||
	switch storage.Type {
 | 
			
		||||
	case LocalStorageType:
 | 
			
		||||
	case MinioStorageType:
 | 
			
		||||
		storage.Minio.Endpoint = sec.Key("MINIO_ENDPOINT").MustString("localhost:9000")
 | 
			
		||||
		storage.Minio.AccessKeyID = sec.Key("MINIO_ACCESS_KEY_ID").MustString("")
 | 
			
		||||
		storage.Minio.SecretAccessKey = sec.Key("MINIO_SECRET_ACCESS_KEY").MustString("")
 | 
			
		||||
		storage.Minio.Bucket = sec.Key("MINIO_BUCKET").MustString("gitea")
 | 
			
		||||
		storage.Minio.Location = sec.Key("MINIO_LOCATION").MustString("us-east-1")
 | 
			
		||||
		storage.Minio.UseSSL = sec.Key("MINIO_USE_SSL").MustBool(false)
 | 
			
		||||
	}
 | 
			
		||||
	return storage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newStorageService() {
 | 
			
		||||
	sec := Cfg.Section("storage")
 | 
			
		||||
	storages["default"] = getStorage(sec)
 | 
			
		||||
 | 
			
		||||
	for _, sec := range Cfg.Section("storage").ChildSections() {
 | 
			
		||||
		name := strings.TrimPrefix(sec.Name(), "storage.")
 | 
			
		||||
		if name == "default" || name == LocalStorageType || name == MinioStorageType {
 | 
			
		||||
			log.Error("storage name %s is system reserved!", name)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		storages[name] = getStorage(sec)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -59,7 +59,7 @@ func (l *LocalStorage) Save(path string, r io.Reader) (int64, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stat returns the info of the file
 | 
			
		||||
func (l *LocalStorage) Stat(path string) (ObjectInfo, error) {
 | 
			
		||||
func (l *LocalStorage) Stat(path string) (os.FileInfo, error) {
 | 
			
		||||
	return os.Stat(filepath.Join(l.dir, path))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -73,3 +73,28 @@ func (l *LocalStorage) Delete(path string) error {
 | 
			
		||||
func (l *LocalStorage) URL(path, name string) (*url.URL, error) {
 | 
			
		||||
	return nil, ErrURLNotSupported
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IterateObjects iterates across the objects in the local storage
 | 
			
		||||
func (l *LocalStorage) IterateObjects(fn func(path string, obj Object) error) error {
 | 
			
		||||
	return filepath.Walk(l.dir, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if path == l.dir {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		if info.IsDir() {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		relPath, err := filepath.Rel(l.dir, path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		obj, err := os.Open(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer obj.Close()
 | 
			
		||||
		return fn(relPath, obj)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,19 @@ var (
 | 
			
		||||
	quoteEscaper               = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type minioObject struct {
 | 
			
		||||
	*minio.Object
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *minioObject) Stat() (os.FileInfo, error) {
 | 
			
		||||
	oi, err := m.Object.Stat()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &minioFileInfo{oi}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MinioStorage returns a minio bucket storage
 | 
			
		||||
type MinioStorage struct {
 | 
			
		||||
	ctx      context.Context
 | 
			
		||||
@@ -69,7 +82,7 @@ func (m *MinioStorage) Open(path string) (Object, error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return object, nil
 | 
			
		||||
	return &minioObject{object}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save save a file to minio
 | 
			
		||||
@@ -104,8 +117,20 @@ func (m minioFileInfo) ModTime() time.Time {
 | 
			
		||||
	return m.LastModified
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m minioFileInfo) IsDir() bool {
 | 
			
		||||
	return strings.HasSuffix(m.ObjectInfo.Key, "/")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m minioFileInfo) Mode() os.FileMode {
 | 
			
		||||
	return os.ModePerm
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m minioFileInfo) Sys() interface{} {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stat returns the stat information of the object
 | 
			
		||||
func (m *MinioStorage) Stat(path string) (ObjectInfo, error) {
 | 
			
		||||
func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
 | 
			
		||||
	info, err := m.client.StatObject(
 | 
			
		||||
		m.ctx,
 | 
			
		||||
		m.bucket,
 | 
			
		||||
@@ -135,3 +160,26 @@ func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
 | 
			
		||||
	reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
 | 
			
		||||
	return m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IterateObjects iterates across the objects in the miniostorage
 | 
			
		||||
func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) error {
 | 
			
		||||
	var opts = minio.GetObjectOptions{}
 | 
			
		||||
	lobjectCtx, cancel := context.WithCancel(m.ctx)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{
 | 
			
		||||
		Prefix:    m.basePath,
 | 
			
		||||
		Recursive: true,
 | 
			
		||||
	}) {
 | 
			
		||||
		object, err := m.client.GetObject(lobjectCtx, m.bucket, mObjInfo.Key, opts)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
 | 
			
		||||
			defer object.Close()
 | 
			
		||||
			return fn(strings.TrimPrefix(m.basePath, mObjInfo.Key), &minioObject{object})
 | 
			
		||||
		}(object, fn); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"time"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
@@ -18,28 +18,25 @@ import (
 | 
			
		||||
var (
 | 
			
		||||
	// ErrURLNotSupported represents url is not supported
 | 
			
		||||
	ErrURLNotSupported = errors.New("url method not supported")
 | 
			
		||||
	// ErrIterateObjectsNotSupported represents IterateObjects not supported
 | 
			
		||||
	ErrIterateObjectsNotSupported = errors.New("iterateObjects method not supported")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Object represents the object on the storage
 | 
			
		||||
type Object interface {
 | 
			
		||||
	io.ReadCloser
 | 
			
		||||
	io.Seeker
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ObjectInfo represents the object info on the storage
 | 
			
		||||
type ObjectInfo interface {
 | 
			
		||||
	Name() string
 | 
			
		||||
	Size() int64
 | 
			
		||||
	ModTime() time.Time
 | 
			
		||||
	Stat() (os.FileInfo, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ObjectStorage represents an object storage to handle a bucket and files
 | 
			
		||||
type ObjectStorage interface {
 | 
			
		||||
	Open(path string) (Object, error)
 | 
			
		||||
	Save(path string, r io.Reader) (int64, error)
 | 
			
		||||
	Stat(path string) (ObjectInfo, error)
 | 
			
		||||
	Stat(path string) (os.FileInfo, error)
 | 
			
		||||
	Delete(path string) error
 | 
			
		||||
	URL(path, name string) (*url.URL, error)
 | 
			
		||||
	IterateObjects(func(path string, obj Object) error) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Copy copys a file from source ObjectStorage to dest ObjectStorage
 | 
			
		||||
@@ -70,14 +67,15 @@ func Init() error {
 | 
			
		||||
	return initLFS()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initAttachments() error {
 | 
			
		||||
func initStorage(storageCfg setting.Storage) (ObjectStorage, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	switch setting.Attachment.StoreType {
 | 
			
		||||
	case "local":
 | 
			
		||||
		Attachments, err = NewLocalStorage(setting.Attachment.Path)
 | 
			
		||||
	case "minio":
 | 
			
		||||
		minio := setting.Attachment.Minio
 | 
			
		||||
		Attachments, err = NewMinioStorage(
 | 
			
		||||
	var s ObjectStorage
 | 
			
		||||
	switch storageCfg.Type {
 | 
			
		||||
	case setting.LocalStorageType:
 | 
			
		||||
		s, err = NewLocalStorage(storageCfg.Path)
 | 
			
		||||
	case setting.MinioStorageType:
 | 
			
		||||
		minio := storageCfg.Minio
 | 
			
		||||
		s, err = NewMinioStorage(
 | 
			
		||||
			context.Background(),
 | 
			
		||||
			minio.Endpoint,
 | 
			
		||||
			minio.AccessKeyID,
 | 
			
		||||
@@ -88,40 +86,22 @@ func initAttachments() error {
 | 
			
		||||
			minio.UseSSL,
 | 
			
		||||
		)
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("Unsupported attachment store type: %s", setting.Attachment.StoreType)
 | 
			
		||||
		return nil, fmt.Errorf("Unsupported attachment store type: %s", storageCfg.Type)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initLFS() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	switch setting.LFS.StoreType {
 | 
			
		||||
	case "local":
 | 
			
		||||
		LFS, err = NewLocalStorage(setting.LFS.ContentPath)
 | 
			
		||||
	case "minio":
 | 
			
		||||
		minio := setting.LFS.Minio
 | 
			
		||||
		LFS, err = NewMinioStorage(
 | 
			
		||||
			context.Background(),
 | 
			
		||||
			minio.Endpoint,
 | 
			
		||||
			minio.AccessKeyID,
 | 
			
		||||
			minio.SecretAccessKey,
 | 
			
		||||
			minio.Bucket,
 | 
			
		||||
			minio.Location,
 | 
			
		||||
			minio.BasePath,
 | 
			
		||||
			minio.UseSSL,
 | 
			
		||||
		)
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("Unsupported LFS store type: %s", setting.LFS.StoreType)
 | 
			
		||||
func initAttachments() (err error) {
 | 
			
		||||
	Attachments, err = initStorage(setting.Attachment.Storage)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
func initLFS() (err error) {
 | 
			
		||||
	LFS, err = initStorage(setting.LFS.Storage)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ func Install(ctx *context.Context) {
 | 
			
		||||
	// Application general settings
 | 
			
		||||
	form.AppName = setting.AppName
 | 
			
		||||
	form.RepoRootPath = setting.RepoRootPath
 | 
			
		||||
	form.LFSRootPath = setting.LFS.ContentPath
 | 
			
		||||
	form.LFSRootPath = setting.LFS.Path
 | 
			
		||||
 | 
			
		||||
	// Note(unknown): it's hard for Windows users change a running user,
 | 
			
		||||
	// 	so just use current one if config says default.
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import (
 | 
			
		||||
 | 
			
		||||
func renderAttachmentSettings(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
 | 
			
		||||
	ctx.Data["AttachmentStoreType"] = setting.Attachment.StoreType
 | 
			
		||||
	ctx.Data["AttachmentStoreType"] = setting.Attachment.Storage.Type
 | 
			
		||||
	ctx.Data["AttachmentAllowedTypes"] = setting.Attachment.AllowedTypes
 | 
			
		||||
	ctx.Data["AttachmentMaxSize"] = setting.Attachment.MaxSize
 | 
			
		||||
	ctx.Data["AttachmentMaxFiles"] = setting.Attachment.MaxFiles
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -29,7 +28,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	gogit "github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
@@ -355,8 +353,8 @@ func LFSDelete(ctx *context.Context) {
 | 
			
		||||
	// 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 = util.Remove(filepath.Join(setting.LFS.ContentPath, oidPath))
 | 
			
		||||
		oidPath := path.Join(oid[0:2], oid[2:4], oid[4:])
 | 
			
		||||
		err = storage.LFS.Delete(oidPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("LFSDelete", err)
 | 
			
		||||
			return
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user