91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
// Package local 提供基于本地文件系统的 Storage 实现。
|
|
package local
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"portal-mini-go/internal/pkg/errors"
|
|
)
|
|
|
|
// Local 是基于本地磁盘的存储实现。
|
|
type Local struct {
|
|
root string
|
|
baseURL string
|
|
}
|
|
|
|
// New 构造本地存储,root 为落盘根目录,baseURL 为对外访问基础路径(不含末尾斜杠)。
|
|
func New(root, baseURL string) *Local {
|
|
return &Local{root: root, baseURL: strings.TrimRight(baseURL, "/")}
|
|
}
|
|
|
|
// Put 将内容写入 root/key,自动创建父目录。
|
|
func (l *Local) Put(_ context.Context, key string, r io.Reader, _ int64) error {
|
|
p, err := l.safePath(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
return errors.Wrap(50000, "mkdir upload dir", err)
|
|
}
|
|
f, err := os.Create(p)
|
|
if err != nil {
|
|
return errors.Wrap(50000, "create upload file", err)
|
|
}
|
|
defer f.Close()
|
|
if _, err := io.Copy(f, r); err != nil {
|
|
return errors.Wrap(50000, "write upload file", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get 打开文件并返回读取流。
|
|
func (l *Local) Get(_ context.Context, key string) (io.ReadCloser, error) {
|
|
p, err := l.safePath(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
return nil, errors.Wrap(50000, "open upload file", err)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// Delete 删除文件;不存在视为成功。
|
|
func (l *Local) Delete(_ context.Context, key string) error {
|
|
p, err := l.safePath(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
|
|
return errors.Wrap(50000, "delete upload file", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// URL 返回 /baseURL/key 形式的访问地址。
|
|
func (l *Local) URL(key string) string {
|
|
return l.baseURL + "/" + strings.TrimLeft(key, "/")
|
|
}
|
|
|
|
// safePath 将 key 解析为 root 内的绝对路径,并禁止任何目录穿越。
|
|
func (l *Local) safePath(key string) (string, error) {
|
|
clean := filepath.Clean("/" + key) // 以 / 开头,消除 . 与 ..
|
|
if strings.Contains(clean, "..") {
|
|
return "", errors.New(40000, "非法的文件 key")
|
|
}
|
|
p := filepath.Join(l.root, clean)
|
|
// 二次校验,确保最终路径没有逃出 root。
|
|
if rel, err := filepath.Rel(l.root, p); err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "", errors.New(40000, "非法的文件 key")
|
|
}
|
|
return p, nil
|
|
}
|