This commit is contained in:
2026-05-17 20:54:09 -04:00
parent 6da2183583
commit 74c21ee5cc
3000 changed files with 11794 additions and 15301 deletions

View File

@@ -0,0 +1,189 @@
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenArchival.Blazor.CustomComponents;
namespace OpenArchival.DataAccess.FileAccessManager;
public class FileAccessManager : IFileAccessManager
{
private IDbContextFactory<ApplicationDbContext> _contextFactory { get; set; }
IOptions<FileUploadOptions> _fileUploadOptions { get; set; }
ILogger<FileAccessManager> _logger { get; set; }
private readonly IImageThumbnailer _thumbnailer;
public FileAccessManager(
IDbContextFactory<ApplicationDbContext> contextFactory,
IOptions<FileUploadOptions> fileOptions,
ILogger<FileAccessManager> logger,
IImageThumbnailer thumbnailer
)
{
_contextFactory = contextFactory;
_fileUploadOptions = fileOptions;
_logger = logger;
_thumbnailer = thumbnailer;
}
/// <summary>
/// Queries the file listing from the database based on the id, then deletes the file from the disk and
/// the file listing from the database.
///
/// </summary>
/// <param name="id"></param>
/// <returns>
/// Returns true if the file is deleted successfully
/// returns false if the file cannot be found in the database
/// </returns>
/// <exception cref="IOException">Thrown if there is an IOException while trying to delete the file.</exception>
public async Task<bool> DeleteFileAsync(int id)
{
await using var context = await _contextFactory.CreateDbContextAsync();
FilePathListing? existingListing = await context.ArtifactFilePaths
.Where(listing=>listing.Id == id)
.SingleOrDefaultAsync();
if (existingListing == null)
{
return false;
}
var filename = existingListing.Path;
var smallThumb = existingListing.SmallThumbnailPath;
var largeThumb = existingListing.LargeThumbnailPath;
// Delete from the database before we delete from the disk to avoid someone trying to read it
// before it deletes
await context.ArtifactFilePaths.Where(listing => listing.Id == id).ExecuteDeleteAsync();
try
{
if (File.Exists(filename))
{
File.Delete(filename);
}
if (!string.IsNullOrEmpty(smallThumb) && File.Exists(smallThumb))
{
File.Delete(smallThumb);
}
if (!string.IsNullOrEmpty(largeThumb) && File.Exists(largeThumb))
{
File.Delete(largeThumb);
}
}
catch (IOException e)
{
throw new IOException("Could not delete file(s) from the disk!", e);
}
return true;
}
/// <summary>
/// Uploads the given browser file to the disk and creates a corresponding database entry.
/// </summary>
/// <param name="browserFile"></param>
/// <returns>Returns the FilePathListing object that was inserted to the database</returns>
/// <exception cref="IOException">Throws wrapped IOException if the FileStream cannot be constructed.</exception>
public async Task<FilePathListing> UploadFileAsync(IBrowserFile browserFile)
{
var diskFileName = $"{Guid.NewGuid()}{Path.GetExtension(browserFile.Name)}";
var destinationPath = Path.Combine(_fileUploadOptions.Value.UploadFolderPath, diskFileName);
await using var browserUploadStream = browserFile.OpenReadStream(maxAllowedSize: _fileUploadOptions.Value.MaxUploadSizeBytes);
try {
await using var outFileStream = new FileStream(destinationPath, FileMode.Create);
await browserUploadStream.CopyToAsync(outFileStream);
} catch (IOException e)
{
throw new IOException("Failed to open file stream to write file to disk", e);
}
// Create the new entity
var newFile = new FilePathListing() { Path = destinationPath, OriginalName = Path.GetFileName(browserFile.Name) };
// Generate thumbnails if it is a supported image
if (_thumbnailer.IsSupportedImage(destinationPath))
{
try
{
var result = await _thumbnailer.GenerateThumbnailsAsync(destinationPath);
newFile.SmallThumbnailPath = result.SmallThumbnailPath;
newFile.LargeThumbnailPath = result.LargeThumbnailPath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate thumbnails for {Path}", destinationPath);
// We don't throw here, as we still want to save the original file
}
}
await using var context = await _contextFactory.CreateDbContextAsync();
context.ArtifactFilePaths.Add(newFile);
// Save the file entry
await context.SaveChangesAsync();
return newFile;
}
public async Task<FilePathListing?> GetFile(int id)
{
await using var context = await _contextFactory.CreateDbContextAsync();
FilePathListing? existingListing = await context.ArtifactFilePaths.FirstOrDefaultAsync(a => a.Id == id);
return existingListing;
}
public async Task<(int Processed, int Failed)> BackfillThumbnailsAsync()
{
await using var context = await _contextFactory.CreateDbContextAsync();
var listingsToProcess = await context.ArtifactFilePaths
.Where(f => string.IsNullOrEmpty(f.SmallThumbnailPath) || string.IsNullOrEmpty(f.LargeThumbnailPath))
.ToListAsync();
int processed = 0;
int failed = 0;
foreach (var listing in listingsToProcess)
{
if (!File.Exists(listing.Path))
{
_logger.LogWarning("File not found on disk during backfill: {Path} (ID: {Id})", listing.Path, listing.Id);
failed++;
continue;
}
if (_thumbnailer.IsSupportedImage(listing.Path))
{
try
{
var result = await _thumbnailer.GenerateThumbnailsAsync(listing.Path);
listing.SmallThumbnailPath = result.SmallThumbnailPath;
listing.LargeThumbnailPath = result.LargeThumbnailPath;
processed++;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate thumbnails during backfill for {Path} (ID: {Id})", listing.Path, listing.Id);
failed++;
}
}
}
if (processed > 0)
{
await context.SaveChangesAsync();
}
return (processed, failed);
}
}