302 lines
11 KiB
C#
302 lines
11 KiB
C#
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);
|
|
}
|
|
}
|