feat(API): Adds method to modify the FileWatcherService: folders, time
This commit is contained in:
@@ -1,27 +1,52 @@
|
|||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
|
using Lactose.Services;
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
|
|
||||||
public class FolderRepository(LactoseDbContext context) : IFolderRepository, IAsyncDisposable {
|
public class FolderRepository(LactoseDbContext context, FileSystemScannerService scanner) : IFolderRepository, IAsyncDisposable {
|
||||||
public void Create(Folder folder) => context.Folders.Add(folder);
|
public void Create(Folder folder) {
|
||||||
|
context.Folders.Add(folder);
|
||||||
|
context.SaveChanges();
|
||||||
|
if (folder.Active) scanner.AddFolder(folder);
|
||||||
|
}
|
||||||
|
|
||||||
public void Update(Guid id, Folder folder) {
|
public void Update(Guid id, Folder folder) {
|
||||||
var origFolder = context.Folders.Find(id);
|
var origFolder = context.Folders.Find(id);
|
||||||
|
|
||||||
if (origFolder != null) {
|
if (origFolder == null) { return; }
|
||||||
origFolder.BasePath = folder.BasePath;
|
|
||||||
origFolder.Active = folder.Active;
|
var oldPath = origFolder;
|
||||||
context.Folders.Update(origFolder);
|
var hasChanged = origFolder.BasePath != folder.BasePath;
|
||||||
context.SaveChanges();
|
var wasActive = origFolder.Active;
|
||||||
|
|
||||||
|
|
||||||
|
origFolder.BasePath = folder.BasePath;
|
||||||
|
origFolder.Active = folder.Active;
|
||||||
|
context.Folders.Update(origFolder);
|
||||||
|
context.SaveChanges();
|
||||||
|
|
||||||
|
if (hasChanged && wasActive) {
|
||||||
|
scanner.RemoveFolder(oldPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasChanged && folder.Active) {}
|
||||||
|
|
||||||
|
if (folder.Active) scanner.AddFolder(origFolder);
|
||||||
|
else scanner.RemoveFolder(origFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Delete(Guid id) {
|
public void Delete(Guid id) {
|
||||||
var folder = context.Folders.Find(id);
|
var folder = context.Folders.Find(id);
|
||||||
if(folder != null) context.Folders.Remove(folder);
|
|
||||||
|
if (folder != null) {
|
||||||
|
context.Folders.Remove(folder);
|
||||||
|
scanner.RemoveFolder(folder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Folder> GetAllUntracked() => context.Folders.AsNoTracking();
|
||||||
|
|
||||||
public Folder? Get(Guid id) => context.Folders.Find(id);
|
public Folder? Get(Guid id) => context.Folders.Find(id);
|
||||||
|
|
||||||
public IEnumerable<Folder> GetAll() => context.Folders;
|
public IEnumerable<Folder> GetAll() => context.Folders;
|
||||||
|
|||||||
@@ -36,4 +36,6 @@ public interface IFolderRepository : IDisposable {
|
|||||||
/// Retrieves all folders.
|
/// Retrieves all folders.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<Folder> GetAll();
|
IEnumerable<Folder> GetAll();
|
||||||
|
|
||||||
|
IEnumerable<Folder> GetAllUntracked();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Butter.Dtos.Settings;
|
|||||||
using Butter.Settings;
|
using Butter.Settings;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
|
using Lactose.Utils;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using static System.String;
|
using static System.String;
|
||||||
|
|
||||||
@@ -11,72 +12,115 @@ namespace Lactose.Services;
|
|||||||
|
|
||||||
public class FileSystemScannerService(ILogger<FileSystemScannerService> logger, IServiceProvider serviceProvider)
|
public class FileSystemScannerService(ILogger<FileSystemScannerService> logger, IServiceProvider serviceProvider)
|
||||||
: BackgroundService {
|
: BackgroundService {
|
||||||
public bool IsRunning { get; private set; } = false;
|
public bool IsScanning { get; private set; } = false;
|
||||||
public bool IsInitialized { get; private set; } = false;
|
public bool IsEnabled { get; private set; } = false;
|
||||||
public TimeSpan ScanInterval { get; private set; }
|
public bool IsInitialized { get; private set; } = false;
|
||||||
List<Folder> folders = [];
|
readonly PeriodicTimer scanTimer = new(TimeSpan.FromMinutes(30));
|
||||||
readonly List<Task> tasks = [];
|
readonly Dictionary<string,CancellationTokenSource> folders = new();
|
||||||
|
readonly List<Task> tasks = [];
|
||||||
|
|
||||||
|
|
||||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) {
|
protected override Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||||
// Resolve the repositories from the service provider
|
// Resolve the repositories from the service provider
|
||||||
var settingsRepository = serviceProvider.GetRequiredService<ISettingsRepository>();
|
using (var settingsRepository = serviceProvider.GetRequiredService<ISettingsRepository>()) {
|
||||||
|
|
||||||
if (settingsRepository.Get(Settings.FolderScanEnabled.AsString())?.Value == "false") {
|
IsEnabled = settingsRepository.Get(Settings.FolderScanEnabled.AsString())?.Value != "false";
|
||||||
logger.LogInformation("Folder scanning is disabled. Service will not start.");
|
|
||||||
return Task.CompletedTask;
|
stoppingToken.Register(() => {
|
||||||
|
logger.LogInformation("Filesystem Scan Service stopping...");
|
||||||
|
folders.Values
|
||||||
|
.Where(tokenSource => !tokenSource.IsCancellationRequested)
|
||||||
|
.ForEach(tokenSource => {
|
||||||
|
tokenSource.Cancel();
|
||||||
|
tokenSource.Dispose();
|
||||||
|
});
|
||||||
|
folders.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
using ( var folderRepository = serviceProvider.GetRequiredService<IFolderRepository>()) {
|
||||||
|
folderRepository.GetAllUntracked()
|
||||||
|
.Where(f => f.Active)
|
||||||
|
.Select(f => f.BasePath)
|
||||||
|
.ForEach(AddFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Filesystem Scan Service starting...");
|
||||||
|
|
||||||
|
var value = settingsRepository.Get(Settings.FolderScanInterval.AsString())?.Value;
|
||||||
|
|
||||||
|
if (value != null && !int.TryParse(value, out var interval)) {
|
||||||
|
logger.LogWarning("Invalid scan interval setting. Defaulting to 30 minutes.");
|
||||||
|
interval = 30; // Default to 30 minutes if parsing fails
|
||||||
|
} else interval = int.Parse(value!);
|
||||||
|
|
||||||
|
SetTimerInterval(TimeSpan.FromMinutes(interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
var service = new Task(() => ServiceLogic(stoppingToken));
|
IsInitialized = true;
|
||||||
|
|
||||||
|
var service = new Task(() => TaskWatcher(stoppingToken));
|
||||||
service.Start();
|
service.Start();
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: This really needs a decent name.
|
void TaskWatcher(CancellationToken stoppingToken) {
|
||||||
void ServiceLogic(CancellationToken stoppingToken) {
|
|
||||||
// Resolve the repositories from the service provider
|
|
||||||
using var folderRepository = serviceProvider.GetRequiredService<IFolderRepository>();
|
|
||||||
using var settingsRepository = serviceProvider.GetRequiredService<ISettingsRepository>();
|
|
||||||
PeriodicTimer scanTimer;
|
|
||||||
logger.LogInformation("Filesystem Scan Service starting...");
|
|
||||||
|
|
||||||
var value = settingsRepository.Get(Settings.FolderScanInterval.AsString())?.Value;
|
|
||||||
|
|
||||||
if (value != null && !int.TryParse(value, out var interval)) {
|
|
||||||
logger.LogWarning("Invalid scan interval setting. Defaulting to 30 minutes.");
|
|
||||||
interval = 30; // Default to 30 minutes if parsing fails
|
|
||||||
} else interval = int.Parse(value!);
|
|
||||||
|
|
||||||
ScanInterval = TimeSpan.FromMinutes(interval);
|
|
||||||
logger.LogInformation($"Service will scan every {ScanInterval.TotalMinutes} minutes.");
|
|
||||||
scanTimer = new(ScanInterval);
|
|
||||||
IsInitialized = true;
|
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested) {
|
while (!stoppingToken.IsCancellationRequested) {
|
||||||
logger.LogInformation("Retrieving folder paths to scan...");
|
|
||||||
folders = folderRepository.GetAll().ToList();
|
scanTimer.WaitForNextTickAsync(stoppingToken);
|
||||||
|
|
||||||
// Wait for all tasks to complete before starting a new scan
|
// Wait for all tasks to complete before starting a new scan
|
||||||
if (tasks.Count > 0) {
|
if (IsScanning) {
|
||||||
logger.LogInformation("Waiting for {count} tasks to complete...", tasks.Count);
|
logger.LogInformation("Waiting for {count} tasks to complete...", tasks.Count);
|
||||||
Task.WaitAll(tasks.ToArray(), stoppingToken);
|
WaitTasks(stoppingToken);
|
||||||
IsRunning = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folders.Count != 0) {
|
if (IsEnabled && folders.Count != 0 && !IsScanning) {
|
||||||
logger.LogInformation("Found {count} folders to scan.", folders.Count);
|
logger.LogInformation("Found {count} folders to scan.", folders.Count);
|
||||||
logger.LogInformation("Starting Tasks for scanning folders...");
|
logger.LogInformation("Starting Tasks for scanning folders...");
|
||||||
|
|
||||||
// Launch a folder scan for each folder, passing the stopping token to each task
|
// Launch a folder scan for each folder, passing the stopping token to each task
|
||||||
folders.ForEach(folder => { tasks.Add(Task.Run(() => ScanFolder(folder.BasePath), stoppingToken)); });
|
folders.Where(entry => !entry.Value.IsCancellationRequested)
|
||||||
IsRunning = true;
|
.Select(entry => entry.Key)
|
||||||
} else {
|
.ToList()
|
||||||
logger.LogWarning("No folders to scan. Waiting for next scan interval...");
|
.ForEach(path => {
|
||||||
scanTimer.WaitForNextTickAsync(stoppingToken);
|
folders[path] = new CancellationTokenSource();
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var entry in folders) {
|
||||||
|
tasks.Add(Task.Run(() => ScanFolder(entry.Key, entry.Value.Token), entry.Value.Token));
|
||||||
|
}
|
||||||
|
IsScanning = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanFolder(string path) {
|
void WaitTasks(CancellationToken stoppingToken) {
|
||||||
using var assetRepository = serviceProvider.GetRequiredService<IAssetRepository>();
|
while (IsScanning) {
|
||||||
|
try {
|
||||||
|
Task.WaitAll(tasks.ToArray(), stoppingToken);
|
||||||
|
tasks.Clear();
|
||||||
|
IsScanning = false;
|
||||||
|
} catch (OperationCanceledException e) {
|
||||||
|
logger.LogInformation("Scanning Service was cancelled.");
|
||||||
|
IsScanning = false;
|
||||||
|
IsEnabled = false;
|
||||||
|
}
|
||||||
|
catch (AggregateException e) {
|
||||||
|
if (!e.InnerExceptions.All(ie => ie is OperationCanceledException)) throw;
|
||||||
|
logger.LogDebug("A Folder Has been removed. Continuing to scan.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanFolder(string path, CancellationToken stoppingToken) {
|
||||||
|
|
||||||
|
|
||||||
|
if (stoppingToken.IsCancellationRequested) {
|
||||||
|
stoppingToken.ThrowIfCancellationRequested();
|
||||||
|
};
|
||||||
|
|
||||||
List<string> filePaths = [];
|
List<string> filePaths = [];
|
||||||
List<string> folderPaths = [];
|
List<string> folderPaths = [];
|
||||||
@@ -88,51 +132,99 @@ public class FileSystemScannerService(ILogger<FileSystemScannerService> logger,
|
|||||||
} catch (Exception e) { logger.LogError(e, "Error scanning folder {folder}", path); }
|
} catch (Exception e) { logger.LogError(e, "Error scanning folder {folder}", path); }
|
||||||
|
|
||||||
// Launch a folder scan for each subfolder
|
// Launch a folder scan for each subfolder
|
||||||
folderPaths.ForEach(folderPath => { tasks.Add(Task.Run(() => ScanFolder(folderPath))); });
|
folderPaths.ForEach(folderPath => { tasks.Add(Task.Run(() => ScanFolder(folderPath, stoppingToken), stoppingToken)); });
|
||||||
|
|
||||||
|
using var assetRepository = serviceProvider.GetRequiredService<IAssetRepository>();
|
||||||
|
|
||||||
// Process all files
|
// Process all files
|
||||||
filePaths.ForEach(filePath => {
|
filePaths.ForEach(filePath => {
|
||||||
|
|
||||||
// Check if the file is already in the database
|
// Check if the file is already in the database
|
||||||
if (assetRepository.FindByPath(filePath) != null) return;
|
if (assetRepository.FindByPath(filePath) != null) return;
|
||||||
|
|
||||||
// Get the file info
|
var asset = AssetFromPath(filePath);
|
||||||
var finfo = new FileInfo(filePath);
|
|
||||||
|
|
||||||
// Determine the type of the file
|
if (asset == null) return;
|
||||||
string ext = finfo.Extension.ToLower();
|
|
||||||
EAssetType? type;
|
|
||||||
|
|
||||||
// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident
|
|
||||||
var mimeImg = MimeTypes.Image.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
|
||||||
var mimeVid = MimeTypes.Video.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
|
||||||
|
|
||||||
if (mimeImg.MimeType != Empty) type = EAssetType.Image;
|
|
||||||
else if (mimeVid.MimeType != Empty) type = EAssetType.Video;
|
|
||||||
else return;
|
|
||||||
|
|
||||||
// Create a new asset
|
|
||||||
var asset = new Asset() {
|
|
||||||
OriginalPath = finfo.FullName,
|
|
||||||
OriginalFilename = finfo.Name,
|
|
||||||
Type = type.Value,
|
|
||||||
IsPubliclyShared = false,
|
|
||||||
CreatedAt = finfo.CreationTime,
|
|
||||||
UpdatedAt = finfo.LastWriteTime,
|
|
||||||
MimeType = type.Value switch {
|
|
||||||
EAssetType.Image => mimeImg.MimeType,
|
|
||||||
EAssetType.Video => mimeVid.MimeType,
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
},
|
|
||||||
FileSize = finfo.Length,
|
|
||||||
Hash = []
|
|
||||||
};
|
|
||||||
|
|
||||||
//TODO: check if the file is already in the database
|
|
||||||
// Add the asset to the database
|
// Add the asset to the database
|
||||||
try { assetRepository.Insert(asset); } catch (DuplicateNameException e) {
|
try { assetRepository.Insert(asset); } catch (DuplicateNameException e) {
|
||||||
logger.LogError(e, $"Duplicate asset name \"{finfo.FullName}\", skipped.");
|
logger.LogError(e, $"Duplicate asset name \"{asset.OriginalFilename}\", skipped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Asset? AssetFromPath(string filePath) {
|
||||||
|
|
||||||
|
// Get the file info
|
||||||
|
var finfo = new FileInfo(filePath);
|
||||||
|
|
||||||
|
// Determine the type of the file
|
||||||
|
string ext = finfo.Extension.ToLower();
|
||||||
|
EAssetType? type;
|
||||||
|
|
||||||
|
// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident
|
||||||
|
var mimeImg = MimeTypes.Image.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
||||||
|
var mimeVid = MimeTypes.Video.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
||||||
|
|
||||||
|
if (mimeImg.MimeType != Empty) type = EAssetType.Image;
|
||||||
|
else if (mimeVid.MimeType != Empty) type = EAssetType.Video;
|
||||||
|
else return null;
|
||||||
|
|
||||||
|
// Create a new asset
|
||||||
|
var asset = new Asset() {
|
||||||
|
OriginalPath = finfo.FullName,
|
||||||
|
OriginalFilename = finfo.Name,
|
||||||
|
Type = type.Value,
|
||||||
|
IsPubliclyShared = false,
|
||||||
|
CreatedAt = finfo.CreationTime,
|
||||||
|
UpdatedAt = finfo.LastWriteTime,
|
||||||
|
MimeType = type.Value switch {
|
||||||
|
EAssetType.Image => mimeImg.MimeType,
|
||||||
|
EAssetType.Video => mimeVid.MimeType,
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
},
|
||||||
|
FileSize = finfo.Length,
|
||||||
|
Hash = []
|
||||||
|
};
|
||||||
|
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTimerInterval(TimeSpan interval) {
|
||||||
|
scanTimer.Period = interval;
|
||||||
|
logger.LogInformation($"Service will scan every {scanTimer.Period.TotalMinutes} minutes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFolder(string folder) {
|
||||||
|
if (folders.ContainsKey(folder)) {
|
||||||
|
logger.LogWarning("Folder {folder} already exists", folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
folders.Add(folder, new CancellationTokenSource());
|
||||||
|
logger.LogInformation("Added folder {folder}", folder);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFolder(Folder folder) => AddFolder(folder.BasePath);
|
||||||
|
public void RemoveFolder(string folder) {
|
||||||
|
if (!folders.TryGetValue(folder, out CancellationTokenSource? cancellationToken)) {
|
||||||
|
logger.LogWarning("Folder {folder} does not exist", folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.Cancel();
|
||||||
|
cancellationToken.Dispose();
|
||||||
|
folders.Remove(folder);
|
||||||
|
logger.LogInformation("Removed folder {folder}", folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveFolder(Folder folder) => RemoveFolder(folder.BasePath);
|
||||||
|
|
||||||
|
public void Enable() {
|
||||||
|
IsEnabled = true;
|
||||||
|
logger.LogInformation("Service is now enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disable() => IsEnabled = false;
|
||||||
}
|
}
|
||||||
10
Lactose/Utils/EnumerableExtensions.cs
Normal file
10
Lactose/Utils/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Lactose.Utils;
|
||||||
|
|
||||||
|
public static class EnumerableExtensions {
|
||||||
|
public static void ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action) {
|
||||||
|
|
||||||
|
foreach (var item in source) {
|
||||||
|
action(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user