✨ Image analyzing and transcoding
This commit is contained in:
		| @@ -10,6 +10,7 @@ | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> | ||||
|         <PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" /> | ||||
|         <PackageReference Include="Casbin.NET" Version="2.12.0" /> | ||||
|         <PackageReference Include="Casbin.NET.Adapter.EFCore" Version="2.5.0" /> | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
| @@ -31,6 +32,9 @@ | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> | ||||
|         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" /> | ||||
|         <PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.4" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0" /> | ||||
|         <PackageReference Include="tusdotnet" Version="2.8.1" /> | ||||
|   | ||||
| @@ -1,22 +1,59 @@ | ||||
| using System.Globalization; | ||||
| using FFMpegCore; | ||||
| using System.Security.Cryptography; | ||||
| using Blurhash.ImageSharp; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Minio; | ||||
| using Minio.DataModel.Args; | ||||
| using Minio.DataModel.Tags; | ||||
| using NodaTime; | ||||
| using SixLabors.ImageSharp.PixelFormats; | ||||
| using SixLabors.ImageSharp.Metadata.Profiles.Exif; | ||||
| using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
|  | ||||
| public class FileService(AppDatabase db, IConfiguration configuration) | ||||
| { | ||||
|     public async Task<CloudFile> AnalyzeFileAsync( | ||||
|     private static readonly List<ExifTag> BlacklistExifTags = | ||||
|     [ | ||||
|         ExifTag.GPSLatitudeRef, | ||||
|         ExifTag.GPSLatitude, | ||||
|         ExifTag.GPSLongitudeRef, | ||||
|         ExifTag.GPSLongitude, | ||||
|         ExifTag.GPSAltitudeRef, | ||||
|         ExifTag.GPSAltitude, | ||||
|         ExifTag.GPSSatellites, | ||||
|         ExifTag.GPSStatus, | ||||
|         ExifTag.GPSMeasureMode, | ||||
|         ExifTag.GPSDOP, | ||||
|         ExifTag.GPSSpeedRef, | ||||
|         ExifTag.GPSSpeed, | ||||
|         ExifTag.GPSTrackRef, | ||||
|         ExifTag.GPSTrack, | ||||
|         ExifTag.GPSImgDirectionRef, | ||||
|         ExifTag.GPSImgDirection, | ||||
|         ExifTag.GPSMapDatum, | ||||
|         ExifTag.GPSDestLatitudeRef, | ||||
|         ExifTag.GPSDestLatitude, | ||||
|         ExifTag.GPSDestLongitudeRef, | ||||
|         ExifTag.GPSDestLongitude, | ||||
|         ExifTag.GPSDestBearingRef, | ||||
|         ExifTag.GPSDestBearing, | ||||
|         ExifTag.GPSDestDistanceRef, | ||||
|         ExifTag.GPSDestDistance, | ||||
|         ExifTag.GPSProcessingMethod, | ||||
|         ExifTag.GPSAreaInformation, | ||||
|         ExifTag.GPSDateStamp, | ||||
|         ExifTag.GPSDifferential | ||||
|     ]; | ||||
|  | ||||
|     public async Task<(CloudFile, Stream)> AnalyzeFileAsync( | ||||
|         Account.Account account, | ||||
|         string fileId, | ||||
|         Stream stream, | ||||
|         string fileName, | ||||
|         string? contentType | ||||
|         string? contentType, | ||||
|         string? filePath = null | ||||
|     ) | ||||
|     { | ||||
|         var fileSize = stream.Length; | ||||
| @@ -24,7 +61,7 @@ public class FileService(AppDatabase db, IConfiguration configuration) | ||||
|         contentType ??= !fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName); | ||||
|  | ||||
|         var existingFile = await db.Files.Where(f => f.Hash == hash).FirstOrDefaultAsync(); | ||||
|         if (existingFile is not null) return existingFile; | ||||
|         if (existingFile is not null) return (existingFile, stream); | ||||
|  | ||||
|         var file = new CloudFile | ||||
|         { | ||||
| @@ -38,6 +75,53 @@ public class FileService(AppDatabase db, IConfiguration configuration) | ||||
|  | ||||
|         switch (contentType.Split('/')[0]) | ||||
|         { | ||||
|             case "image": | ||||
|                 stream.Position = 0; | ||||
|                 using (var imageSharp = await Image.LoadAsync<Rgba32>(stream)) | ||||
|                 { | ||||
|                     var width = imageSharp.Width; | ||||
|                     var height = imageSharp.Height; | ||||
|                     var blurhash = Blurhasher.Encode(imageSharp, 3, 3); | ||||
|                     var format = imageSharp.Metadata.DecodedImageFormat?.Name ?? "unknown"; | ||||
|  | ||||
|                     var exifProfile = imageSharp.Metadata.ExifProfile; | ||||
|                     ushort orientation = 1; | ||||
|                     List<IExifValue> exif = []; | ||||
|  | ||||
|                     if (exifProfile is not null) | ||||
|                     { | ||||
|                         exif = exifProfile.Values | ||||
|                             .Where(v => !BlacklistExifTags.Contains((ExifTag)v.Tag)) | ||||
|                             .ToList<IExifValue>(); | ||||
|  | ||||
|                         if (exifProfile.Values.FirstOrDefault(e => e.Tag == ExifTag.Orientation) | ||||
|                                 ?.GetValue() is ushort o) | ||||
|                             orientation = o; | ||||
|                     } | ||||
|  | ||||
|                     if (orientation is 6 or 8) | ||||
|                         (width, height) = (height, width); | ||||
|  | ||||
|                     var aspectRatio = height != 0 ? (double)width / height : 0; | ||||
|  | ||||
|                     file.FileMeta = new Dictionary<string, object> | ||||
|                     { | ||||
|                         ["blur"] = blurhash, | ||||
|                         ["format"] = format, | ||||
|                         ["width"] = width, | ||||
|                         ["height"] = height, | ||||
|                         ["orientation"] = orientation, | ||||
|                         ["ratio"] = aspectRatio, | ||||
|                         ["exif"] = exif | ||||
|                     }; | ||||
|  | ||||
|                     var newStream = new MemoryStream(); | ||||
|                     await imageSharp.SaveAsWebpAsync(newStream); | ||||
|                     file.MimeType = "image/webp"; | ||||
|                     stream = newStream; | ||||
|                 } | ||||
|  | ||||
|                 break; | ||||
|             case "video": | ||||
|             case "audio": | ||||
|                 var mediaInfo = await FFProbe.AnalyseAsync(stream); | ||||
| @@ -56,7 +140,7 @@ public class FileService(AppDatabase db, IConfiguration configuration) | ||||
|  | ||||
|         db.Files.Add(file); | ||||
|         await db.SaveChangesAsync(); | ||||
|         return file; | ||||
|         return (file, stream); | ||||
|     } | ||||
|  | ||||
|     private static async Task<string> HashFileAsync(Stream stream, int chunkSize = 1024 * 1024, long? fileSize = null) | ||||
|   | ||||
| @@ -11,8 +11,11 @@ | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4a28847852ee9ba45fd3107526c0a749a733bd4f4ebf33aa3c9a59737a3f758_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F832399abc13b45b6bdbabfa022e4a28487e00_003F7f_003F7aece4dd_003FEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEvents_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F20_003F86914b63_003FEvents_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExifTag_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa932cb9090ed48088111ae919dcdd9021ba00_003Fd7_003F0472c800_003FExifTag_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExifTag_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F5c_003F8ed75f18_003FExifTag_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F8c_003F9f6e3f4f_003FFileResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AForwardedHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcfe5737f9bb84738979cbfedd11822a8ea00_003F50_003F9a335f87_003FForwardedHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImageFile_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa932cb9090ed48088111ae919dcdd9021ba00_003F71_003F0a804432_003FImageFile_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user