Initial commit: Lagerbestand Blazor Projekt

This commit is contained in:
spaethf committed 2026-09-21 21:28:23 +02:00
commit cba9414e92
105 files changed
+36431

No files matched your search

+254
View File
@@ -0,0 +1,254 @@
using Lagerbestand.Models;
using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace Lagerbestand.Services;
/// <summary>
/// File storage service for disk/NFS-based file operations
/// </summary>
public class FileStorageService : IFileStorageService
{
private readonly ISiteSettingsService _siteSettingsService;
private readonly ILogger<FileStorageService> _logger;
private const string SettingsKey = "FileStorage";
public FileStorageService(ISiteSettingsService siteSettingsService, ILogger<FileStorageService> logger)
{
_siteSettingsService = siteSettingsService;
_logger = logger;
}
public async Task<FileStorageSettings> GetSettingsAsync()
{
try
{
var json = await _siteSettingsService.GetValueAsync(SettingsKey);
if (string.IsNullOrEmpty(json))
{
return new FileStorageSettings();
}
return JsonSerializer.Deserialize<FileStorageSettings>(json) ?? new FileStorageSettings();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading file storage settings");
return new FileStorageSettings();
}
}
public async Task SaveSettingsAsync(FileStorageSettings settings)
{
var json = JsonSerializer.Serialize(settings);
await _siteSettingsService.SetValueAsync(SettingsKey, json);
_logger.LogInformation("File storage settings saved. Path: {BasePath}, Enabled: {IsEnabled}",
settings.BasePath, settings.IsEnabled);
}
public Task<(bool IsValid, string? ErrorMessage)> ValidateStoragePathAsync(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return Task.FromResult((false, (string?)"Der Pfad darf nicht leer sein."));
}
try
{
// Check if directory exists or can be created
if (!Directory.Exists(path))
{
try
{
Directory.CreateDirectory(path);
_logger.LogInformation("Created storage directory: {Path}", path);
}
catch (Exception ex)
{
return Task.FromResult((false, (string?)$"Verzeichnis kann nicht erstellt werden: {ex.Message}"));
}
}
// Test write access
var testFile = Path.Combine(path, $".write_test_{Guid.NewGuid():N}");
try
{
File.WriteAllText(testFile, "test");
File.Delete(testFile);
}
catch (Exception ex)
{
return Task.FromResult((false, (string?)$"Keine Schreibrechte: {ex.Message}"));
}
return Task.FromResult((true, (string?)null));
}
catch (Exception ex)
{
return Task.FromResult((false, (string?)$"Fehler: {ex.Message}"));
}
}
public async Task<string> SaveFileAsync(byte[] content, string fileName, string subDirectory = "attachments")
{
var settings = await GetSettingsAsync();
if (!settings.IsEnabled || string.IsNullOrWhiteSpace(settings.BasePath))
{
throw new InvalidOperationException("Dateispeicher ist nicht konfiguriert.");
}
// Create unique filename to avoid collisions
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
var uniqueId = Guid.NewGuid().ToString("N")[..8];
var safeFileName = SanitizeFileName(fileName);
var extension = Path.GetExtension(safeFileName);
var nameWithoutExt = Path.GetFileNameWithoutExtension(safeFileName);
var finalFileName = $"{nameWithoutExt}_{timestamp}_{uniqueId}{extension}";
// Create subdirectory path with year/month structure
var datePath = DateTime.UtcNow.ToString("yyyy/MM");
var relativePath = Path.Combine(subDirectory, datePath, finalFileName);
var fullPath = Path.Combine(settings.BasePath, relativePath);
// Ensure directory exists
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
await File.WriteAllBytesAsync(fullPath, content);
_logger.LogInformation("File saved: {RelativePath} ({Size} bytes)", relativePath, content.Length);
return relativePath;
}
public async Task<byte[]?> ReadFileAsync(string relativePath)
{
var settings = await GetSettingsAsync();
if (!settings.IsEnabled || string.IsNullOrWhiteSpace(settings.BasePath))
{
return null;
}
var fullPath = ResolveSafePath(settings.BasePath, relativePath);
if (fullPath == null || !File.Exists(fullPath))
{
_logger.LogWarning("File not found: {RelativePath}", relativePath);
return null;
}
return await File.ReadAllBytesAsync(fullPath);
}
public async Task<bool> DeleteFileAsync(string relativePath)
{
var settings = await GetSettingsAsync();
if (!settings.IsEnabled || string.IsNullOrWhiteSpace(settings.BasePath))
{
return false;
}
var fullPath = ResolveSafePath(settings.BasePath, relativePath);
if (fullPath == null)
{
return false;
}
if (!File.Exists(fullPath))
{
return true; // Already deleted
}
try
{
File.Delete(fullPath);
_logger.LogInformation("File deleted: {RelativePath}", relativePath);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting file: {RelativePath}", relativePath);
return false;
}
}
public async Task<bool> FileExistsAsync(string relativePath)
{
var settings = await GetSettingsAsync();
if (!settings.IsEnabled || string.IsNullOrWhiteSpace(settings.BasePath))
{
return false;
}
var fullPath = ResolveSafePath(settings.BasePath, relativePath);
return await Task.FromResult(fullPath != null && File.Exists(fullPath));
}
public async Task<long> GetFileSizeAsync(string relativePath)
{
var settings = await GetSettingsAsync();
if (!settings.IsEnabled || string.IsNullOrWhiteSpace(settings.BasePath))
{
return 0;
}
var fullPath = ResolveSafePath(settings.BasePath, relativePath);
if (fullPath == null || !File.Exists(fullPath))
{
return 0;
}
var fileInfo = new FileInfo(fullPath);
return await Task.FromResult(fileInfo.Length);
}
/// <summary>
/// Resolves a relative path against the base path and blocks path traversal attempts.
/// Returns null if the resolved path escapes the base directory.
/// </summary>
private string? ResolveSafePath(string basePath, string relativePath)
{
try
{
var fullBasePath = Path.GetFullPath(basePath);
var fullPath = Path.GetFullPath(Path.Combine(fullBasePath, relativePath));
if (!fullPath.StartsWith(fullBasePath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Path traversal attempt blocked: {RelativePath}", relativePath);
return null;
}
return fullPath;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Invalid path rejected: {RelativePath}", relativePath);
return null;
}
}
private static string SanitizeFileName(string fileName)
{
var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = string.Join("_", fileName.Split(invalidChars, StringSplitOptions.RemoveEmptyEntries));
// Limit length
if (sanitized.Length > 200)
{
var ext = Path.GetExtension(sanitized);
var name = Path.GetFileNameWithoutExtension(sanitized);
sanitized = name[..(200 - ext.Length)] + ext;
}
return sanitized;
}
}