190 lines
6.7 KiB
C#
190 lines
6.7 KiB
C#
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);
|
|
}
|
|
}
|