mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-24 19:41:14 +02:00
* feat(storage): add local file storage fallback
- Add local storage implementation for file uploads
- Update .env.example with LOCAL_UPLOAD_DIR and LOCAL_UPLOAD_BASE_URL
- Integrate local storage into server router and handlers
- Add storage abstraction layer with util functions
* ♻️ refactor(storage): improve path handling and file serving
switch from path to filepath for better cross-platform support and replace manual file serving logic with http.ServeFile to enhance security against path traversal. update unit tests to use t.Setenv for cleaner environment variable management.
215 lines
5.3 KiB
Go
215 lines
5.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLocalStorage_Upload(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("LOCAL_UPLOAD_DIR", tmpDir)
|
|
os.Unsetenv("LOCAL_UPLOAD_BASE_URL")
|
|
// No LOCAL_UPLOAD_BASE_URL set - should return relative path
|
|
|
|
store := NewLocalStorageFromEnv()
|
|
if store == nil {
|
|
t.Fatal("NewLocalStorageFromEnv returned nil")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
data := []byte("hello world")
|
|
contentType := "text/plain"
|
|
filename := "test.txt"
|
|
|
|
link, err := store.Upload(ctx, "test-key.txt", data, contentType, filename)
|
|
if err != nil {
|
|
t.Fatalf("Upload failed: %v", err)
|
|
}
|
|
|
|
expectedLink := "/uploads/test-key.txt"
|
|
if link != expectedLink {
|
|
t.Errorf("link = %q, want %q", link, expectedLink)
|
|
}
|
|
|
|
filePath := filepath.Join(tmpDir, "test-key.txt")
|
|
stored, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read uploaded file: %v", err)
|
|
}
|
|
if string(stored) != string(data) {
|
|
t.Errorf("stored data = %q, want %q", stored, data)
|
|
}
|
|
}
|
|
|
|
func TestLocalStorage_Upload_WithBaseURL(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("LOCAL_UPLOAD_DIR", tmpDir)
|
|
t.Setenv("LOCAL_UPLOAD_BASE_URL", "http://localhost:8080")
|
|
|
|
store := NewLocalStorageFromEnv()
|
|
if store == nil {
|
|
t.Fatal("NewLocalStorageFromEnv returned nil")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
data := []byte("hello world")
|
|
contentType := "text/plain"
|
|
filename := "test.txt"
|
|
|
|
link, err := store.Upload(ctx, "test-key.txt", data, contentType, filename)
|
|
if err != nil {
|
|
t.Fatalf("Upload failed: %v", err)
|
|
}
|
|
|
|
// When LOCAL_UPLOAD_BASE_URL is set, should return full URL
|
|
expectedLink := "http://localhost:8080/uploads/test-key.txt"
|
|
if link != expectedLink {
|
|
t.Errorf("link = %q, want %q", link, expectedLink)
|
|
}
|
|
|
|
filePath := filepath.Join(tmpDir, "test-key.txt")
|
|
stored, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read uploaded file: %v", err)
|
|
}
|
|
if string(stored) != string(data) {
|
|
t.Errorf("stored data = %q, want %q", stored, data)
|
|
}
|
|
}
|
|
|
|
func TestLocalStorage_Delete(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("LOCAL_UPLOAD_DIR", tmpDir)
|
|
|
|
store := NewLocalStorageFromEnv()
|
|
if store == nil {
|
|
t.Fatal("NewLocalStorageFromEnv returned nil")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
data := []byte("hello world")
|
|
|
|
_, err := store.Upload(ctx, "delete-me.txt", data, "text/plain", "delete-me.txt")
|
|
if err != nil {
|
|
t.Fatalf("Upload failed: %v", err)
|
|
}
|
|
|
|
filePath := filepath.Join(tmpDir, "delete-me.txt")
|
|
if _, err := os.ReadFile(filePath); err != nil {
|
|
t.Fatalf("file should exist: %v", err)
|
|
}
|
|
|
|
store.Delete(ctx, "delete-me.txt")
|
|
|
|
if _, err := os.ReadFile(filePath); !os.IsNotExist(err) {
|
|
t.Errorf("file should be deleted")
|
|
}
|
|
}
|
|
|
|
func TestLocalStorage_KeyFromURL(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("LOCAL_UPLOAD_DIR", tmpDir)
|
|
// No baseURL set
|
|
|
|
store := NewLocalStorageFromEnv()
|
|
if store == nil {
|
|
t.Fatal("NewLocalStorageFromEnv returned nil")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
rawURL string
|
|
expected string
|
|
}{
|
|
{"local URL format", "/uploads/abc123.png", "abc123.png"},
|
|
{"local URL with subdir", "/uploads/2024/01/image.jpg", "image.jpg"},
|
|
{"just filename", "abc123.png", "abc123.png"},
|
|
{"full path", "/some/path/to/file.pdf", "file.pdf"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := store.KeyFromURL(tc.rawURL)
|
|
if got != tc.expected {
|
|
t.Errorf("KeyFromURL(%q) = %q, want %q", tc.rawURL, got, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLocalStorage_KeyFromURL_WithBaseURL(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("LOCAL_UPLOAD_DIR", tmpDir)
|
|
t.Setenv("LOCAL_UPLOAD_BASE_URL", "http://localhost:8080")
|
|
|
|
store := NewLocalStorageFromEnv()
|
|
if store == nil {
|
|
t.Fatal("NewLocalStorageFromEnv returned nil")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
rawURL string
|
|
expected string
|
|
}{
|
|
{"full URL format", "http://localhost:8080/uploads/abc123.png", "abc123.png"},
|
|
{"full URL with subdir", "http://localhost:8080/uploads/2024/01/image.jpg", "image.jpg"},
|
|
{"local URL format still works", "/uploads/abc123.png", "abc123.png"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := store.KeyFromURL(tc.rawURL)
|
|
if got != tc.expected {
|
|
t.Errorf("KeyFromURL(%q) = %q, want %q", tc.rawURL, got, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLocalStorage_DeleteKeys(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("LOCAL_UPLOAD_DIR", tmpDir)
|
|
|
|
store := NewLocalStorageFromEnv()
|
|
if store == nil {
|
|
t.Fatal("NewLocalStorageFromEnv returned nil")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
data := []byte("hello world")
|
|
|
|
keys := []string{"file1.txt", "file2.txt", "file3.txt"}
|
|
for _, key := range keys {
|
|
_, err := store.Upload(ctx, key, data, "text/plain", key)
|
|
if err != nil {
|
|
t.Fatalf("Upload %s failed: %v", key, err)
|
|
}
|
|
}
|
|
|
|
store.DeleteKeys(ctx, keys)
|
|
|
|
for _, key := range keys {
|
|
filePath := filepath.Join(tmpDir, key)
|
|
if _, err := os.ReadFile(filePath); !os.IsNotExist(err) {
|
|
t.Errorf("file %s should be deleted", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalStorage_KeyFromURL_Empty(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("LOCAL_UPLOAD_DIR", tmpDir)
|
|
|
|
store := NewLocalStorageFromEnv()
|
|
if store == nil {
|
|
t.Fatal("NewLocalStorageFromEnv returned nil")
|
|
}
|
|
|
|
if got := store.KeyFromURL(""); got != "" {
|
|
t.Errorf("KeyFromURL(\"\") = %q, want empty string", got)
|
|
}
|
|
}
|