♻️ Extract the Storage service to DysonNetwork.Drive microservice

This commit is contained in:
2025-07-06 17:29:26 +08:00
parent 6a3d04af3d
commit 14b79f16f4
71 changed files with 2629 additions and 346 deletions

View File

@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using DysonNetwork.Drive.Data;
using DysonNetwork.Drive.Interfaces;
using DysonNetwork.Drive.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NodaTime;
namespace DysonNetwork.Drive.Services;
public class FileReferenceService : IFileReferenceService, IDisposable
{
private readonly AppDatabase _dbContext;
private readonly IFileService _fileService;
private readonly IClock _clock;
private readonly ILogger<FileReferenceService> _logger;
private bool _disposed = false;
public FileReferenceService(
AppDatabase dbContext,
IFileService fileService,
IClock clock,
ILogger<FileReferenceService> logger)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_fileService = fileService ?? throw new ArgumentNullException(nameof(fileService));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<CloudFileReference> CreateReferenceAsync(
Guid fileId,
string resourceId,
string resourceType,
string referenceType,
string? referenceId = null,
string? referenceName = null,
string? referenceMimeType = null,
long? referenceSize = null,
string? referenceUrl = null,
string? referenceThumbnailUrl = null,
string? referencePreviewUrl = null,
string? referenceMetadata = null,
IDictionary<string, object>? metadata = null,
CancellationToken cancellationToken = default)
{
// Verify file exists
var fileExists = await _fileService.FileExistsAsync(fileId, cancellationToken);
if (!fileExists)
{
throw new FileNotFoundException($"File with ID {fileId} not found.");
}
var reference = new CloudFileReference
{
Id = Guid.NewGuid(),
FileId = fileId,
ResourceId = resourceId,
ResourceType = resourceType,
ReferenceType = referenceType,
ReferenceId = referenceId,
ReferenceName = referenceName,
ReferenceMimeType = referenceMimeType,
ReferenceSize = referenceSize,
ReferenceUrl = referenceUrl,
ReferenceThumbnailUrl = referenceThumbnailUrl,
ReferencePreviewUrl = referencePreviewUrl,
ReferenceMetadata = referenceMetadata,
IsActive = true,
CreatedAt = _clock.GetCurrentInstant().ToDateTimeOffset()
};
if (metadata != null && metadata.Any())
{
var options = new JsonSerializerOptions { WriteIndented = true };
reference.Metadata = JsonDocument.Parse(JsonSerializer.Serialize(metadata, options));
}
_dbContext.FileReferences.Add(reference);
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Created reference {ReferenceId} for file {FileId} to resource {ResourceType}/{ResourceId}",
reference.Id, fileId, resourceType, resourceId);
return reference;
}
public async Task<CloudFileReference> GetReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default)
{
var reference = await _dbContext.FileReferences
.AsNoTracking()
.Include(r => r.File)
.FirstOrDefaultAsync(r => r.Id == referenceId, cancellationToken);
if (reference == null)
{
throw new KeyNotFoundException($"Reference with ID {referenceId} not found.");
}
return reference;
}
public async Task<IEnumerable<CloudFileReference>> GetReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default)
{
return await _dbContext.FileReferences
.AsNoTracking()
.Where(r => r.FileId == fileId && r.IsActive)
.ToListAsync(cancellationToken);
}
public async Task<IEnumerable<CloudFileReference>> GetReferencesForResourceAsync(
string resourceId,
string resourceType,
CancellationToken cancellationToken = default)
{
return await _dbContext.FileReferences
.AsNoTracking()
.Where(r => r.ResourceId == resourceId &&
r.ResourceType == resourceType &&
r.IsActive)
.ToListAsync(cancellationToken);
}
public async Task<IEnumerable<CloudFileReference>> GetReferencesOfTypeAsync(
string referenceType,
CancellationToken cancellationToken = default)
{
return await _dbContext.FileReferences
.AsNoTracking()
.Where(r => r.ReferenceType == referenceType && r.IsActive)
.ToListAsync(cancellationToken);
}
public async Task<bool> DeleteReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default)
{
var reference = await _dbContext.FileReferences
.FirstOrDefaultAsync(r => r.Id == referenceId, cancellationToken);
if (reference == null)
{
return false;
}
reference.IsActive = false;
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Deleted reference {ReferenceId}", referenceId);
return true;
}
public async Task<int> DeleteReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default)
{
var references = await _dbContext.FileReferences
.Where(r => r.FileId == fileId && r.IsActive)
.ToListAsync(cancellationToken);
foreach (var reference in references)
{
reference.IsActive = false;
}
var count = await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Deleted {Count} references for file {FileId}", count, fileId);
return count;
}
public async Task<int> DeleteReferencesForResourceAsync(
string resourceId,
string resourceType,
CancellationToken cancellationToken = default)
{
var references = await _dbContext.FileReferences
.Where(r => r.ResourceId == resourceId &&
r.ResourceType == resourceType &&
r.IsActive)
.ToListAsync(cancellationToken);
foreach (var reference in references)
{
reference.IsActive = false;
}
var count = await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Deleted {Count} references for resource {ResourceType}/{ResourceId}",
count, resourceType, resourceId);
return count;
}
public async Task<CloudFileReference> UpdateReferenceMetadataAsync(
Guid referenceId,
IDictionary<string, object> metadata,
CancellationToken cancellationToken = default)
{
var reference = await GetReferenceAsync(referenceId, cancellationToken);
var options = new JsonSerializerOptions { WriteIndented = true };
reference.Metadata = JsonDocument.Parse(JsonSerializer.Serialize(metadata, options));
reference.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset();
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Updated metadata for reference {ReferenceId}", referenceId);
return reference;
}
public async Task<bool> ReferenceExistsAsync(Guid referenceId, CancellationToken cancellationToken = default)
{
return await _dbContext.FileReferences
.AsNoTracking()
.AnyAsync(r => r.Id == referenceId && r.IsActive, cancellationToken);
}
public async Task<bool> HasReferenceAsync(
Guid fileId,
string resourceId,
string resourceType,
string? referenceType = null,
CancellationToken cancellationToken = default)
{
var query = _dbContext.FileReferences
.AsNoTracking()
.Where(r => r.FileId == fileId &&
r.ResourceId == resourceId &&
r.ResourceType == resourceType &&
r.IsActive);
if (!string.IsNullOrEmpty(referenceType))
{
query = query.Where(r => r.ReferenceType == referenceType);
}
return await query.AnyAsync(cancellationToken);
}
public async Task<CloudFileReference> UpdateReferenceResourceAsync(
Guid referenceId,
string newResourceId,
string newResourceType,
CancellationToken cancellationToken = default)
{
var reference = await GetReferenceAsync(referenceId, cancellationToken);
reference.ResourceId = newResourceId;
reference.ResourceType = newResourceType;
reference.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset();
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Updated reference {ReferenceId} to point to resource {ResourceType}/{ResourceId}",
referenceId, newResourceType, newResourceId);
return reference;
}
public async Task<IEnumerable<CloudFile>> GetFilesForResourceAsync(
string resourceId,
string resourceType,
string? referenceType = null,
CancellationToken cancellationToken = default)
{
var query = _dbContext.FileReferences
.AsNoTracking()
.Include(r => r.File)
.Where(r => r.ResourceId == resourceId &&
r.ResourceType == resourceType &&
r.IsActive);
if (!string.IsNullOrEmpty(referenceType))
{
query = query.Where(r => r.ReferenceType == referenceType);
}
var references = await query.ToListAsync(cancellationToken);
return references.Select(r => r.File!);
}
public async Task<IEnumerable<CloudFile>> GetFilesForReferenceTypeAsync(
string referenceType,
CancellationToken cancellationToken = default)
{
var references = await _dbContext.FileReferences
.AsNoTracking()
.Include(r => r.File)
.Where(r => r.ReferenceType == referenceType && r.IsActive)
.ToListAsync(cancellationToken);
return references.Select(r => r.File!);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_dbContext?.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,301 @@
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<FileService> _logger;
private readonly AppDatabase _dbContext;
private readonly IClock _clock;
private bool _disposed = false;
public FileService(AppDatabase dbContext, IClock clock, ILogger<FileService> 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<CloudFile> 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<Stream> 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<CloudFile> UploadFileAsync(
Stream fileStream,
string fileName,
string contentType,
IDictionary<string, string>? 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<bool> 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<CloudFile> UpdateFileMetadataAsync(Guid fileId, IDictionary<string, string> 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<bool> FileExistsAsync(Guid fileId, CancellationToken cancellationToken = default)
{
return _dbContext.Files
.AsNoTracking()
.AnyAsync(f => f.Id == fileId && !f.IsDeleted, cancellationToken);
}
public Task<string> 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<string> 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<CloudFile> CopyFileAsync(Guid sourceFileId, string? newName = null, IDictionary<string, string>? 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<CloudFile> MoveFileAsync(Guid sourceFileId, string? newName = null, IDictionary<string, string>? 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<CloudFile> 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<long> GetFileSizeAsync(Guid fileId, CancellationToken cancellationToken = default)
{
var file = await GetFileAsync(fileId, cancellationToken);
return file.Size;
}
public async Task<string> 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<Stream> 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<CloudFile> 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);
}
}

View File

@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace DysonNetwork.Drive.Services;
public interface ICacheService
{
Task<T?> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, System.TimeSpan? expiry = null);
Task RemoveAsync(string key);
Task<bool> ExistsAsync(string key);
Task<long> IncrementAsync(string key, long value = 1);
Task<long> DecrementAsync(string key, long value = 1);
}