init
This commit is contained in:
189
OpenArchival.DataAccess.FileAccessManager/FileAccessManager.cs
Normal file
189
OpenArchival.DataAccess.FileAccessManager/FileAccessManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user