Initial commit: Lagerbestand Blazor Projekt
This commit is contained in:
commit
cba9414e92
105 files changed
+36431
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user