Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
105
internal/app/files/blobfs.go
Normal file
105
internal/app/files/blobfs.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// BlobBackend 是 blob 字节内容的存储后端。第一阶段只有本地磁盘实现。
|
||||
// 内容寻址:Put 返回的 objectKey 是内容 sha256,相同内容自动去重。
|
||||
type BlobBackend interface {
|
||||
Name() string
|
||||
Put(ctx context.Context, data []byte) (objectKey string, err error)
|
||||
Get(ctx context.Context, objectKey string) ([]byte, error)
|
||||
// GetRange 只读 [offset, offset+limit) 段并返回该段字节与文件总大小(limit<=0 读到末尾),
|
||||
// 避免大文件每个 chunk 都整文件读入内存(getFile 按 chunk 多次请求 ⇒ 否则 O(N²) 放大)。
|
||||
GetRange(ctx context.Context, objectKey string, offset, limit int64) (data []byte, total int64, err error)
|
||||
}
|
||||
|
||||
// LocalFS 把 blob 字节存到本地磁盘根目录下,路径按内容 hash 两级 fanout。
|
||||
type LocalFS struct {
|
||||
root string
|
||||
}
|
||||
|
||||
// NewLocalFS 创建本地磁盘 blob backend,确保根目录存在。
|
||||
func NewLocalFS(root string) (*LocalFS, error) {
|
||||
if root == "" {
|
||||
return nil, fmt.Errorf("blob root dir is empty")
|
||||
}
|
||||
if err := os.MkdirAll(root, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create blob root %q: %w", root, err)
|
||||
}
|
||||
return &LocalFS{root: root}, nil
|
||||
}
|
||||
|
||||
// Name 返回后端标识,与 file_blobs.backend 一致。
|
||||
func (l *LocalFS) Name() string { return "localfs" }
|
||||
|
||||
func (l *LocalFS) pathFor(objectKey string) string {
|
||||
if len(objectKey) < 4 {
|
||||
return filepath.Join(l.root, "_", objectKey)
|
||||
}
|
||||
return filepath.Join(l.root, objectKey[:2], objectKey[2:4], objectKey)
|
||||
}
|
||||
|
||||
// Put 写入内容并返回 sha256 hex 作为 objectKey;同内容已存在则跳过写入(去重)。
|
||||
func (l *LocalFS) Put(_ context.Context, data []byte) (string, error) {
|
||||
sum := sha256.Sum256(data)
|
||||
key := hex.EncodeToString(sum[:])
|
||||
path := l.pathFor(key)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return "", fmt.Errorf("create blob dir: %w", err)
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
||||
return "", fmt.Errorf("write blob: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return "", fmt.Errorf("commit blob: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Get 读取 objectKey 对应的全部字节。
|
||||
func (l *LocalFS) Get(_ context.Context, objectKey string) ([]byte, error) {
|
||||
return os.ReadFile(l.pathFor(objectKey))
|
||||
}
|
||||
|
||||
// GetRange 用 ReadAt 只读 [offset, offset+limit) 段,total 取自文件大小;
|
||||
// n 受 total 约束,故即便客户端传超大 limit 也只分配文件实际大小,不会按客户端巨值分配。
|
||||
func (l *LocalFS) GetRange(_ context.Context, objectKey string, offset, limit int64) ([]byte, int64, error) {
|
||||
f, err := os.Open(l.pathFor(objectKey))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total := info.Size()
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if offset >= total {
|
||||
return []byte{}, total, nil
|
||||
}
|
||||
n := total - offset
|
||||
if limit > 0 && limit < n {
|
||||
n = limit
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
read, err := f.ReadAt(buf, offset)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, 0, err
|
||||
}
|
||||
return buf[:read], total, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue