summaryrefslogtreecommitdiff
path: root/Services
diff options
context:
space:
mode:
Diffstat (limited to 'Services')
-rw-r--r--Services/ConfigService.cs72
-rw-r--r--Services/FeedService.cs212
-rw-r--r--Services/MediaService.cs400
-rw-r--r--Services/OcrService.cs128
-rw-r--r--Services/TagService.cs305
-rw-r--r--Services/UserService.cs76
6 files changed, 0 insertions, 1193 deletions
diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs
deleted file mode 100644
index ac1f155..0000000
--- a/Services/ConfigService.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using HyperBooru.ApiModels;
-
-namespace HyperBooru.Services;
-
-public interface IConfigService {
- public string DataPath { get; }
- public string KeyPath { get; }
- public string DbConnectionString { get; }
- public string MediaBasePath { get; }
- public string ThumbnailBasePath { get; }
- public string ConvertedMediaBasePath { get; }
- public bool EnableOcr { get; }
-}
-
-public class ConfigService : IConfigService {
- private IConfiguration config;
-
- private const string AppName = "HyperBooru";
-
- public string DataPath {
- get {
- #if DEBUG
- return "Data";
- #else
- string? path = config["DataPath"];
- if(path is not null)
- return path;
-
- switch(Environment.OSVersion.Platform) {
- case PlatformID.Win32NT:
- return Path.Join(
- Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
- AppName);
- case PlatformID.Unix:
- return $"/var/lib/{AppName.ToLower()}";
- default:
- throw new NotImplementedException(
- $"Unknown Operating System: {Environment.OSVersion.Platform}");
- }
- #endif
- }
- }
-
- public string KeyPath =>
- Path.Join(DataPath, "keys");
-
- public string DbConnectionString =>
- config.GetConnectionString("DefaultConnection") ??
- throw new HBException("Unable to get default connection string");
-
- public string MediaBasePath =>
- Path.Join(DataPath, "media");
-
- public string ThumbnailBasePath =>
- Path.Join(DataPath, "thumb");
-
- public string ConvertedMediaBasePath =>
- Path.Join(DataPath, "converted");
-
- public bool EnableOcr =>
- bool.TryParse(config["DisableOcr"], out bool x) ? !x : true;
-
- public ConfigService(IConfiguration config) {
- this.config = config;
- InitDirectoryStructure();
- }
-
- private void InitDirectoryStructure() {
- Directory.CreateDirectory(DataPath);
- Directory.CreateDirectory(MediaBasePath);
- }
-} \ No newline at end of file
diff --git a/Services/FeedService.cs b/Services/FeedService.cs
deleted file mode 100644
index 3744e73..0000000
--- a/Services/FeedService.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-using HyperBooru.ApiModels;
-using Microsoft.EntityFrameworkCore;
-
-namespace HyperBooru.Services;
-
-public interface IFeedService {
- public Media[] LoadChunk(
- bool selectIngest,
- bool includeNsfw,
- Media? key = null,
- int count = 50,
- SortOrder sortOrder = SortOrder.ObjectId);
-
- public Media[] LoadChunk(
- bool selectIngest,
- bool includeNsfw,
- string query,
- Media? key = null,
- int count = 50,
- SortOrder sortOrder = SortOrder.ObjectId);
-
- public Media[] LoadChunk(
- bool selectIngest,
- bool includeNsfw,
- Guid tagId,
- Media? key = null,
- int count = 50,
- SortOrder sortOrder = SortOrder.ObjectId);
-
- public Media[] LoadChunk(FeedRequest feedRequest);
-}
-
-public class FeedService : IFeedService {
- private IDbContextFactory<HBContext> dbFactory;
-
- public FeedService(IDbContextFactory<HBContext> dbFactory) =>
- this.dbFactory = dbFactory;
-
- public Media[] LoadChunk(
- bool selectIngest,
- bool includeNsfw,
- Media? continuationToken,
- int count,
- SortOrder sortOrder) => LoadChunkInternal(
- selectIngest, includeNsfw, null, null, continuationToken?.Guid, count, sortOrder);
-
- public Media[] LoadChunk(
- bool selectIngest,
- bool includeNsfw,
- string query,
- Media? continuationToken,
- int count,
- SortOrder sortOrder) => LoadChunkInternal(
- selectIngest, includeNsfw, query, null, continuationToken?.Guid, count, sortOrder);
-
- public Media[] LoadChunk(
- bool selectIngest,
- bool includeNsfw,
- Guid tagId,
- Media? continuationToken,
- int count,
- SortOrder sortOrder) => LoadChunkInternal(
- selectIngest, includeNsfw, null, tagId, continuationToken?.Guid, count, sortOrder);
-
- public Media[] LoadChunk(FeedRequest feedRequest) {
- switch(feedRequest) {
- case FeedSearchRequest searchRequest:
- return LoadChunkInternal(
- selectIngest: searchRequest.SelectIngest,
- includeNsfw: searchRequest.IncludeNsfw,
- query: searchRequest.Query,
- tagId: null,
- continuationToken: searchRequest.ContinuationToken,
- count: searchRequest.Count,
- sortOrder: searchRequest.SortOrder);
- case FeedTagRequest tagRequest:
- return LoadChunkInternal(
- selectIngest: tagRequest.SelectIngest,
- includeNsfw: tagRequest.IncludeNsfw,
- query: null,
- tagId: tagRequest.TagId,
- continuationToken: tagRequest.ContinuationToken,
- count: tagRequest.Count,
- sortOrder: tagRequest.SortOrder);
- default:
- return LoadChunkInternal(
- selectIngest: feedRequest.SelectIngest,
- includeNsfw: feedRequest.IncludeNsfw,
- query: null,
- tagId: null,
- continuationToken: feedRequest.ContinuationToken,
- count: feedRequest.Count,
- sortOrder: feedRequest.SortOrder);
- }
- }
-
- private Media[] LoadChunkInternal(
- bool selectIngest,
- bool includeNsfw,
- string? query,
- Guid? tagId,
- Guid? continuationToken,
- int count,
- SortOrder sortOrder) {
-
- if(selectIngest && !includeNsfw)
- return Array.Empty<Media>();
-
- using var db = dbFactory.CreateDbContext();
-
- IQueryable<Media> media = db.Media
- .AsSingleQuery()
- .AsNoTracking()
- .Include(m => m.Tags)
- .Include(m => m.CurrentUploadedFile);
-
- if(!includeNsfw)
- media = media
- .Where(m => !TagsThatImply(db, HBContext.NsfwTag)
- .Intersect(m.Tags.Select(t => t.TagDefinitionId))
- .Any());
-
- if(selectIngest) {
- media = media
- .Where(m => m.Tags
- .Select(t => t.TagDefinitionId)
- .Contains((int) HBObjectId.IngestTag));
- } else {
- media = media
- .Where(m => !m.Tags
- .Select(t => t.TagDefinitionId)
- .Contains((int) HBObjectId.IngestTag));
- }
-
- if(query is not null) {
- media = Search(media, query);
- } else if(tagId is not null) {
- media = media
- .Where(m => TagsThatImply(db, (Guid) tagId)
- .Intersect(m.Tags.Select(t => t.TagDefinitionId))
- .Any());
- }
-
- if(continuationToken is not null)
- media = media
- .Where(m => m.ObjectId > db.Media.First(m => m.Guid == continuationToken).ObjectId);
-
- switch(sortOrder) {
- case SortOrder.ObjectId:
- media = media.OrderBy(m => m.ObjectId);
- break;
- case SortOrder.LastWriteTime:
- media = media.OrderBy(m => m.CurrentUploadedFile!.LastWriteTime);
- break;
- case SortOrder.Random:
- media = media.OrderBy(m => EF.Functions.Random());
- break;
- }
-
- return media
- .Take(count)
- .ToArray();
- }
-
- private static IQueryable<Media> Search(IQueryable<Media> media, string query) {
- // TODO: search implicit tags as well
-
- query = query.ToLower().Trim();
-
- return media
- .Where(m =>
- (m.ShortDescription != null && m.ShortDescription.ToLower().Contains(query)) ||
- (m.LongDescription != null && m.LongDescription.ToLower().Contains(query)) ||
- (m.UploadedFiles.Any(uf => uf.Filename != null && uf.Filename.ToLower().Contains(query))) ||
- (m.OcrData != null && m.OcrData.SearchableText.ToLower().Contains(query)) ||
- (m.Tags.Any(t => t.TagDefinition.Name.ToLower().Contains(query))));
- }
-
- private static IQueryable<int> TagsThatImply(HBContext db, Guid tagId) =>
- db.Database.SqlQueryRaw<int>("""
- WITH RECURSIVE basetag AS (
- SELECT "ObjectId" FROM "Objects" WHERE "Guid" = {0}
- ),
- impliedtags AS (
- SELECT
- "TagDefinitionObjectId"
- FROM
- "TagDefinitionTagDefinition"
- INNER JOIN
- basetag
- ON
- "ImplicitTagsObjectId" = basetag."ObjectId"
- UNION
- SELECT
- "TagDefinitionTagDefinition"."TagDefinitionObjectId"
- FROM
- "TagDefinitionTagDefinition"
- INNER JOIN
- impliedtags
- ON
- impliedtags."TagDefinitionObjectId" = "TagDefinitionTagDefinition"."ImplicitTagsObjectId"
- )
- SELECT DISTINCT
- "TagDefinitionObjectId" AS "Value"
- FROM impliedtags
- UNION
- SELECT
- "ObjectId" AS "Value"
- FROM
- basetag
- """, tagId);
-}
diff --git a/Services/MediaService.cs b/Services/MediaService.cs
deleted file mode 100644
index 2d1533c..0000000
--- a/Services/MediaService.cs
+++ /dev/null
@@ -1,400 +0,0 @@
-using HyperBooru.ApiModels;
-using ImageMagick;
-using Microsoft.EntityFrameworkCore;
-using MimeDetective;
-using MimeDetective.Definitions;
-using System.Security.Cryptography;
-using System.Text.RegularExpressions;
-
-namespace HyperBooru.Services;
-
-public interface IMediaService {
- public void SetDescription(
- Media media,
- string? shortDescription,
- string? longDescription);
-
- public void SetIngest(Media media, bool ingest);
-
- public Media Create(
- Stream fileData,
- string fileName,
- string? checksum = null,
- DateTime? lastAccessTime = null,
- DateTime? lastWriteTime = null,
- DateTime? createTime = null,
- string? path = null,
- PathType? pathType = null,
- Guid[]? tagIds = null);
-
- public void Delete(Guid media);
- public void Delete(Media media);
- public void DeleteThumbnails(Guid media);
- public void DeleteThumbnails(Media media);
- public Stream GetThumbnail(Guid media, int? width, int? height);
- public Stream GetThumbnail(Media media, int? width, int? height);
- public Stream GetConverted(Guid mediaId, string mimeType = "image/png");
- public Stream GetConverted(Media media, string mimeType = "image/png");
- public string GetPath(Guid media);
- public string GetPath(Media media);
-
-}
-
-public class MediaService : IMediaService {
- private readonly Dictionary<string,MagickFormat> FormatMap = new() {
- ["image/jpeg"] = MagickFormat.Jpeg,
- ["image/jpg"] = MagickFormat.Jpg,
- ["image/png"] = MagickFormat.Png,
- ["image/webp"] = MagickFormat.WebP
- };
-
- private IDbContextFactory<HBContext> dbFactory;
- private IConfigService config;
-
- private IContentInspector inspector;
-
- public MediaService(IDbContextFactory<HBContext> dbFactory,
- IConfigService config) {
-
- this.dbFactory = dbFactory;
- this.config = config;
-
- ContentInspectorBuilder inspectorBuilder = new() {
- Definitions =
- DefaultDefinitions.FileTypes.Images.All()
- .Union(DefaultDefinitions.FileTypes.Video.All())
- .ToList()
- };
-
- inspector = inspectorBuilder.Build();
- }
-
- public void SetIngest(Media media, bool ingest) {
- using var db = dbFactory.CreateDbContext();
- media = db.Media
- .Include(m => m.Tags)
- .ThenInclude(t => t.TagDefinition)
- .First(m => m.Guid == media.Guid);
- var ingestTag = db.TagDefinitions
- .First(td => td.Guid == HBContext.IngestTag);
-
- if(ingest) {
- if(!media.Tags.Select(t => t.TagDefinition.Guid).Contains(HBContext.IngestTag))
- media.Tags.Add(new(ingestTag));
- } else {
- media.Tags.RemoveAll(t => t.TagDefinition.Guid == HBContext.IngestTag);
- }
-
- db.SaveChanges();
- }
-
- public void SetDescription(
- Media media,
- string? shortDescription,
- string? longDescription) {
-
- using var db = dbFactory.CreateDbContext();
- var m = db.Media.First(m => m.Guid == media.Guid);
-
- shortDescription = shortDescription?.Trim();
- longDescription = longDescription?.Trim();
-
- if(string.IsNullOrEmpty(shortDescription))
- shortDescription = null;
- if(string.IsNullOrEmpty(longDescription))
- longDescription = null;
-
- m.ShortDescription = shortDescription;
- m.LongDescription = longDescription;
-
- db.SaveChanges();
- }
-
- public Media Create(
- Stream fileData,
- string fileName,
- string? checksum = null,
- DateTime? lastAccessTime = null,
- DateTime? lastWriteTime = null,
- DateTime? createTime = null,
- string? path = null,
- PathType? pathType = null,
- Guid[]? tagIds = null) {
-
- using var db = dbFactory.CreateDbContext();
- using var transaction = db.Database.BeginTransaction();
-
- if(fileData.Length == 0)
- throw new MediaCreateException("File is empty");
-
- // Calculate the checksum using the in-memory file contents
- var hash = BitConverter
- .ToString(MD5.Create().ComputeHash(fileData))
- .Replace("-", "")
- .ToLower();
-
- if(checksum is not null && hash != checksum.ToLower())
- throw new MediaCreateException("Checksum does not match");
-
- // Determine the MIME type
- fileData.Seek(0, SeekOrigin.Begin);
- var defs = inspector.Inspect(fileData);
- var mime = defs.ByMimeType().FirstOrDefault()?.MimeType;
- if(mime is null)
- throw new MediaCreateException("Unsupported file type");
-
- // Read the image with ImageMagick to determine the width and height
- fileData.Seek(0, SeekOrigin.Begin);
- using var magickImage = new MagickImage(fileData);
-
- var media = db.Media
- .Include(m => m.UploadedFiles)
- .Include(m => m.Tags)
- .FirstOrDefault(m => m.UploadedFiles.Any(uf => uf.Checksum == hash));
-
- var fileRecord = new UploadedFile() {
- Filename = fileName,
- Length = fileData.Length,
- Checksum = hash,
- ChecksumVerified = checksum is not null,
- MimeType = mime,
- Width = (int) magickImage.Width,
- Height = (int) magickImage.Height,
- UploadTime = DateTime.UtcNow,
- LastAccessTime = lastAccessTime,
- LastWriteTime = lastWriteTime,
- CreateTime = createTime,
- Path = pathType is null ? null : path,
- PathType = pathType
- };
-
- var tags = Array.Empty<TagDefinition>();
- if(tagIds is not null) {
- tagIds = tagIds.Distinct().ToArray();
-
- tags = db.TagDefinitions
- .Where(td => tagIds.Contains(td.Guid))
- .ToArray();
-
- if(tags.Count() < tagIds.Count()) {
- var badIds = tagIds
- .Where(x => !tags.Select(td => td.Guid).Contains(x))
- .Order();
-
- throw new MediaCreateException(
- $"Non-existent tags specified: {string.Join(", ", badIds)}");
- }
- }
-
- if(media is null) {
- var ingestTagDef = db.TagDefinitions
- .First(td => td.Guid == HBContext.IngestTag);
-
- media = new() {
- UploadedFiles = new() {
- fileRecord
- },
- Tags = tags is null ? [ new() { TagDefinition = ingestTagDef } ] : tags
- .Select(td => new Tag() { TagDefinition = td })
- .ToList()
- };
-
- using var newFile = File.Create(GetPath(media));
-
- fileData.Seek(0, SeekOrigin.Begin);
- fileData.CopyTo(newFile);
- newFile.Flush();
-
- db.Media.Add(media);
- db.SaveChanges();
- media.CurrentUploadedFile = fileRecord;
- db.SaveChanges();
- } else {
- var fileHashes = media.UploadedFiles
- .Select(uf => GetUploadedFileHash(uf));
- // Only add the uploaded file record if it contains new information
- if(!fileHashes.Contains(GetUploadedFileHash(fileRecord)))
- media.UploadedFiles.Add(fileRecord);
- // Add new tags if needed
- var missingTags = tags
- .Where(td => !media.Tags.Select(t => t.TagDefinition.Guid).Contains(td.Guid));
- media.Tags.AddRange(missingTags.Select(td => new Tag() { TagDefinition = td }));
- db.Update(media);
- db.SaveChanges();
- }
-
- transaction.Commit();
-
- return media;
- }
-
- public void Delete(Guid media) {
- using var db = dbFactory.CreateDbContext();
- var m = db.Media.First(m => m.Guid == media);
-
- var path = Path.Join(
- config.MediaBasePath,
- m.Guid.ToString().Substring(0, 2),
- m.Guid.ToString().Substring(2, 2),
- m.Guid.ToString());
-
- try {
- var fileInfo = new FileInfo(path);
- fileInfo.Delete();
- fileInfo.Directory?.Delete();
- fileInfo.Directory?.Parent?.Delete();
- } catch(IOException) {}
-
- try {
- DeleteThumbnails(media);
- } catch {}
-
- db.Media.Remove(m);
- db.SaveChanges();
- }
-
- public void Delete(Media media) =>
- Delete(media.Guid);
-
- public void DeleteThumbnails(Guid media) {
- var dir = new DirectoryInfo(Path.Join(
- config.ThumbnailBasePath,
- media.ToString().Substring(0, 2),
- media.ToString().Substring(2, 2)));
-
- var pattern = new Regex($"^{media}-[0-9]+-[0-9]+$");
- var toDelete = dir.GetFiles()
- .Where(f => pattern.IsMatch(f.Name))
- .ToList();
-
- List<Exception> exceptions = new();
-
- foreach(var file in toDelete) {
- try {
- file.Delete();
- } catch(Exception e) {
- exceptions.Add(e);
- }
- }
-
- try {
- dir.Delete();
- dir.Parent?.Delete();
- } catch(Exception e) {
- exceptions.Add(e);
- }
-
- // TODO: wrap the AggregateException in a ThumbnailException
- if(exceptions.Count() > 1)
- throw new AggregateException(exceptions);
- }
-
- public void DeleteThumbnails(Media media) =>
- DeleteThumbnails(media.Guid);
-
- public Stream GetThumbnail(Guid mediaId, int? width, int? height) {
- if(width is null && height is null)
- throw new ThumbnailException(
- "Both width and height cannot be null!",
- mediaId);
-
- var thumbPath = GetThumbnailPath(mediaId, width, height);
-
- if(File.Exists(thumbPath))
- return System.IO.File.OpenRead(thumbPath);
-
- if(!File.Exists(GetPath(mediaId)))
- throw new ObjectNotFoundException([ mediaId ]);
-
- using var image = new MagickImage(GetPath(mediaId));
-
- if(width > image.Width || height > image.Height) {
- width = (int) image.Width;
- height = (int) image.Height;
- }
-
- image.Thumbnail((uint) (width ?? -1), (uint) (height ?? -1));
- image.Write(thumbPath, MagickFormat.Jpeg);
-
- return System.IO.File.OpenRead(thumbPath);
- }
-
- public Stream GetConverted(Guid mediaId, string mimeType) {
- if(!FormatMap.TryGetValue(mimeType, out var format))
- throw new MediaException($"Cannot convert to unknown format ({mimeType})", mediaId);
-
- var convertedPath = GetConvertedPath(mediaId, mimeType);
-
- if(File.Exists(convertedPath))
- return System.IO.File.OpenRead(convertedPath);
-
- if(!File.Exists(GetPath(mediaId)))
- throw new ObjectNotFoundException([ mediaId ]);
-
- using var image = new MagickImage(GetPath(mediaId));
- image.Write(convertedPath, format);
-
- return System.IO.File.OpenRead(convertedPath);
- }
-
- public Stream GetThumbnail(Media media, int? width, int? height) =>
- GetThumbnail(media.Guid, width, height);
-
- public Stream GetConverted(Media media, string mimeType) =>
- GetConverted(media.Guid, mimeType);
-
- public string GetPath(Guid mediaId) {
- var fileInfo = new FileInfo(
- Path.Join(
- config.MediaBasePath,
- mediaId.ToString().Substring(0, 2),
- mediaId.ToString().Substring(2, 2),
- mediaId.ToString()));
-
- Directory.CreateDirectory(fileInfo.Directory!.FullName);
- return fileInfo.FullName;
- }
-
- public string GetThumbnailPath(Guid mediaId, int? width, int? height) {
- if(width is null && height is null)
- throw new ThumbnailException(
- "Both width and height cannot be null!",
- mediaId);
-
- var fileInfo = new FileInfo(Path.Join(
- config.ThumbnailBasePath,
- mediaId.ToString().Substring(0, 2),
- mediaId.ToString().Substring(2, 2),
- $"{mediaId.ToString()}-{(width ?? 0)}-{(height ?? 0)}"));
-
- Directory.CreateDirectory(fileInfo.Directory!.FullName);
- return fileInfo.FullName;
- }
-
- public string GetConvertedPath(Guid mediaId, string mimeType) {
- var fileInfo = new FileInfo(Path.Join(
- config.ConvertedMediaBasePath,
- mediaId.ToString().Substring(0, 2),
- mediaId.ToString().Substring(2, 2),
- $"{mediaId.ToString()}-{mimeType.Split('/')[1]}"));
-
- Directory.CreateDirectory(fileInfo.Directory!.FullName);
- return fileInfo.FullName;
- }
-
- public string GetPath(Media media) =>
- GetPath(media.Guid);
-
- public string GetThumbnailPath(Media media, int? width, int? height) =>
- GetThumbnailPath(media.Guid, width, height);
-
- public string GetConvertedPath(Media media, string mimeType) =>
- GetConvertedPath(media.Guid, mimeType);
-
- private int GetUploadedFileHash(UploadedFile uf) => (
- uf.CreateTime,
- uf.LastWriteTime,
- uf.Filename,
- uf.Length,
- uf.Checksum).GetHashCode();
-}
diff --git a/Services/OcrService.cs b/Services/OcrService.cs
deleted file mode 100644
index d43db2e..0000000
--- a/Services/OcrService.cs
+++ /dev/null
@@ -1,128 +0,0 @@
-using HyperBooru.Util;
-using Microsoft.EntityFrameworkCore;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
-using Tesseract;
-
-namespace HyperBooru.Services;
-
-public class OcrService : IHostedService {
- private readonly string[] InvalidMimeTypes = [ "image/heic", "image/webp" ];
-
- private readonly TimeSpan ProcessInterval = TimeSpan.FromMinutes(30);
- private readonly TimeSpan StartupDelay = TimeSpan.FromSeconds(30);
-
- private readonly Regex SpaceRegex = new(@"[^0-9a-z]+", RegexOptions.Compiled);
-
- private Task? task;
- private CancellationTokenSource cts = new();
-
- private Timer timer;
-
- private IConfigService configService;
- private IServiceScopeFactory scopeFactory;
- private ILogger<OcrService> logger;
- private IDbContextFactory<HBContext> dbFactory;
-
- public OcrService(
- IConfigService configService,
- IServiceScopeFactory scopeFactory,
- ILogger<OcrService> logger,
- IDbContextFactory<HBContext> dbFactory) {
-
- this.configService = configService;
- this.scopeFactory = scopeFactory;
- this.logger = logger;
- this.dbFactory = dbFactory;
-
- timer = new((object? state) => {
- if(task is not null && !task.IsCompleted)
- return;
- cts = new();
- task = ProcessAllAsync(cts.Token);
- });
- }
-
- public Task StartAsync(CancellationToken ct) {
- if(configService.EnableOcr) {
- logger.LogInformation("Service starting...");
- timer.Change(StartupDelay, ProcessInterval);
- }
-
- return Task.CompletedTask;
- }
-
- public Task StopAsync(CancellationToken ct) {
- logger.LogInformation("Service stopping...");
- timer.Change(Timeout.Infinite, Timeout.Infinite);
- cts.Cancel();
- return Task.CompletedTask;
- }
-
- async Task ProcessAllAsync(CancellationToken ct) {
- using var scope = scopeFactory.CreateScope();
- var mediaService = scope.ServiceProvider
- .GetRequiredService<IMediaService>();
-
- using var db = dbFactory.CreateDbContext();
- Guid[] guids = db.Media
- .AsNoTracking()
- .Include(m => m.CurrentUploadedFile)
- .Include(m => m.OcrData)
- .Where(m => m.OcrData == null)
- .Where(m => m.CurrentUploadedFile!.MimeType.Contains("image/"))
- .Where(m => !InvalidMimeTypes.Contains(m.CurrentUploadedFile!.MimeType))
- .Select(m => m.Guid)
- .ToArray();
- db.Dispose();
-
- logger.LogInformation($"Performing OCR pass on {guids.Count()} media items");
-
- var factory = new TaskFactory(new LimitedConcurrencyTaskScheduler());
- var tasks = new List<Task>();
-
- var stopwatch = Stopwatch.StartNew();
-
- foreach(var guid in guids)
- tasks.Add(factory.StartNew(() => Process(guid, mediaService), ct));
-
- await Task.WhenAll(tasks);
- stopwatch.Stop();
-
- var time = stopwatch.Elapsed.ToStringHumanReadable();
- logger.LogInformation(
- $"Performed OCR pass on {guids.Count()} media items in {time}");
- }
-
- private void Process(Guid media, IMediaService mediaService) {
- logger.LogDebug($"Performing OCR on media item {media}");
-
- using var db = dbFactory.CreateDbContext();
- var m = db.Media
- .Include(m => m.OcrData)
- .First(m => m.Guid == media);
-
- OcrData o = m.OcrData ?? new();
-
- using var engine = new TesseractEngine("tessdata", "eng", EngineMode.Default);
- using var image = Pix.LoadFromFile(mediaService.GetPath(m));
- engine.SetVariable("debug_file", NullFile);
-
- o.Timestamp = DateTime.UtcNow;
- o.Text = engine.Process(image).GetText().Trim();
- o.SearchableText = SpaceRegex.Replace(o.Text.ToLower(), " ").Trim();
-
- m.OcrData = o;
- db.SaveChanges();
- }
-
- private string NullFile {
- get {
- if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- return "NUL";
- else
- return "/dev/null";
- }
- }
-}
diff --git a/Services/TagService.cs b/Services/TagService.cs
deleted file mode 100644
index f7b91dc..0000000
--- a/Services/TagService.cs
+++ /dev/null
@@ -1,305 +0,0 @@
-using HyperBooru.ApiModels;
-using Microsoft.EntityFrameworkCore;
-
-namespace HyperBooru.Services;
-
-public interface ITagService {
- public void AddTag(Guid obj, Guid tagDef);
- public void AddTag(HBObject obj, TagDefinition tagDef);
- public void RemoveTag(Guid obj, Guid tagDef);
- public void RemoveTag(HBObject obj, TagDefinition tagDef);
- public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs);
- public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs);
- public void AddImplicitTag(Guid tagDef, Guid implicitTagDef);
- public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef);
- public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef);
- public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef);
- public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null);
- public void DeleteTagDefinition(Guid tagDef);
- public void DeleteTagDefinition(TagDefinition tagDef);
- public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null);
- public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null);
- public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj);
- public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj);
- public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef);
- public TagDefinition[] TagsThatImply(Guid tagDef);
- public TagDefinition[] TagsThatImply(TagDefinition tagDef);
-}
-
-public class TagService : ITagService {
- private IDbContextFactory<HBContext> dbFactory;
-
- public TagService(IDbContextFactory<HBContext> dbFactory) =>
- this.dbFactory = dbFactory;
-
- public void AddTag(Guid obj, Guid tagDef) {
- using var db = dbFactory.CreateDbContext();
-
- var tag = db.TagDefinitions.First(td => td.Guid == tagDef);
-
- db.Objects
- .Include(o => o.Tags)
- .ThenInclude(t => t.TagDefinition)
- .Where(o => !o.Tags.Select(t => t.TagDefinition.Guid).Contains(tagDef))
- .FirstOrDefault(o => o.Guid == obj)?
- .Tags
- .Add(new(tag));
-
- db.SaveChanges();
- }
-
- public void AddTag(HBObject obj, TagDefinition tagDef) =>
- AddTag(obj.Guid, tagDef.Guid);
-
- public void RemoveTag(Guid obj, Guid tagDef) {
- using var db = dbFactory.CreateDbContext();
-
- db.Objects
- .Include(o => o.Tags)
- .ThenInclude(t => t.TagDefinition)
- .First(o => o.Guid == obj)
- .Tags
- .RemoveAll(t => t.TagDefinition.Guid == tagDef);
-
- db.SaveChanges();
- }
-
- public void RemoveTag(HBObject obj, TagDefinition tagDef) =>
- RemoveTag(obj.Guid, tagDef.Guid);
-
- public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs) {
- using var db = dbFactory.CreateDbContext();
- using var transaction = db.Database.BeginTransaction();
-
- var tag = db.TagDefinitions
- .Include(td => td.ImplicitTags)
- .First(td => td.Guid == tagDef);
-
- tag.ImplicitTags.RemoveAll(td => !implicitTagDefs.Contains(td.Guid));
- tag.ImplicitTags.AddRange(
- db.TagDefinitions
- .Where(td => implicitTagDefs.Contains(td.Guid))
- .Where(td => !tag.ImplicitTags
- .Select(td => td.Guid)
- .Contains(td.Guid)));
-
- db.SaveChanges();
- transaction.Commit();
- }
-
- public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs) =>
- SetImplicitTags(tagDef.Guid, implicitTagDefs.Select(td => td.Guid).ToArray());
-
- public void AddImplicitTag(Guid tagDef, Guid implicitTagDef) {
- using var db = dbFactory.CreateDbContext();
-
- var tag = db.TagDefinitions
- .Include(td => td.ImplicitTags)
- .First(td => td.Guid == tagDef);
- var implicitTag = db.TagDefinitions.First(td => td.Guid == implicitTagDef);
-
- tag.ImplicitTags.Add(implicitTag);
- db.SaveChanges();
- }
-
- public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) =>
- AddImplicitTag(tagDef, implicitTagDef);
-
- public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef) {
- using var db = dbFactory.CreateDbContext();
-
- var tag = db.TagDefinitions
- .Include(td => td.ImplicitTags)
- .First(td => td.Guid == tagDef);
-
- tag.ImplicitTags.RemoveAll(td => td.Guid == implicitTagDef);
- db.SaveChanges();
- }
-
- public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) =>
- RemoveImplicitTag(tagDef, implicitTagDef);
-
- public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null) {
- using var db = dbFactory.CreateDbContext();
-
- if(string.IsNullOrEmpty(@namespace))
- @namespace = null;
- if(string.IsNullOrEmpty(alias))
- alias = null;
-
- // Remove leading and trailing whitespace
- name = name.Trim();
- @namespace = @namespace?.Trim();
- alias = alias?.Trim();
-
- TagDefinition tagDef = new() {
- Source = TagSource.UserTag,
- Namespace = @namespace,
- Name = name,
- Alias = alias
- };
-
- bool nameExists = db.TagDefinitions.Any(td => td.Name.ToLower() == name.ToLower());
- bool aliasExists = false;
- if(alias is not null)
- aliasExists = db.TagDefinitions
- .Where(td => td.Alias != null)
- .Any(td => td.Alias!.ToLower() == alias.ToLower());
- if(nameExists || aliasExists)
- throw new TagDuplicateException(nameExists, aliasExists);
-
- if(!db.TagDefinitions.Contains(tagDef))
- db.TagDefinitions.Add(tagDef);
- db.SaveChanges();
- }
-
- public void DeleteTagDefinition(Guid tagDef) {
- using var db = dbFactory.CreateDbContext();
-
- var tag = db.TagDefinitions.First(td => td.Guid == tagDef);
-
- using var transaction = db.Database.BeginTransaction();
-
- db.Tags.RemoveRange(
- db.Tags
- .Include(t => t.TagDefinition)
- .Where(t => t.TagDefinition.Guid == tagDef));
- db.TagDefinitions.Remove(tag);
- db.SaveChanges();
-
- transaction.Commit();
- }
-
- public void DeleteTagDefinition(TagDefinition tagDef) =>
- DeleteTagDefinition(tagDef.Guid);
-
- public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null) {
- using var db = dbFactory.CreateDbContext();
-
- if(string.IsNullOrEmpty(@namespace))
- @namespace = null;
- if(string.IsNullOrEmpty(alias))
- alias = null;
-
- // Remove leading and trailing whitespace
- name = name.Trim();
- @namespace = @namespace?.Trim();
- alias = alias?.Trim();
-
- var tag = db.TagDefinitions.First(td => td.Guid == tagDef);
-
- TagDefinition? nameExisting = db.TagDefinitions.FirstOrDefault(td => td.Name.ToLower() == name.ToLower());
- TagDefinition? aliasExisting = null;
- if(alias is not null)
- aliasExisting = db.TagDefinitions
- .Where(td => td.Alias != null)
- .FirstOrDefault(td => td.Alias!.ToLower() == alias.ToLower());
- bool nameExists = nameExisting is not null && nameExisting != tag;
- bool aliasExists = aliasExisting is not null && aliasExisting != tag;
- if(nameExists || aliasExists)
- throw new TagDuplicateException(nameExists, aliasExists);
-
- tag.Name = name;
- tag.Namespace = @namespace;
- tag.Alias = alias;
-
- db.SaveChanges();
- }
-
- public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null) =>
- UpdateTagDefinition(tagDef.Guid, name, @namespace, alias);
-
- private (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(IEnumerable<TagDefinition> tagDefs) {
- using var db = dbFactory.CreateDbContext();
-
- var tagGuids = tagDefs
- .Select(td => td.Guid)
- .ToArray();
-
- // Query all tag definitions
- var allTags = db.TagDefinitions
- .Include(td => td.ImplicitTags)
- .ToArray();
-
- var tags = new List<TagDefinition>(
- allTags.IntersectBy(
- tagGuids,
- td => td.Guid));
-
- while(true) {
- var toAdd = tags
- .SelectMany(td => td.ImplicitTags)
- .ExceptBy(tags.Select(td => td.Guid), td => td.Guid)
- .ToArray();
-
- if(toAdd.Count() == 0)
- break;
-
- tags.AddRange(toAdd);
- }
-
- return tags
- .Select(td => new ValueTuple<TagDefinition, bool>(td, !tagGuids.Contains(td.Guid)))
- .ToArray();
- }
-
- public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj) {
- using var db = dbFactory.CreateDbContext();
-
- // Query a list of tag GUIDs for this object
- var tags = db.Objects
- .Include(o => o.Tags)
- .ThenInclude(t => t.TagDefinition)
- .First(o => o.Guid == obj)
- .Tags
- .Select(t => t.TagDefinition)
- .ToArray();
-
- return GetAllTags(tags);
- }
-
-
- public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj) =>
- GetAllTags(obj.Guid);
-
- public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef) {
- using var db = dbFactory.CreateDbContext();
-
- var tags = db.TagDefinitions
- .Include(td => td.ImplicitTags)
- .First(td => td.Guid == tagDef.Guid)
- .ImplicitTags
- .ToArray();
-
- return GetAllTags(tags);
- }
-
- public TagDefinition[] TagsThatImply(Guid tagDef) {
- using var db = dbFactory.CreateDbContext();
-
- var tagDefs = db.TagDefinitions
- .Include(td => td.ImplicitTags)
- .ToArray();
-
- var tags = new List<TagDefinition>() {
- db.TagDefinitions.First(td => td.Guid == tagDef)
- };
-
- while(true) {
- var toAdd = tagDefs
- .Where(td => td.ImplicitTags.Select(it => it.Guid).Intersect(tags.Select(td => td.Guid)).Any())
- .ExceptBy(tags.Select(td => td.Guid), td => td.Guid)
- .ToArray();
-
- if(toAdd.Count() == 0)
- break;
-
- tags.AddRange(toAdd);
- }
-
- return tags.ToArray();
- }
-
- public TagDefinition[] TagsThatImply(TagDefinition tagDef) =>
- TagsThatImply(tagDef.Guid);
-}
diff --git a/Services/UserService.cs b/Services/UserService.cs
deleted file mode 100644
index 9e79dc6..0000000
--- a/Services/UserService.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using Microsoft.AspNetCore.Cryptography.KeyDerivation;
-
-namespace HyperBooru.Services;
-
-public interface IUserService {
- public UserSessionState UserSessionState { get; }
-}
-
-public class UserService : IUserService {
- public UserSessionState UserSessionState =>
- globalUserService.GetSessionState(httpContext.Session.Id);
-
- private IHttpContextAccessor httpContextAccessor;
- private IGlobalUserService globalUserService;
-
- private HttpContext httpContext =>
- httpContextAccessor.HttpContext!;
-
- public UserService(
- IHttpContextAccessor httpContextAccessor,
- IGlobalUserService globalUserService) {
-
- this.httpContextAccessor = httpContextAccessor;
- this.globalUserService = globalUserService;
-
- // HTTP context session states are discarded if no values
- // are set. Set a dummy value so that the session state
- // will not be discarded later when we actually need it.
- httpContext.Session.SetInt32("Persist", 1);
- }
-
- public static string HashPassword(string password) =>
- Convert.ToBase64String(
- KeyDerivation.Pbkdf2(
- password,
- Array.Empty<byte>(),
- KeyDerivationPrf.HMACSHA512,
- 100_000,
- 512 / 8));
-}
-
-public interface IGlobalUserService {
- public UserSessionState GetSessionState(string id);
-}
-
-public class GlobalUserService : IGlobalUserService {
- // TODO: prune this list periodically
- private Dictionary<string, UserSessionState> sessionStates = new();
-
- public UserSessionState GetSessionState(string id) {
- sessionStates.TryGetValue(id, out var state);
-
- if(state is null) {
- state = new();
- sessionStates[id] = state;
- }
-
- return state;
- }
-}
-
-public record UserSessionState {
- public event UserSessionStateChange OnStateChange;
-
- public bool ShowNsfw {
- get => showNsfw;
- set {
- showNsfw = value;
- OnStateChange.Invoke(this);
- }
- }
-
- private bool showNsfw = false;
-}
-
-public delegate void UserSessionStateChange(UserSessionState sessionState);