mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 14:49:09 +02:00
- Add GET /api/config endpoint exposing cdn_domain from CLOUDFRONT_DOMAIN - Create packages/core/config/ zustand store, fetched at app startup - Extract file card preprocessing to packages/ui/markdown/file-cards.ts with isCdnUrl(url, cdnDomain) using exact hostname match - Add file card support to packages/ui/markdown/Markdown.tsx (was missing) - Remove hardcoded .copilothub.ai hostname check from file-card.tsx - Fix LocalStorage.CdnDomain() to return hostname not full URL - Always run preprocessFileCards regardless of cdnDomain availability (!file syntax works without CDN domain, only legacy matching needs it) - Use useConfigStore hook in common/markdown.tsx for reactive updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
126 lines
3.2 KiB
Go
126 lines
3.2 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type LocalStorage struct {
|
|
uploadDir string
|
|
baseURL string
|
|
}
|
|
|
|
// NewLocalStorageFromEnv creates a LocalStorage from environment variables.
|
|
// Returns nil if upload directory cannot be created.
|
|
//
|
|
// Environment variables:
|
|
// - LOCAL_UPLOAD_DIR (default: "./data/uploads")
|
|
// - LOCAL_UPLOAD_BASE_URL (optional, e.g., "http://localhost:8080")
|
|
func NewLocalStorageFromEnv() *LocalStorage {
|
|
uploadDir := os.Getenv("LOCAL_UPLOAD_DIR")
|
|
if uploadDir == "" {
|
|
uploadDir = "./data/uploads"
|
|
}
|
|
|
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
|
slog.Error("failed to create upload directory", "dir", uploadDir, "error", err)
|
|
return nil
|
|
}
|
|
|
|
baseURL := strings.TrimSuffix(os.Getenv("LOCAL_UPLOAD_BASE_URL"), "/")
|
|
|
|
slog.Info("local storage initialized", "dir", uploadDir, "baseURL", baseURL)
|
|
return &LocalStorage{
|
|
uploadDir: uploadDir,
|
|
baseURL: baseURL,
|
|
}
|
|
}
|
|
|
|
func (s *LocalStorage) CdnDomain() string {
|
|
if s.baseURL == "" {
|
|
return ""
|
|
}
|
|
u, err := url.Parse(s.baseURL)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return u.Hostname()
|
|
}
|
|
|
|
func (s *LocalStorage) KeyFromURL(rawURL string) string {
|
|
if s.baseURL != "" && strings.HasPrefix(rawURL, s.baseURL) {
|
|
rawURL = strings.TrimPrefix(rawURL, s.baseURL)
|
|
}
|
|
|
|
prefix := "/uploads/"
|
|
if idx := strings.Index(rawURL, prefix); idx >= 0 {
|
|
return rawURL[idx+len(prefix):]
|
|
}
|
|
if i := strings.LastIndex(rawURL, "/"); i >= 0 {
|
|
return rawURL[i+1:]
|
|
}
|
|
return rawURL
|
|
}
|
|
|
|
func (s *LocalStorage) Delete(ctx context.Context, key string) {
|
|
if key == "" {
|
|
return
|
|
}
|
|
filePath := filepath.Join(s.uploadDir, key)
|
|
if err := os.Remove(filePath); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
slog.Error("local storage Delete failed", "key", key, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *LocalStorage) DeleteKeys(ctx context.Context, keys []string) {
|
|
for _, key := range keys {
|
|
s.Delete(ctx, key)
|
|
}
|
|
}
|
|
|
|
func (s *LocalStorage) Upload(ctx context.Context, key string, data []byte, contentType string, filename string) (string, error) {
|
|
dest := filepath.Join(s.uploadDir, key)
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
|
|
return "", fmt.Errorf("local storage MkdirAll: %w", err)
|
|
}
|
|
if err := os.WriteFile(dest, data, 0644); err != nil {
|
|
return "", fmt.Errorf("local storage WriteFile: %w", err)
|
|
}
|
|
|
|
if s.baseURL != "" {
|
|
return fmt.Sprintf("%s/uploads/%s", s.baseURL, key), nil
|
|
}
|
|
return fmt.Sprintf("/uploads/%s", key), nil
|
|
}
|
|
|
|
func (s *LocalStorage) GetFilePath(key string) string {
|
|
return filepath.Join(s.uploadDir, key)
|
|
}
|
|
|
|
func (s *LocalStorage) ServeFile(w http.ResponseWriter, r *http.Request, filename string) {
|
|
filePath := filepath.Join(s.uploadDir, filename)
|
|
slog.Info("serving file", "filename", filename, "filepath", filePath)
|
|
|
|
// Use http.ServeFile which has built-in path traversal protection
|
|
// It sanitizes the path and prevents access outside the directory
|
|
http.ServeFile(w, r, filePath)
|
|
}
|
|
|
|
func (s *LocalStorage) UploadFromReader(ctx context.Context, key string, reader io.Reader, contentType string, filename string) (string, error) {
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return "", fmt.Errorf("local storage ReadAll: %w", err)
|
|
}
|
|
|
|
return s.Upload(ctx, key, data, contentType, filename)
|
|
}
|