260 lines
6.8 KiB
Go
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)
|
|
}
|