[backend/drive] Add several media processing configuration options, replacing constants & assumptions
This commit is contained in:
parent
9a662f2bea
commit
744a01d138
6 changed files with 178 additions and 30 deletions
|
@ -73,10 +73,10 @@ public class MediaController(DriveService driveSvc, DatabaseContext db) : Contro
|
|||
{
|
||||
Id = file.Id,
|
||||
Type = AttachmentEntity.GetType(file.Type),
|
||||
Url = file.Url,
|
||||
Url = file.PublicUrl,
|
||||
Blurhash = file.Blurhash,
|
||||
Description = file.Comment,
|
||||
PreviewUrl = file.ThumbnailUrl,
|
||||
PreviewUrl = file.PublicThumbnailUrl,
|
||||
RemoteUrl = file.Uri,
|
||||
Sensitive = file.IsSensitive,
|
||||
//Metadata = TODO,
|
||||
|
|
|
@ -79,6 +79,8 @@ public sealed class Config
|
|||
public sealed class StorageSection
|
||||
{
|
||||
public readonly TimeSpan? MediaRetentionTimeSpan;
|
||||
public readonly int? MaxCacheSizeBytes;
|
||||
public readonly int? MaxUploadSizeBytes;
|
||||
|
||||
public bool CleanAvatars = false;
|
||||
public bool CleanBanners = false;
|
||||
|
@ -125,10 +127,69 @@ public sealed class Config
|
|||
}
|
||||
}
|
||||
|
||||
public bool EnableLibVips { get; init; }
|
||||
public string? MaxUploadSize
|
||||
{
|
||||
get => MaxUploadSizeBytes?.ToString();
|
||||
init
|
||||
{
|
||||
if (value == null || string.IsNullOrWhiteSpace(value) || value.Trim() == "0" || value.Trim() == "-1")
|
||||
{
|
||||
MaxUploadSizeBytes = null;
|
||||
return;
|
||||
}
|
||||
|
||||
public LocalStorageSection? Local { get; init; }
|
||||
public ObjectStorageSection? ObjectStorage { get; init; }
|
||||
var hasSuffix = !char.IsAsciiDigit(value.Trim()[^1]);
|
||||
var substr = hasSuffix ? value.Trim()[..^1] : value.Trim();
|
||||
|
||||
if (!int.TryParse(substr, out var num))
|
||||
throw new Exception("Invalid max upload size");
|
||||
|
||||
char? suffix = hasSuffix ? value.Trim()[^1] : null;
|
||||
|
||||
MaxUploadSizeBytes = suffix switch
|
||||
{
|
||||
null => num,
|
||||
'k' or 'K' => num * 1024,
|
||||
'm' or 'M' => num * 1024 * 1024,
|
||||
'g' or 'G' => num * 1024 * 1024 * 1024,
|
||||
_ => throw new Exception("Unsupported suffix, use one of: [K]ilobytes, [M]egabytes, [G]igabytes")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string? MaxCacheSize
|
||||
{
|
||||
get => MaxCacheSizeBytes?.ToString();
|
||||
init
|
||||
{
|
||||
if (value == null || string.IsNullOrWhiteSpace(value) || value.Trim() == "0" || value.Trim() == "-1")
|
||||
{
|
||||
MaxCacheSizeBytes = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var hasSuffix = !char.IsAsciiDigit(value.Trim()[^1]);
|
||||
var substr = hasSuffix ? value.Trim()[..^1] : value.Trim();
|
||||
|
||||
if (!int.TryParse(substr, out var num))
|
||||
throw new Exception("Invalid max cache size");
|
||||
|
||||
char? suffix = hasSuffix ? value.Trim()[^1] : null;
|
||||
|
||||
MaxCacheSizeBytes = suffix switch
|
||||
{
|
||||
null => num,
|
||||
'k' or 'K' => num * 1024,
|
||||
'm' or 'M' => num * 1024 * 1024,
|
||||
'g' or 'G' => num * 1024 * 1024 * 1024,
|
||||
_ => throw new Exception("Unsupported suffix, use one of: [K]ilobytes, [M]egabytes, [G]igabytes")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public LocalStorageSection? Local { get; init; }
|
||||
public ObjectStorageSection? ObjectStorage { get; init; }
|
||||
public MediaProcessingSection MediaProcessing { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class LocalStorageSection
|
||||
|
@ -148,4 +209,42 @@ public sealed class Config
|
|||
public string? SetAcl { get; init; }
|
||||
public bool DisableValidation { get; init; } = false;
|
||||
}
|
||||
|
||||
public sealed class MediaProcessingSection
|
||||
{
|
||||
public readonly int MaxFileSizeBytes = 10 * 1024 * 1024;
|
||||
public Enums.ImageProcessor ImageProcessor { get; init; } = Enums.ImageProcessor.ImageSharp;
|
||||
public int MaxResolutionMpx { get; init; } = 30;
|
||||
public bool LocalOnly { get; init; } = false;
|
||||
|
||||
public string MaxFileSize
|
||||
{
|
||||
get => MaxFileSizeBytes.ToString();
|
||||
init
|
||||
{
|
||||
if (value == null || string.IsNullOrWhiteSpace(value) || value.Trim() == "0" || value.Trim() == "-1")
|
||||
{
|
||||
throw new
|
||||
Exception("Invalid max file size, to disable media processing set ImageProcessor to 'None'");
|
||||
}
|
||||
|
||||
var hasSuffix = !char.IsAsciiDigit(value.Trim()[^1]);
|
||||
var substr = hasSuffix ? value.Trim()[..^1] : value.Trim();
|
||||
|
||||
if (!int.TryParse(substr, out var num))
|
||||
throw new Exception("Invalid max file size");
|
||||
|
||||
char? suffix = hasSuffix ? value.Trim()[^1] : null;
|
||||
|
||||
MaxFileSizeBytes = suffix switch
|
||||
{
|
||||
null => num,
|
||||
'k' or 'K' => num * 1024,
|
||||
'm' or 'M' => num * 1024 * 1024,
|
||||
'g' or 'G' => num * 1024 * 1024 * 1024,
|
||||
_ => throw new Exception("Unsupported suffix, use one of: [K]ilobytes, [M]egabytes, [G]igabytes")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,4 +35,11 @@ public static class Enums
|
|||
Restricted = 2,
|
||||
Public = 3
|
||||
}
|
||||
|
||||
public enum ImageProcessor
|
||||
{
|
||||
ImageSharp,
|
||||
LibVips,
|
||||
None
|
||||
}
|
||||
}
|
|
@ -106,6 +106,9 @@ public class DriveService(
|
|||
|
||||
public async Task<DriveFile> StoreFile(Stream input, User user, DriveFileCreationRequest request)
|
||||
{
|
||||
if (user.Host == null && input.Length > storageConfig.Value.MaxUploadSizeBytes)
|
||||
throw GracefulException.UnprocessableEntity("Attachment is too large.");
|
||||
|
||||
await using var data = new BufferedStream(input);
|
||||
var digest = await DigestHelpers.Sha256DigestAsync(data);
|
||||
logger.LogDebug("Storing file {digest} for user {userId}", digest, user.Id);
|
||||
|
@ -131,21 +134,26 @@ public class DriveService(
|
|||
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var shouldStore = storageConfig.Value.MediaRetention != null || user.Host == null;
|
||||
var storedInternal = storageConfig.Value.Provider == Enums.FileStorage.Local;
|
||||
|
||||
var shouldCache =
|
||||
storageConfig.Value is { MediaRetentionTimeSpan: not null, MediaProcessing.LocalOnly: false } &&
|
||||
data.Length <= storageConfig.Value.MaxCacheSizeBytes;
|
||||
|
||||
var shouldStore = user.Host == null || shouldCache;
|
||||
|
||||
if (request.Uri == null && user.Host != null)
|
||||
throw GracefulException.UnprocessableEntity("Refusing to store file without uri for remote user");
|
||||
|
||||
string? blurhash = null;
|
||||
|
||||
DriveFile.FileProperties? properties = null;
|
||||
var properties = new DriveFile.FileProperties();
|
||||
|
||||
string url;
|
||||
string? thumbnailUrl = null;
|
||||
string? webpublicUrl = null;
|
||||
|
||||
var isReasonableSize = data.Length < 10 * 1024 * 1024; // skip images larger than 10MB
|
||||
var isReasonableSize = data.Length < storageConfig.Value.MediaProcessing.MaxFileSizeBytes;
|
||||
var isImage = request.MimeType.StartsWith("image/") || request.MimeType == "image";
|
||||
var filename = GenerateFilenameKeepingExtension(request.Filename);
|
||||
|
||||
|
@ -158,6 +166,7 @@ public class DriveService(
|
|||
{
|
||||
var genWebp = user.Host == null;
|
||||
var res = await imageProcessor.ProcessImage(data, request, true, genWebp);
|
||||
properties = res?.Properties ?? properties;
|
||||
|
||||
blurhash = res?.Blurhash;
|
||||
thumbnailFilename = res?.RenderThumbnail != null ? GenerateWebpFilename("thumbnail-") : null;
|
||||
|
@ -245,7 +254,6 @@ public class DriveService(
|
|||
}
|
||||
else
|
||||
{
|
||||
//TODO: check against file size limit
|
||||
if (storedInternal)
|
||||
{
|
||||
var pathBase = storageConfig.Value.Local?.Path ??
|
||||
|
@ -290,7 +298,7 @@ public class DriveService(
|
|||
RequestHeaders = request.RequestHeaders,
|
||||
RequestIp = request.RequestIp,
|
||||
Blurhash = blurhash,
|
||||
Properties = properties!,
|
||||
Properties = properties,
|
||||
ThumbnailUrl = thumbnailUrl,
|
||||
ThumbnailAccessKey = thumbnailFilename,
|
||||
WebpublicType = webpublicUrl != null ? "image/webp" : null,
|
||||
|
|
|
@ -18,24 +18,27 @@ namespace Iceshrimp.Backend.Core.Services;
|
|||
|
||||
public class ImageProcessor
|
||||
{
|
||||
private readonly ILogger<ImageProcessor> _logger;
|
||||
private readonly ILogger<ImageProcessor> _logger;
|
||||
private readonly IOptionsMonitor<Config.StorageSection> _config;
|
||||
|
||||
#if EnableLibVips
|
||||
private readonly IOptions<Config.StorageSection> _config;
|
||||
#endif
|
||||
|
||||
public ImageProcessor(ILogger<ImageProcessor> logger, IOptions<Config.StorageSection> config)
|
||||
public ImageProcessor(ILogger<ImageProcessor> logger, IOptionsMonitor<Config.StorageSection> config)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
|
||||
if (config.CurrentValue.MediaProcessing.ImageProcessor == Enums.ImageProcessor.None)
|
||||
{
|
||||
_logger.LogInformation("Image processing is disabled as per the configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator =
|
||||
MemoryAllocator.Create(new MemoryAllocatorOptions { AllocationLimitMegabytes = 20 });
|
||||
|
||||
#if EnableLibVips
|
||||
_config = config;
|
||||
if (!_config.Value.EnableLibVips)
|
||||
if (_config.CurrentValue.MediaProcessing.ImageProcessor != Enums.ImageProcessor.LibVips)
|
||||
{
|
||||
_logger.LogInformation("VIPS support was enabled at compile time, but is not enabled in the configuration, skipping VIPS init");
|
||||
_logger.LogDebug("VIPS support was enabled at compile time, but is not enabled in the configuration, skipping VIPS init");
|
||||
_logger.LogInformation("Using ImageSharp for image processing.");
|
||||
return;
|
||||
}
|
||||
|
@ -55,9 +58,9 @@ public class ImageProcessor
|
|||
VipsLogDelegate);
|
||||
_logger.LogInformation("Using VIPS for image processing.");
|
||||
#else
|
||||
if (config.Value.EnableLibVips)
|
||||
if (config.CurrentValue.MediaProcessing.ImageProcessor == Enums.ImageProcessor.LibVips)
|
||||
{
|
||||
_logger.LogWarning("VIPS support was disabled at compile time, but EnableLibVips is set in the configuration. Either compile with -p:EnableLibVips=true, or disable EnableLibVips in the configuration.");
|
||||
_logger.LogWarning("VIPS support was disabled at compile time, but ImageProcessor is set to LibVips in the configuration. Either compile with -p:EnableLibVips=true, or set the ImageProcessor configuration option to something else.");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -91,6 +94,12 @@ public class ImageProcessor
|
|||
if (request.MimeType == "image" && ident.Metadata.DecodedImageFormat?.DefaultMimeType != null)
|
||||
request.MimeType = ident.Metadata.DecodedImageFormat.DefaultMimeType;
|
||||
|
||||
if (_config.CurrentValue.MediaProcessing.ImageProcessor == Enums.ImageProcessor.None)
|
||||
{
|
||||
var props = new DriveFile.FileProperties { Width = ident.Size.Width, Height = ident.Size.Height };
|
||||
return new Result { Properties = props };
|
||||
}
|
||||
|
||||
// Don't generate thumb/webp for animated images
|
||||
if (ident.FrameMetadataCollection.Count != 0)
|
||||
{
|
||||
|
@ -98,16 +107,16 @@ public class ImageProcessor
|
|||
genWebp = false;
|
||||
}
|
||||
|
||||
if (ident.Width * ident.Height > 30000000)
|
||||
if (ident.Width * ident.Height > _config.CurrentValue.MediaProcessing.MaxResolutionMpx * 1000 * 1000)
|
||||
{
|
||||
_logger.LogDebug("Image is larger than 30mpx ({width}x{height}), bypassing image processing pipeline",
|
||||
ident.Width, ident.Height);
|
||||
_logger.LogDebug("Image is larger than {mpx}mpx ({width}x{height}), bypassing image processing pipeline",
|
||||
_config.CurrentValue.MediaProcessing.MaxResolutionMpx, ident.Width, ident.Height);
|
||||
var props = new DriveFile.FileProperties { Width = ident.Size.Width, Height = ident.Size.Height };
|
||||
return new Result { Properties = props };
|
||||
}
|
||||
|
||||
#if EnableLibVips
|
||||
if (_config.Value.EnableLibVips)
|
||||
if (_config.CurrentValue.MediaProcessing.ImageProcessor == Enums.ImageProcessor.LibVips)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
@ -74,17 +74,19 @@ MaxConnections = 100
|
|||
;; Options: [Local, ObjectStorage]
|
||||
Provider = Local
|
||||
|
||||
;; Max file size for locally originating media, files larger than this will error on upload (-1 = no limit)
|
||||
MaxUploadSize = 100M
|
||||
|
||||
;; Max file size for remote media, files larger than this will never be cached (-1 = no limit)
|
||||
MaxCacheSize = 20M
|
||||
|
||||
;; Amount of time remote media is retained in the cache (0 = disabled, -1 = infinite)
|
||||
MediaRetention = 30d
|
||||
|
||||
;; Whether to cleanup avatars & banners past the media retention time
|
||||
;; Whether to cleanup remote avatars & banners past the media retention time
|
||||
CleanAvatars = false
|
||||
CleanBanners = false
|
||||
|
||||
;; Whether to enable LibVIPS support for image processing. Trades faster processing speed for higher memory usage.
|
||||
;; Note: Requires compilation with -p:EnableLibVips=true
|
||||
EnableLibVips = false
|
||||
|
||||
[Storage:Local]
|
||||
;; Path where media is stored at. Must be writable for the service user.
|
||||
Path = /path/to/media/location
|
||||
|
@ -103,6 +105,29 @@ Path = /path/to/media/location
|
|||
;; Only enable this if you have a cache in front of the object storage access URL that makes the validation fail on restart.
|
||||
;;DisableValidation = false
|
||||
|
||||
[Storage:MediaProcessing]
|
||||
;; Which image processor to use.
|
||||
;;
|
||||
;; ImageSharp = .NET library, slower, lower memory footprint. No external dependencies.
|
||||
;; LibVips = Native library, faster, higher and spikier memory footprint. Requires compilation with -p:EnableLibVips=true & for libvips to be installed on the system.
|
||||
;; None = Disables image processing, fastest, lowest memory footprint. Caution: metadata (e.g. location data) for locally originating images will *not* be stripped!
|
||||
;;
|
||||
;; Options: [ImageSharp, LibVips, None]
|
||||
ImageProcessor = ImageSharp
|
||||
|
||||
;; Whether to only process locally originating media. This is useful if you're working with a cpu-constrained environment,
|
||||
;; and want both remote media caching and local media processing.
|
||||
LocalOnly = false
|
||||
|
||||
;; Maximum file size for files to be considered for image processing.
|
||||
;; Caution: metadata (e.g. location data) for locally originating images will *not* be stripped for files larger than this
|
||||
MaxFileSize = 10M
|
||||
|
||||
;; Maximum resolution for files to be considered for image processing, in megapixels
|
||||
;; Note that processing an image requires up to 4MB of system memory per megapixel, in some edge case scenarios.
|
||||
;; Caution: metadata (e.g. location data) for locally originating images will *not* be stripped for files larger than this
|
||||
MaxResolutionMpx = 30
|
||||
|
||||
[Logging:LogLevel]
|
||||
Default = Information
|
||||
Iceshrimp = Information
|
||||
|
|
Loading…
Add table
Reference in a new issue