tss-rocks/backend/internal/storage/local.go
CDN 05ddc1f783
Some checks failed
Build Backend / Build Docker Image (push) Successful in 3m33s
Test Backend / test (push) Failing after 31s
[feature] migrate to monorepo
2025-02-21 00:49:20 +08:00

260 lines
6.8 KiB
Go

package storage
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
type LocalStorage struct {
rootDir string
metaDir string
}
func NewLocalStorage(rootDir string) (*LocalStorage, error) {
// Ensure the root directory exists
if err := os.MkdirAll(rootDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create root directory: %w", err)
}
// Create metadata directory
metaDir := filepath.Join(rootDir, ".meta")
if err := os.MkdirAll(metaDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create metadata directory: %w", err)
}
return &LocalStorage{
rootDir: rootDir,
metaDir: metaDir,
}, nil
}
func (s *LocalStorage) generateID() (string, error) {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func (s *LocalStorage) saveMetadata(id string, info *FileInfo) error {
metaPath := filepath.Join(s.metaDir, id+".meta")
file, err := os.Create(metaPath)
if err != nil {
return fmt.Errorf("failed to create metadata file: %w", err)
}
defer file.Close()
data := fmt.Sprintf("%s\n%s", info.Name, info.ContentType)
if _, err := file.WriteString(data); err != nil {
return fmt.Errorf("failed to write metadata: %w", err)
}
return nil
}
func (s *LocalStorage) loadMetadata(id string) (string, string, error) {
metaPath := filepath.Join(s.metaDir, id+".meta")
data, err := os.ReadFile(metaPath)
if err != nil {
if os.IsNotExist(err) {
return id, "", nil // Return ID as name if metadata doesn't exist
}
return "", "", fmt.Errorf("failed to read metadata: %w", err)
}
parts := bytes.Split(data, []byte("\n"))
name := string(parts[0])
contentType := ""
if len(parts) > 1 {
contentType = string(parts[1])
}
return name, contentType, nil
}
func (s *LocalStorage) Save(ctx context.Context, name string, contentType string, reader io.Reader) (*FileInfo, error) {
if reader == nil {
return nil, fmt.Errorf("reader cannot be nil")
}
// Generate a unique ID for the file
id, err := s.generateID()
if err != nil {
return nil, fmt.Errorf("failed to generate file ID: %w", err)
}
// Create the file path
filePath := filepath.Join(s.rootDir, id)
// Create the file
file, err := os.Create(filePath)
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
defer file.Close()
// Copy the content
size, err := io.Copy(file, reader)
if err != nil {
// Clean up the file if there's an error
os.Remove(filePath)
return nil, fmt.Errorf("failed to write file content: %w", err)
}
now := time.Now()
info := &FileInfo{
ID: id,
Name: name,
Size: size,
ContentType: contentType,
CreatedAt: now,
UpdatedAt: now,
URL: fmt.Sprintf("/api/media/file/%s", id),
}
// Save metadata
if err := s.saveMetadata(id, info); err != nil {
os.Remove(filePath)
return nil, err
}
return info, nil
}
func (s *LocalStorage) Get(ctx context.Context, id string) (io.ReadCloser, *FileInfo, error) {
filePath := filepath.Join(s.rootDir, id)
// Open the file
file, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil, fmt.Errorf("file not found: %s", id)
}
return nil, nil, fmt.Errorf("failed to open file: %w", err)
}
// Get file info
stat, err := file.Stat()
if err != nil {
file.Close()
return nil, nil, fmt.Errorf("failed to get file info: %w", err)
}
// Load metadata
name, contentType, err := s.loadMetadata(id)
if err != nil {
file.Close()
return nil, nil, err
}
info := &FileInfo{
ID: id,
Name: name,
Size: stat.Size(),
ContentType: contentType,
CreatedAt: stat.ModTime(),
UpdatedAt: stat.ModTime(),
URL: fmt.Sprintf("/api/media/file/%s", id),
}
return file, info, nil
}
func (s *LocalStorage) Delete(ctx context.Context, id string) error {
filePath := filepath.Join(s.rootDir, id)
if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("file not found: %s", id)
}
return fmt.Errorf("failed to delete file: %w", err)
}
// Remove metadata
metaPath := filepath.Join(s.metaDir, id+".meta")
if err := os.Remove(metaPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove metadata: %w", err)
}
return nil
}
func (s *LocalStorage) List(ctx context.Context, prefix string, limit int, offset int) ([]*FileInfo, error) {
var files []*FileInfo
var count int
err := filepath.Walk(s.rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories and metadata directory
if info.IsDir() || path == s.metaDir {
if path == s.metaDir {
return filepath.SkipDir
}
return nil
}
// Get the file ID (basename of the path)
id := filepath.Base(path)
// Load metadata to get the original name
name, contentType, err := s.loadMetadata(id)
if err != nil {
return err
}
// Skip files that don't match the prefix
if prefix != "" && !strings.HasPrefix(name, prefix) {
return nil
}
// Skip files before offset
if count < offset {
count++
return nil
}
// Stop if we've reached the limit
if limit > 0 && len(files) >= limit {
return filepath.SkipDir
}
files = append(files, &FileInfo{
ID: id,
Name: name,
Size: info.Size(),
ContentType: contentType,
CreatedAt: info.ModTime(),
UpdatedAt: info.ModTime(),
URL: fmt.Sprintf("/api/media/file/%s", id),
})
count++
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list files: %w", err)
}
return files, nil
}
func (s *LocalStorage) Exists(ctx context.Context, id string) (bool, error) {
filePath := filepath.Join(s.rootDir, id)
_, err := os.Stat(filePath)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("failed to check file existence: %w", err)
}