using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using DysonNetwork.Drive.Data; using DysonNetwork.Drive.Extensions; using DysonNetwork.Drive.Interfaces; using DysonNetwork.Drive.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NodaTime; namespace DysonNetwork.Drive.Services; public class FileService : IFileService, IDisposable { private readonly ILogger _logger; private readonly AppDatabase _dbContext; private readonly IClock _clock; private bool _disposed = false; public FileService(AppDatabase dbContext, IClock clock, ILogger logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task GetFileAsync(Guid fileId, CancellationToken cancellationToken = default) { var file = await _dbContext.Files .AsNoTracking() .FirstOrDefaultAsync(f => f.Id == fileId, cancellationToken); if (file == null) { throw new FileNotFoundException($"File with ID {fileId} not found."); } return file; } public async Task DownloadFileAsync(Guid fileId, CancellationToken cancellationToken = default) { var file = await GetFileAsync(fileId, cancellationToken); // In a real implementation, this would stream the file from storage (e.g., S3, local filesystem) // For now, we'll return a MemoryStream with a placeholder var placeholder = $"This is a placeholder for file {fileId} with name {file.Name}"; var memoryStream = new MemoryStream(); var writer = new StreamWriter(memoryStream); await writer.WriteAsync(placeholder); await writer.FlushAsync(); memoryStream.Position = 0; return memoryStream; } public async Task UploadFileAsync( Stream fileStream, string fileName, string contentType, IDictionary? metadata = null, CancellationToken cancellationToken = default) { if (fileStream == null) throw new ArgumentNullException(nameof(fileStream)); if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentNullException(nameof(fileName)); if (string.IsNullOrWhiteSpace(contentType)) throw new ArgumentNullException(nameof(contentType)); // In a real implementation, this would upload to a storage service var now = _clock.GetCurrentInstant(); var file = new CloudFile { Id = Guid.NewGuid(), Name = Path.GetFileName(fileName), OriginalName = fileName, MimeType = contentType, Size = fileStream.Length, StoragePath = $"uploads/{now.ToUnixTimeMilliseconds()}/{Guid.NewGuid()}/{Path.GetFileName(fileName)}", StorageProvider = "local", // or "s3", "azure", etc. CreatedAt = now.ToDateTimeOffset(), IsPublic = false, IsTemporary = false, IsDeleted = false }; if (metadata != null) { file.Metadata = System.Text.Json.JsonSerializer.Serialize(metadata); } _dbContext.Files.Add(file); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Uploaded file {FileId} with name {FileName}", file.Id, file.Name); return file; } public async Task DeleteFileAsync(Guid fileId, CancellationToken cancellationToken = default) { var file = await _dbContext.Files.FindAsync(new object[] { fileId }, cancellationToken); if (file == null) { return false; } // In a real implementation, this would also delete the file from storage file.IsDeleted = true; file.DeletedAt = _clock.GetCurrentInstant(); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Soft-deleted file {FileId}", fileId); return true; } public async Task UpdateFileMetadataAsync(Guid fileId, IDictionary metadata, CancellationToken cancellationToken = default) { var file = await GetFileAsync(fileId, cancellationToken); file.Metadata = System.Text.Json.JsonSerializer.Serialize(metadata); var now = _clock.GetCurrentInstant(); file.UpdatedAt = new DateTimeOffset(now.ToDateTimeUtc(), TimeSpan.Zero); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Updated metadata for file {FileId}", fileId); return file; } public Task FileExistsAsync(Guid fileId, CancellationToken cancellationToken = default) { return _dbContext.Files .AsNoTracking() .AnyAsync(f => f.Id == fileId && !f.IsDeleted, cancellationToken); } public Task GetFileUrlAsync(Guid fileId, TimeSpan? expiry = null, CancellationToken cancellationToken = default) { // In a real implementation, this would generate a signed URL with the specified expiry return Task.FromResult($"https://storage.dyson.network/files/{fileId}"); } public Task GetFileThumbnailUrlAsync(Guid fileId, int? width = null, int? height = null, TimeSpan? expiry = null, CancellationToken cancellationToken = default) { // In a real implementation, this would generate a signed thumbnail URL var size = width.HasValue || height.HasValue ? $"_{width ?? 0}x{height ?? 0}" : string.Empty; return Task.FromResult($"https://storage.dyson.network/thumbnails/{fileId}{size}"); } public async Task CopyFileAsync(Guid sourceFileId, string? newName = null, IDictionary? newMetadata = null, CancellationToken cancellationToken = default) { var sourceFile = await GetFileAsync(sourceFileId, cancellationToken); var newFile = new CloudFile { Id = Guid.NewGuid(), Name = newName ?? sourceFile.Name, OriginalName = sourceFile.OriginalName, MimeType = sourceFile.MimeType, Size = sourceFile.Size, StoragePath = $"copies/{_clock.GetCurrentInstant().ToUnixTimeMilliseconds()}/{Guid.NewGuid()}/{sourceFile.Name}", StorageProvider = sourceFile.StorageProvider, ContentHash = sourceFile.ContentHash, ThumbnailPath = sourceFile.ThumbnailPath, PreviewPath = sourceFile.PreviewPath, Width = sourceFile.Width, Height = sourceFile.Height, Duration = sourceFile.Duration, Metadata = newMetadata != null ? System.Text.Json.JsonSerializer.Serialize(newMetadata) : sourceFile.Metadata, IsPublic = sourceFile.IsPublic, IsTemporary = sourceFile.IsTemporary, IsDeleted = false, ExpiresAt = sourceFile.ExpiresAt, UploadedById = sourceFile.UploadedById, UploadedByType = sourceFile.UploadedByType, CreatedAt = _clock.GetCurrentInstant().ToDateTimeOffset(), UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset() }; _dbContext.Files.Add(newFile); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Copied file {SourceFileId} to {NewFileId}", sourceFileId, newFile.Id); return newFile; } public async Task MoveFileAsync(Guid sourceFileId, string? newName = null, IDictionary? newMetadata = null, CancellationToken cancellationToken = default) { var sourceFile = await GetFileAsync(sourceFileId, cancellationToken); // In a real implementation, this would move the file in storage var newPath = $"moved/{_clock.GetCurrentInstant().ToUnixTimeMilliseconds()}/{Guid.NewGuid()}/{newName ?? sourceFile.Name}"; sourceFile.Name = newName ?? sourceFile.Name; sourceFile.StoragePath = newPath; sourceFile.UpdatedAt = _clock.GetCurrentInstant(); if (newMetadata != null) { sourceFile.Metadata = System.Text.Json.JsonSerializer.Serialize(newMetadata); } await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Moved file {FileId} to {NewPath}", sourceFileId, newPath); return sourceFile; } public async Task RenameFileAsync(Guid fileId, string newName, CancellationToken cancellationToken = default) { var file = await GetFileAsync(fileId, cancellationToken); file.Name = newName; file.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset(); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Renamed file {FileId} to {NewName}", fileId, newName); return file; } public async Task GetFileSizeAsync(Guid fileId, CancellationToken cancellationToken = default) { var file = await GetFileAsync(fileId, cancellationToken); return file.Size; } public async Task GetFileHashAsync(Guid fileId, CancellationToken cancellationToken = default) { var file = await GetFileAsync(fileId, cancellationToken); if (string.IsNullOrEmpty(file.ContentHash)) { // In a real implementation, this would compute the hash of the file content file.ContentHash = Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..16]; await _dbContext.SaveChangesAsync(cancellationToken); } return file.ContentHash; } public async Task GetFileThumbnailAsync(Guid fileId, int? width = null, int? height = null, CancellationToken cancellationToken = default) { // In a real implementation, this would generate or retrieve a thumbnail var placeholder = $"This is a thumbnail for file {fileId} with size {width ?? 0}x{height ?? 0}"; var memoryStream = new MemoryStream(); var writer = new StreamWriter(memoryStream); await writer.WriteAsync(placeholder); await writer.FlushAsync(); memoryStream.Position = 0; return memoryStream; } public async Task SetFileVisibilityAsync(Guid fileId, bool isPublic, CancellationToken cancellationToken = default) { var file = await GetFileAsync(fileId, cancellationToken); if (file.IsPublic != isPublic) { file.IsPublic = isPublic; file.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset(); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Set visibility of file {FileId} to {Visibility}", fileId, isPublic ? "public" : "private"); } return file; } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _dbContext?.Dispose(); } _disposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }