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) }