[feature] migrate to monorepo
This commit is contained in:
commit
05ddc1f783
267 changed files with 75165 additions and 0 deletions
260
backend/internal/storage/local.go
Normal file
260
backend/internal/storage/local.go
Normal file
|
@ -0,0 +1,260 @@
|
|||
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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue