mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
9a18bea324
Move storage backend configuration from startup YAML to system_config-backed runtime configuration. Add local, S3-compatible, R2, MinIO, OSS, and WebDAV backend support. Add a storage migration async task using the existing task dispatch framework. Migration target config is carried in task payload, and maintenance mode is derived from task execution state. Split upload file management and storage operations, add the admin storage configuration tab, and update migrations and Swagger docs.
121 lines
2.8 KiB
Go
121 lines
2.8 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type localBackend struct {
|
|
root string
|
|
}
|
|
|
|
func newLocalBackend(cfg LocalConfig) (*localBackend, error) {
|
|
root := filepath.Clean(cfg.Root)
|
|
if root == "" {
|
|
return nil, fmt.Errorf("local root is required")
|
|
}
|
|
return &localBackend{root: root}, nil
|
|
}
|
|
|
|
func (b *localBackend) Put(_ context.Context, key string, body io.Reader, _ int64, _ string) (string, error) {
|
|
path, err := b.path(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), storageDirPerm); err != nil {
|
|
return "", err
|
|
}
|
|
file, err := os.OpenFile( //nolint:gosec // path is constrained to the configured storage root.
|
|
path,
|
|
os.O_CREATE|os.O_TRUNC|os.O_WRONLY,
|
|
storageFilePerm,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := io.Copy(file, body); err != nil {
|
|
_ = file.Close()
|
|
_ = os.Remove(path)
|
|
return "", err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
_ = os.Remove(path)
|
|
return "", err
|
|
}
|
|
return filepath.ToSlash(key), nil
|
|
}
|
|
|
|
func (b *localBackend) Get(_ context.Context, key string) (*Object, error) {
|
|
path, err := b.path(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, err := os.Open(path) //nolint:gosec // path is constrained to the configured storage root.
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return nil, err
|
|
}
|
|
contentType := mime.TypeByExtension(filepath.Ext(path))
|
|
if contentType == "" {
|
|
contentType = defaultContentType
|
|
}
|
|
return &Object{Body: file, ContentLength: info.Size(), ContentType: contentType}, nil
|
|
}
|
|
|
|
func (b *localBackend) Delete(_ context.Context, key string) error {
|
|
path, err := b.path(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Remove(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (b *localBackend) Test(_ context.Context) error {
|
|
return os.MkdirAll(b.root, storageDirPerm)
|
|
}
|
|
|
|
func (b *localBackend) path(key string) (string, error) {
|
|
if filepath.IsAbs(key) {
|
|
cleanPath := filepath.Clean(key)
|
|
absRoot, err := filepath.Abs(b.root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
absPath, err := filepath.Abs(cleanPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel, err := filepath.Rel(absRoot, absPath)
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
return "", fmt.Errorf("storage key escapes local root")
|
|
}
|
|
return cleanPath, nil
|
|
}
|
|
cleanKey := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(key, "/")))
|
|
if cleanKey == "." || cleanKey == "" || strings.HasPrefix(cleanKey, "..") {
|
|
return "", fmt.Errorf("invalid local storage key %q", key)
|
|
}
|
|
path := filepath.Join(b.root, cleanKey)
|
|
rel, err := filepath.Rel(b.root, path)
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
return "", fmt.Errorf("storage key escapes local root")
|
|
}
|
|
return path, nil
|
|
}
|