Local file system storage adapter implementation that provides file storage operations using the local file system, implementing the blob storage abstractions defined in DKNet.Svc.BlobStorage.Abstractions while supporting Domain-Driven Design (DDD) and Onion Architecture principles for development, testing, and on-premises scenarios.
DKNet.Svc.BlobStorage.Local provides a complete implementation of the blob storage abstractions for local file system storage, enabling applications to store, retrieve, and manage files on the local disk or network file shares. This adapter is ideal for development environments, testing scenarios, on-premises deployments, and applications that require local file storage without cloud dependencies.
DKNet.Svc.BlobStorage.Local implements the Infrastructure Layer of the Onion Architecture, providing concrete local file system storage capabilities without affecting higher layers:
┌─────────────────────────────────────────────────────────────────┐
│ 🌐 Presentation Layer │
│ (Controllers, API Endpoints) │
│ │
│ Uses: File upload/download endpoints with local URLs │
│ Returns: Local file paths, download streams, upload results │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────┴───────────────────────────────────────┐
│ 🎯 Application Layer │
│ (Use Cases, Application Services) │
│ │
│ Depends on: IBlobService abstraction │
│ Benefits from: Fast local access, no cloud dependencies │
│ Orchestrates: File processing workflows with local storage │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────┴───────────────────────────────────────┐
│ 💼 Domain Layer │
│ (Entities, Aggregates, Domain Services) │
│ │
│ 🎭 Domain entities reference file locations as value objects │
│ 📝 File metadata as business concepts (path, directory) │
│ 🏷️ Completely unaware of local file system implementation │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────┴───────────────────────────────────────┐
│ 🗄️ Infrastructure Layer │
│ (Local File System Implementation) │
│ │
│ 💾 LocalBlobService - File system operations │
│ 🔧 LocalDirectoryOptions - Local path configuration │
│ ⚙️ LocalDirectorySetup - Service registration and setup │
│ 🔒 Path security and validation │
│ 📊 File system monitoring and diagnostics │
│ 🌍 Cross-platform file system compatibility │
└─────────────────────────────────────────────────────────────────┘
dotnet add package DKNet.Svc.BlobStorage.Local
dotnet add package DKNet.Svc.BlobStorage.Abstractions
using DKNet.Svc.BlobStorage.Local;
using DKNet.Svc.BlobStorage.Abstractions;
// appsettings.json configuration
{
"LocalBlobStorage": {
"RootPath": "C:\\App\\Storage", // Windows
// "RootPath": "/var/app/storage", // Linux/macOS
"CreateDirectoryIfNotExists": true,
"EnableMetadataStorage": true,
"MetadataStorageType": "SidecarFiles", // Options: SidecarFiles, ExtendedAttributes
"MaxFileSize": 104857600, // 100MB
"AllowedFileExtensions": [".jpg", ".jpeg", ".png", ".pdf", ".docx", ".txt"],
"PreserveDirectoryStructure": true,
"EnableFileWatcher": true,
"TempDirectory": "temp",
"BackupDirectory": "backups"
}
}
// Service registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddLocalBlobStorage(
this IServiceCollection services,
IConfiguration configuration)
{
// Configure local storage options
services.Configure<LocalDirectoryOptions>(configuration.GetSection("LocalBlobStorage"));
// Register blob storage service
services.AddScoped<IBlobService, LocalBlobService>();
// Optional: Register file watcher service
services.AddSingleton<IFileWatcherService, FileWatcherService>();
return services;
}
// Alternative with explicit configuration
public static IServiceCollection AddLocalBlobStorageWithPath(
this IServiceCollection services,
string rootPath,
Action<LocalDirectoryOptions>? configureOptions = null)
{
services.Configure<LocalDirectoryOptions>(options =>
{
options.RootPath = rootPath;
options.CreateDirectoryIfNotExists = true;
options.EnableMetadataStorage = true;
configureOptions?.Invoke(options);
});
services.AddScoped<IBlobService, LocalBlobService>();
return services;
}
}
// Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Method 1: Configuration-based setup
services.AddLocalBlobStorage(Configuration);
// Method 2: Explicit path setup
services.AddLocalBlobStorageWithPath(
Path.Combine(Environment.ContentRootPath, "Storage"),
options =>
{
options.EnableFileWatcher = true;
options.MetadataStorageType = "ExtendedAttributes";
});
}
public class DocumentService
{
private readonly IBlobService _blobService;
private readonly ILogger<DocumentService> _logger;
public DocumentService(IBlobService blobService, ILogger<DocumentService> logger)
{
_blobService = blobService;
_logger = logger;
}
// Upload document to local storage
public async Task<string> UploadDocumentAsync(IFormFile file, string userId, string category)
{
try
{
var fileName = GenerateFileName(file.FileName, userId, category);
using var stream = file.OpenReadStream();
var blobData = new BlobData
{
FileName = fileName,
ContentType = file.ContentType,
Content = stream,
Metadata = new Dictionary<string, string>
{
["UploadedBy"] = userId,
["Category"] = category,
["OriginalFileName"] = file.FileName,
["UploadedAt"] = DateTime.UtcNow.ToString("O"),
["FileSize"] = file.Length.ToString(),
["CheckSum"] = await ComputeChecksumAsync(file.OpenReadStream())
}
};
var result = await _blobService.SaveAsync(blobData);
_logger.LogInformation("Document uploaded to local storage: {FileName} -> {LocalPath}",
file.FileName, result.FileName);
return result.FileName;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload document to local storage: {FileName}", file.FileName);
throw;
}
}
// Download document from local storage
public async Task<FileResult> DownloadDocumentAsync(string fileName)
{
try
{
var exists = await _blobService.ExistsAsync(fileName);
if (!exists)
{
throw new FileNotFoundException($"Document not found: {fileName}");
}
var blobData = await _blobService.GetAsync(fileName);
// Verify file integrity
var storedChecksum = blobData.Metadata.GetValueOrDefault("CheckSum");
if (!string.IsNullOrEmpty(storedChecksum))
{
var currentChecksum = await ComputeChecksumAsync(blobData.Content);
if (storedChecksum != currentChecksum)
{
_logger.LogWarning("File integrity check failed for: {FileName}", fileName);
}
}
return new FileStreamResult(blobData.Content, blobData.ContentType)
{
FileDownloadName = GetOriginalFileName(blobData.Metadata) ?? Path.GetFileName(fileName)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to download document from local storage: {FileName}", fileName);
throw;
}
}
// Get local file path for direct access
public async Task<string> GetLocalFilePathAsync(string fileName)
{
var exists = await _blobService.ExistsAsync(fileName);
if (!exists)
{
throw new FileNotFoundException($"File not found: {fileName}");
}
// This is specific to local storage - get actual file path
if (_blobService is LocalBlobService localService)
{
return localService.GetPhysicalPath(fileName);
}
throw new InvalidOperationException("This operation is only supported for local storage");
}
// List files in directory
public async Task<IEnumerable<BlobInfo>> ListUserDocumentsAsync(string userId, string category = null)
{
var prefix = string.IsNullOrEmpty(category)
? $"{userId}/"
: $"{userId}/{category}/";
return await _blobService.ListAsync(prefix);
}
// Move file to different category
public async Task MoveDocumentAsync(string fileName, string newCategory)
{
try
{
var exists = await _blobService.ExistsAsync(fileName);
if (!exists)
{
throw new FileNotFoundException($"File not found: {fileName}");
}
// Get file data
var blobData = await _blobService.GetAsync(fileName);
// Create new file name with different category
var pathParts = fileName.Split('/');
if (pathParts.Length >= 3)
{
pathParts[1] = newCategory; // Assuming format: userId/category/filename
var newFileName = string.Join("/", pathParts);
// Save to new location
blobData.FileName = newFileName;
await _blobService.SaveAsync(blobData);
// Delete old file
await _blobService.DeleteAsync(fileName);
_logger.LogInformation("Document moved: {OldPath} -> {NewPath}", fileName, newFileName);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to move document: {FileName}", fileName);
throw;
}
}
private string GenerateFileName(string originalFileName, string userId, string category)
{
var extension = Path.GetExtension(originalFileName);
var safeFileName = Path.GetFileNameWithoutExtension(originalFileName)
.Replace(" ", "_")
.Replace("#", "_");
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
return $"{userId}/{category}/{timestamp}_{safeFileName}{extension}";
}
private async Task<string> ComputeChecksumAsync(Stream stream)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hash = await sha256.ComputeHashAsync(stream);
return Convert.ToBase64String(hash);
}
private string GetOriginalFileName(IDictionary<string, string> metadata)
{
return metadata.GetValueOrDefault("OriginalFileName");
}
}
public class AdvancedLocalStorageService
{
private readonly LocalDirectoryOptions _options;
private readonly ILogger<AdvancedLocalStorageService> _logger;
private readonly IFileWatcherService _fileWatcher;
public AdvancedLocalStorageService(
IOptions<LocalDirectoryOptions> options,
ILogger<AdvancedLocalStorageService> logger,
IFileWatcherService fileWatcher)
{
_options = options.Value;
_logger = logger;
_fileWatcher = fileWatcher;
}
// Batch file operations
public async Task<BatchOperationResult> BatchCopyAsync(IEnumerable<string> sourceFiles, string destinationPrefix)
{
var results = new List<BatchOperationItem>();
var semaphore = new SemaphoreSlim(Environment.ProcessorCount); // Limit concurrency
var tasks = sourceFiles.Select(async sourceFile =>
{
await semaphore.WaitAsync();
try
{
var destinationFile = $"{destinationPrefix}/{Path.GetFileName(sourceFile)}";
var fullSourcePath = Path.Combine(_options.RootPath, sourceFile);
var fullDestinationPath = Path.Combine(_options.RootPath, destinationFile);
// Ensure destination directory exists
Directory.CreateDirectory(Path.GetDirectoryName(fullDestinationPath));
// Copy file
await CopyFileAsync(fullSourcePath, fullDestinationPath);
return new BatchOperationItem
{
FileName = sourceFile,
Success = true,
Message = $"Copied to {destinationFile}"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to copy file: {SourceFile}", sourceFile);
return new BatchOperationItem
{
FileName = sourceFile,
Success = false,
Message = ex.Message
};
}
finally
{
semaphore.Release();
}
});
var batchResults = await Task.WhenAll(tasks);
return new BatchOperationResult
{
TotalFiles = sourceFiles.Count(),
SuccessfulOperations = batchResults.Count(r => r.Success),
FailedOperations = batchResults.Count(r => !r.Success),
Results = batchResults.ToList()
};
}
// File system cleanup operations
public async Task<CleanupResult> CleanupOldFilesAsync(TimeSpan maxAge, string pathPattern = "*")
{
var cleanupResult = new CleanupResult();
var cutoffDate = DateTime.UtcNow.Subtract(maxAge);
try
{
var searchPath = Path.Combine(_options.RootPath, pathPattern);
var files = Directory.EnumerateFiles(_options.RootPath, pathPattern, SearchOption.AllDirectories);
foreach (var file in files)
{
var fileInfo = new FileInfo(file);
if (fileInfo.LastWriteTime < cutoffDate)
{
try
{
// Move to backup before deletion if backup is enabled
if (!string.IsNullOrEmpty(_options.BackupDirectory))
{
await BackupFileAsync(file);
}
File.Delete(file);
cleanupResult.DeletedFiles++;
cleanupResult.FreedSpace += fileInfo.Length;
_logger.LogDebug("Deleted old file: {FilePath}", file);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete file: {FilePath}", file);
cleanupResult.FailedDeletions++;
}
}
}
// Clean up empty directories
await CleanupEmptyDirectoriesAsync(_options.RootPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Cleanup operation failed");
throw;
}
return cleanupResult;
}
// File integrity verification
public async Task<IntegrityCheckResult> VerifyFileIntegrityAsync(string fileName)
{
var result = new IntegrityCheckResult { FileName = fileName };
try
{
var fullPath = Path.Combine(_options.RootPath, fileName);
if (!File.Exists(fullPath))
{
result.IsValid = false;
result.Issues.Add("File does not exist");
return result;
}
// Check file size
var fileInfo = new FileInfo(fullPath);
result.FileSize = fileInfo.Length;
// Verify checksum if metadata exists
var metadataPath = GetMetadataPath(fileName);
if (File.Exists(metadataPath))
{
var metadata = await ReadMetadataAsync(metadataPath);
if (metadata.TryGetValue("CheckSum", out var storedChecksum))
{
using var fileStream = File.OpenRead(fullPath);
var currentChecksum = await ComputeChecksumAsync(fileStream);
if (storedChecksum == currentChecksum)
{
result.ChecksumValid = true;
}
else
{
result.IsValid = false;
result.Issues.Add("Checksum mismatch");
}
}
}
// Check file permissions
try
{
using var stream = File.OpenRead(fullPath);
result.IsReadable = true;
}
catch
{
result.IsValid = false;
result.Issues.Add("File is not readable");
}
result.IsValid = result.Issues.Count == 0;
}
catch (Exception ex)
{
result.IsValid = false;
result.Issues.Add($"Verification failed: {ex.Message}");
}
return result;
}
// Setup file system monitoring
public void StartFileSystemMonitoring()
{
if (!_options.EnableFileWatcher)
return;
_fileWatcher.FileCreated += OnFileCreated;
_fileWatcher.FileModified += OnFileModified;
_fileWatcher.FileDeleted += OnFileDeleted;
_fileWatcher.StartWatching(_options.RootPath);
_logger.LogInformation("File system monitoring started for: {RootPath}", _options.RootPath);
}
private async void OnFileCreated(object sender, FileSystemEventArgs e)
{
_logger.LogDebug("File created: {FilePath}", e.FullPath);
// Optionally trigger business events
// await _eventPublisher.PublishAsync(new FileCreatedEvent(e.FullPath));
}
private async void OnFileModified(object sender, FileSystemEventArgs e)
{
_logger.LogDebug("File modified: {FilePath}", e.FullPath);
// Update metadata if needed
await UpdateFileMetadataAsync(e.FullPath);
}
private void OnFileDeleted(object sender, FileSystemEventArgs e)
{
_logger.LogDebug("File deleted: {FilePath}", e.FullPath);
}
private async Task CopyFileAsync(string source, string destination)
{
using var sourceStream = File.OpenRead(source);
using var destinationStream = File.Create(destination);
await sourceStream.CopyToAsync(destinationStream);
}
private async Task BackupFileAsync(string filePath)
{
if (string.IsNullOrEmpty(_options.BackupDirectory))
return;
var backupDir = Path.Combine(_options.RootPath, _options.BackupDirectory);
Directory.CreateDirectory(backupDir);
var fileName = Path.GetFileName(filePath);
var backupPath = Path.Combine(backupDir, $"{DateTime.UtcNow:yyyyMMdd_HHmmss}_{fileName}");
await CopyFileAsync(filePath, backupPath);
}
private async Task CleanupEmptyDirectoriesAsync(string path)
{
foreach (var directory in Directory.GetDirectories(path))
{
await CleanupEmptyDirectoriesAsync(directory);
if (!Directory.EnumerateFileSystemEntries(directory).Any())
{
try
{
Directory.Delete(directory);
_logger.LogDebug("Deleted empty directory: {DirectoryPath}", directory);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete empty directory: {DirectoryPath}", directory);
}
}
}
}
private string GetMetadataPath(string fileName)
{
return Path.Combine(_options.RootPath, $"{fileName}.metadata");
}
private async Task<Dictionary<string, string>> ReadMetadataAsync(string metadataPath)
{
var json = await File.ReadAllTextAsync(metadataPath);
return JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
}
private async Task UpdateFileMetadataAsync(string filePath)
{
// Update LastModified timestamp in metadata
var relativePath = Path.GetRelativePath(_options.RootPath, filePath);
var metadataPath = GetMetadataPath(relativePath);
if (File.Exists(metadataPath))
{
var metadata = await ReadMetadataAsync(metadataPath);
metadata["LastModified"] = DateTime.UtcNow.ToString("O");
var json = JsonSerializer.Serialize(metadata, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(metadataPath, json);
}
}
private async Task<string> ComputeChecksumAsync(Stream stream)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hash = await sha256.ComputeHashAsync(stream);
return Convert.ToBase64String(hash);
}
}
public class BatchOperationResult
{
public int TotalFiles { get; set; }
public int SuccessfulOperations { get; set; }
public int FailedOperations { get; set; }
public List<BatchOperationItem> Results { get; set; }
}
public class BatchOperationItem
{
public string FileName { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
}
public class CleanupResult
{
public int DeletedFiles { get; set; }
public int FailedDeletions { get; set; }
public long FreedSpace { get; set; }
public string FormattedFreedSpace => FormatBytes(FreedSpace);
private static string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
public class IntegrityCheckResult
{
public string FileName { get; set; }
public bool IsValid { get; set; }
public bool IsReadable { get; set; }
public bool ChecksumValid { get; set; }
public long FileSize { get; set; }
public List<string> Issues { get; set; } = new();
}
public class LocalStorageTestUtilities
{
private readonly IBlobService _blobService;
private readonly LocalDirectoryOptions _options;
public LocalStorageTestUtilities(IBlobService blobService, IOptions<LocalDirectoryOptions> options)
{
_blobService = blobService;
_options = options.Value;
}
// Create test data for development/testing
public async Task SeedTestDataAsync()
{
var testFiles = new[]
{
new { Name = "test1/documents/sample.pdf", Content = "Sample PDF content", Type = "application/pdf" },
new { Name = "test1/images/photo.jpg", Content = "Sample JPEG content", Type = "image/jpeg" },
new { Name = "test2/documents/report.docx", Content = "Sample DOCX content", Type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }
};
foreach (var testFile in testFiles)
{
var content = Encoding.UTF8.GetBytes(testFile.Content);
using var stream = new MemoryStream(content);
var blobData = new BlobData
{
FileName = testFile.Name,
ContentType = testFile.Type,
Content = stream,
Metadata = new Dictionary<string, string>
{
["CreatedBy"] = "TestDataSeeder",
["CreatedAt"] = DateTime.UtcNow.ToString("O"),
["IsTestData"] = "true"
}
};
await _blobService.SaveAsync(blobData);
}
}
// Clean up test data
public async Task CleanupTestDataAsync()
{
var testFiles = await _blobService.ListAsync("");
foreach (var file in testFiles)
{
if (file.Metadata?.GetValueOrDefault("IsTestData") == "true")
{
await _blobService.DeleteAsync(file.FileName);
}
}
}
// Create temporary directory for testing
public string CreateTempDirectory()
{
var tempPath = Path.Combine(_options.RootPath, "temp", Guid.NewGuid().ToString());
Directory.CreateDirectory(tempPath);
return tempPath;
}
// Verify storage setup
public async Task<StorageHealthCheck> VerifyStorageHealthAsync()
{
var health = new StorageHealthCheck();
try
{
// Check if root directory exists and is writable
if (!Directory.Exists(_options.RootPath))
{
health.Issues.Add($"Root directory does not exist: {_options.RootPath}");
}
else
{
// Test write access
var testFile = Path.Combine(_options.RootPath, $"test_{Guid.NewGuid()}.tmp");
try
{
await File.WriteAllTextAsync(testFile, "test");
File.Delete(testFile);
health.IsWritable = true;
}
catch (Exception ex)
{
health.Issues.Add($"No write access: {ex.Message}");
}
// Check available space
var drive = new DriveInfo(Path.GetPathRoot(_options.RootPath));
health.AvailableSpace = drive.AvailableFreeSpace;
health.TotalSpace = drive.TotalSize;
if (health.AvailableSpace < 100 * 1024 * 1024) // Less than 100MB
{
health.Issues.Add("Low disk space available");
}
}
health.IsHealthy = health.Issues.Count == 0;
}
catch (Exception ex)
{
health.Issues.Add($"Health check failed: {ex.Message}");
}
return health;
}
}
public class StorageHealthCheck
{
public bool IsHealthy { get; set; }
public bool IsWritable { get; set; }
public long AvailableSpace { get; set; }
public long TotalSpace { get; set; }
public List<string> Issues { get; set; } = new();
public string FormattedAvailableSpace => FormatBytes(AvailableSpace);
public string FormattedTotalSpace => FormatBytes(TotalSpace);
private static string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
public static class CrossPlatformPathHelper
{
public static string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
return path;
// Convert to platform-specific path separators
path = path.Replace('\\', Path.DirectorySeparatorChar)
.Replace('/', Path.DirectorySeparatorChar);
// Handle UNC paths on Windows
if (OperatingSystem.IsWindows() && path.StartsWith($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}"))
{
return path; // UNC path, leave as-is
}
return Path.GetFullPath(path);
}
public static bool IsValidFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return false;
// Check for invalid characters
var invalidChars = Path.GetInvalidFileNameChars();
if (fileName.Any(c => invalidChars.Contains(c)))
return false;
// Check for reserved names on Windows
if (OperatingSystem.IsWindows())
{
var reservedNames = new[] { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName).ToUpperInvariant();
if (reservedNames.Contains(nameWithoutExtension))
return false;
}
return true;
}
public static string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return "file";
var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = new string(fileName.Where(c => !invalidChars.Contains(c)).ToArray());
// Ensure it's not empty after sanitization
if (string.IsNullOrWhiteSpace(sanitized))
return "file";
return sanitized;
}
}
public class NetworkShareStorageService
{
private readonly LocalDirectoryOptions _options;
private readonly ILogger<NetworkShareStorageService> _logger;
public async Task<bool> TestNetworkShareAccessAsync()
{
try
{
if (!_options.RootPath.StartsWith(@"\\"))
return true; // Not a network share
// Test network connectivity
var testFile = Path.Combine(_options.RootPath, $"connectivity_test_{Guid.NewGuid()}.tmp");
await File.WriteAllTextAsync(testFile, "test");
File.Delete(testFile);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Network share access test failed: {RootPath}", _options.RootPath);
return false;
}
}
}
public class DockerVolumeService
{
public static void ConfigureForDocker(LocalDirectoryOptions options)
{
// Configure for Docker volume mounting
options.RootPath = "/app/storage";
options.CreateDirectoryIfNotExists = true;
options.EnableFileWatcher = false; // File watchers don't work well in containers
}
}
// Good: Secure path handling
public static class SecurePathHelper
{
public static string ValidatePath(string basePath, string relativePath)
{
var fullPath = Path.GetFullPath(Path.Combine(basePath, relativePath));
if (!fullPath.StartsWith(basePath))
{
throw new SecurityException("Path traversal attempt detected");
}
return fullPath;
}
}
// Good: File extension validation
services.Configure<LocalDirectoryOptions>(options =>
{
options.AllowedFileExtensions = new[] { ".jpg", ".png", ".pdf", ".docx" };
options.MaxFileSize = 10 * 1024 * 1024; // 10MB
});
// Good: Use streaming for large files
public async Task ProcessLargeFileAsync(Stream fileStream, string fileName)
{
const int bufferSize = 64 * 1024; // 64KB buffer
using var fileWriteStream = File.Create(fileName);
await fileStream.CopyToAsync(fileWriteStream, bufferSize);
}
// Good: Implement caching for metadata
private readonly MemoryCache _metadataCache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 1000
});
// Good: Handle file system exceptions
public async Task<BlobData> SafeGetFileAsync(string fileName)
{
try
{
return await _blobService.GetAsync(fileName);
}
catch (FileNotFoundException)
{
throw new BlobNotFoundException($"File not found: {fileName}");
}
catch (UnauthorizedAccessException)
{
throw new BlobAccessDeniedException($"Access denied: {fileName}");
}
catch (IOException ex)
{
throw new BlobStorageException($"I/O error accessing file: {fileName}", ex);
}
}
DKNet.Svc.BlobStorage.Local integrates seamlessly with other DKNet components:
💡 Development Tip: Use DKNet.Svc.BlobStorage.Local for development, testing, and on-premises scenarios where cloud storage is not needed or available. Always implement proper path validation to prevent directory traversal attacks, use appropriate file permissions, and consider implementing file integrity checks for critical applications. The local adapter is perfect for rapid development iteration and automated testing scenarios.