package storage import ( "context" "crypto/rand" "encoding/hex" "fmt" "io" "os" "path/filepath" "time" ) type LocalStore struct { rootDir string baseURL string } func NewLocalStore(rootDir, baseURL string) (*LocalStore, error) { if err := os.MkdirAll(rootDir, 0755); err != nil { return nil, fmt.Errorf("create storage dir: %w", err) } return &LocalStore{rootDir: rootDir, baseURL: baseURL}, nil } func (s *LocalStore) Upload(ctx context.Context, tenantID string, file *File) (*StoredFile, error) { now := time.Now() dir := filepath.Join(s.rootDir, tenantID, now.Format("2006/01/02")) if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("create upload dir: %w", err) } ext := filepath.Ext(file.Name) key := generateKey(ext) destPath := filepath.Join(dir, key) dest, err := os.Create(destPath) if err != nil { return nil, fmt.Errorf("create file: %w", err) } defer dest.Close() written, err := io.Copy(dest, file.Reader) if err != nil { os.Remove(destPath) return nil, fmt.Errorf("write file: %w", err) } relKey := filepath.Join(tenantID, now.Format("2006/01/02"), key) return &StoredFile{ Key: relKey, URL: s.URL(relKey), Size: written, }, nil } func (s *LocalStore) Delete(ctx context.Context, key string) error { path := filepath.Join(s.rootDir, key) if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("delete file: %w", err) } return nil } func (s *LocalStore) Exists(ctx context.Context, key string) (bool, error) { path := filepath.Join(s.rootDir, key) _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err } func (s *LocalStore) URL(key string) string { return fmt.Sprintf("%s/uploads/%s", s.baseURL, key) } func generateKey(ext string) string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) + ext }