This commit is contained in:
Stephan D
2026-03-10 12:31:09 +01:00
parent d87e709f43
commit e77d1ab793
287 changed files with 2089 additions and 1550 deletions

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/domainprovider"
@@ -34,7 +35,10 @@ func (storage *LocalStorage) Delete(ctx context.Context, objID string) error {
default:
}
filePath := filepath.Join(storage.storageDir, objID)
filePath, err := storage.resolvePath(objID)
if err != nil {
return err
}
if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) {
storage.logger.Debug("File not found", zap.String("obj_ref", objID))
@@ -54,7 +58,11 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
default:
}
filePath := filepath.Join(storage.storageDir, objID)
filePath, err := storage.resolvePath(objID)
if err != nil {
return "", err
}
//nolint:gosec // File path is resolved and constrained to storage root.
dst, err := os.Create(filePath)
if err != nil {
storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID))
@@ -78,7 +86,9 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
}
case <-ctx.Done():
// Context was cancelled, clean up the partial file
os.Remove(filePath)
if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) {
storage.logger.Warn("Failed to remove partially written file", zap.Error(removeErr), zap.String("obj_ref", objID))
}
return "", ctx.Err()
}
@@ -93,7 +103,10 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
default:
}
filePath := filepath.Join(storage.storageDir, objRef)
filePath, err := storage.resolvePath(objRef)
if err != nil {
return response.Internal(storage.logger, storage.service, err)
}
if _, err := os.Stat(filePath); err != nil {
storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef))
return response.Internal(storage.logger, storage.service, err)
@@ -117,7 +130,7 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
func ensureDir(dirName string) error {
info, err := os.Stat(dirName)
if os.IsNotExist(err) {
return os.MkdirAll(dirName, 0o755)
return os.MkdirAll(dirName, 0o750)
}
if err != nil {
return err
@@ -128,6 +141,24 @@ func ensureDir(dirName string) error {
return nil
}
func (storage *LocalStorage) resolvePath(objID string) (string, error) {
objID = strings.TrimSpace(objID)
if objID == "" {
return "", merrors.InvalidArgument("obj_ref is required", "obj_ref")
}
filePath := filepath.Join(storage.storageDir, objID)
relPath, err := filepath.Rel(storage.storageDir, filePath)
if err != nil {
return "", merrors.InternalWrap(err, "failed to resolve local file path")
}
if relPath == "." || strings.HasPrefix(relPath, "..") {
return "", merrors.InvalidArgument("obj_ref is invalid", "obj_ref")
}
return filePath, nil
}
func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) {
dir := filepath.Join(cfg.RootPath, directory)
if err := ensureDir(dir); err != nil {

View File

@@ -55,7 +55,7 @@ func setupTestStorage(t *testing.T) (*LocalStorage, string, func()) {
// Return cleanup function
cleanup := func() {
os.RemoveAll(tempDir)
require.NoError(t, os.RemoveAll(tempDir))
}
return storage, tempDir, cleanup
@@ -81,7 +81,7 @@ func setupBenchmarkStorage(b *testing.B) (*LocalStorage, string, func()) {
// Return cleanup function
cleanup := func() {
os.RemoveAll(tempDir)
require.NoError(b, os.RemoveAll(tempDir))
}
return storage, tempDir, cleanup
@@ -138,6 +138,7 @@ func TestLocalStorage_Save(t *testing.T) {
// Verify file was actually saved
filePath := filepath.Join(tempDir, tt.objID)
//nolint:gosec // Test-controlled path inside temporary directory.
content, err := os.ReadFile(filePath)
assert.NoError(t, err)
assert.Equal(t, tt.content, string(content))
@@ -186,7 +187,7 @@ func TestLocalStorage_Delete(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
tests := []struct {
@@ -232,7 +233,7 @@ func TestLocalStorage_Delete_ContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
// Create a context that's already cancelled
@@ -256,7 +257,7 @@ func TestLocalStorage_Get(t *testing.T) {
// Create a test file
testContent := "test file content"
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte(testContent), 0o644)
err := os.WriteFile(testFile, []byte(testContent), 0o600)
require.NoError(t, err)
tests := []struct {
@@ -285,7 +286,7 @@ func TestLocalStorage_Get(t *testing.T) {
handler := storage.Get(ctx, tt.objID)
// Create test request
req := httptest.NewRequest("GET", "/files/"+tt.objID, nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/"+tt.objID, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
@@ -304,7 +305,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
// Create a context that's already cancelled
@@ -314,7 +315,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
handler := storage.Get(ctx, "test.txt")
// Create test request
req := httptest.NewRequest("GET", "/files/test.txt", nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
@@ -328,14 +329,14 @@ func TestLocalStorage_Get_RequestContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
ctx := context.Background()
handler := storage.Get(ctx, "test.txt")
// Create test request with cancelled context
req := httptest.NewRequest("GET", "/files/test.txt", nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
reqCtx, cancel := context.WithCancel(req.Context())
req = req.WithContext(reqCtx)
cancel() // Cancel the request context
@@ -352,7 +353,9 @@ func TestCreateLocalFileStorage(t *testing.T) {
// Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "storage_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
logger := zap.NewNop()
cfg := config.LocalFSSConfig{
@@ -372,10 +375,12 @@ func TestCreateLocalFileStorage_InvalidPath(t *testing.T) {
// Build a deterministic failure case: the target path already exists as a file.
tempDir, err := os.MkdirTemp("", "storage_invalid_path_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
fileAtTargetPath := filepath.Join(tempDir, "test")
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o644)
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o600)
require.NoError(t, err)
logger := zap.NewNop()
@@ -426,7 +431,7 @@ func TestLocalStorage_ConcurrentOperations(t *testing.T) {
// Create files to delete
for i := 0; i < 5; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644)
err := os.WriteFile(filePath, []byte("content"), 0o600)
require.NoError(t, err)
}
@@ -536,7 +541,7 @@ func BenchmarkLocalStorage_Delete(b *testing.B) {
// Pre-create files for deletion
for i := 0; i < b.N; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644)
err := os.WriteFile(filePath, []byte("content"), 0o600)
if err != nil {
b.Fatal(err)
}