feat(API): Adds method to modify the FileWatcherService: folders, time
This commit is contained in:
@@ -1,26 +1,51 @@
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Lactose.Services;
|
||||
|
||||
namespace Lactose.Repositories;
|
||||
|
||||
public class FolderRepository(LactoseDbContext context) : IFolderRepository, IAsyncDisposable {
|
||||
public void Create(Folder folder) => context.Folders.Add(folder);
|
||||
public class FolderRepository(LactoseDbContext context, FileSystemScannerService scanner) : IFolderRepository, IAsyncDisposable {
|
||||
public void Create(Folder folder) {
|
||||
context.Folders.Add(folder);
|
||||
context.SaveChanges();
|
||||
if (folder.Active) scanner.AddFolder(folder);
|
||||
}
|
||||
|
||||
public void Update(Guid id, Folder folder) {
|
||||
var origFolder = context.Folders.Find(id);
|
||||
|
||||
if (origFolder != null) {
|
||||
origFolder.BasePath = folder.BasePath;
|
||||
origFolder.Active = folder.Active;
|
||||
context.Folders.Update(origFolder);
|
||||
context.SaveChanges();
|
||||
if (origFolder == null) { return; }
|
||||
|
||||
var oldPath = origFolder;
|
||||
var hasChanged = origFolder.BasePath != folder.BasePath;
|
||||
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) {
|
||||
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);
|
||||
|
||||
|
||||
@@ -36,4 +36,6 @@ public interface IFolderRepository : IDisposable {
|
||||
/// Retrieves all folders.
|
||||
/// </summary>
|
||||
IEnumerable<Folder> GetAll();
|
||||
|
||||
IEnumerable<Folder> GetAllUntracked();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Butter.Dtos.Settings;
|
||||
using Butter.Settings;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Utils;
|
||||
using System.Data;
|
||||
using static System.String;
|
||||
|
||||
@@ -11,76 +12,119 @@ namespace Lactose.Services;
|
||||
|
||||
public class FileSystemScannerService(ILogger<FileSystemScannerService> logger, IServiceProvider serviceProvider)
|
||||
: BackgroundService {
|
||||
public bool IsRunning { get; private set; } = false;
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
public TimeSpan ScanInterval { get; private set; }
|
||||
List<Folder> folders = [];
|
||||
readonly List<Task> tasks = [];
|
||||
|
||||
public bool IsScanning { get; private set; } = false;
|
||||
public bool IsEnabled { get; private set; } = false;
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
readonly PeriodicTimer scanTimer = new(TimeSpan.FromMinutes(30));
|
||||
readonly Dictionary<string,CancellationTokenSource> folders = new();
|
||||
readonly List<Task> tasks = [];
|
||||
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
// 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") {
|
||||
logger.LogInformation("Folder scanning is disabled. Service will not start.");
|
||||
return Task.CompletedTask;
|
||||
IsEnabled = settingsRepository.Get(Settings.FolderScanEnabled.AsString())?.Value != "false";
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
var service = new Task(() => ServiceLogic(stoppingToken));
|
||||
var service = new Task(() => TaskWatcher(stoppingToken));
|
||||
service.Start();
|
||||
return service;
|
||||
}
|
||||
|
||||
//TODO: This really needs a decent name.
|
||||
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;
|
||||
void TaskWatcher(CancellationToken stoppingToken) {
|
||||
|
||||
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
|
||||
if (tasks.Count > 0) {
|
||||
if (IsScanning) {
|
||||
logger.LogInformation("Waiting for {count} tasks to complete...", tasks.Count);
|
||||
Task.WaitAll(tasks.ToArray(), stoppingToken);
|
||||
IsRunning = false;
|
||||
WaitTasks(stoppingToken);
|
||||
}
|
||||
|
||||
if (folders.Count != 0) {
|
||||
if (IsEnabled && folders.Count != 0 && !IsScanning) {
|
||||
logger.LogInformation("Found {count} folders to scan.", folders.Count);
|
||||
logger.LogInformation("Starting Tasks for scanning folders...");
|
||||
|
||||
// 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)); });
|
||||
IsRunning = true;
|
||||
} else {
|
||||
logger.LogWarning("No folders to scan. Waiting for next scan interval...");
|
||||
scanTimer.WaitForNextTickAsync(stoppingToken);
|
||||
folders.Where(entry => !entry.Value.IsCancellationRequested)
|
||||
.Select(entry => entry.Key)
|
||||
.ToList()
|
||||
.ForEach(path => {
|
||||
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) {
|
||||
using var assetRepository = serviceProvider.GetRequiredService<IAssetRepository>();
|
||||
void WaitTasks(CancellationToken stoppingToken) {
|
||||
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> folderPaths = [];
|
||||
|
||||
|
||||
// Get all files and folders in the folder
|
||||
try {
|
||||
filePaths.AddRange(Directory.GetFiles(path));
|
||||
@@ -88,51 +132,99 @@ public class FileSystemScannerService(ILogger<FileSystemScannerService> logger,
|
||||
} catch (Exception e) { logger.LogError(e, "Error scanning folder {folder}", path); }
|
||||
|
||||
// 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
|
||||
filePaths.ForEach(filePath => {
|
||||
|
||||
// Check if the file is already in the database
|
||||
if (assetRepository.FindByPath(filePath) != null) return;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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
|
||||
|
||||
var asset = AssetFromPath(filePath);
|
||||
|
||||
if (asset == null) return;
|
||||
|
||||
// Add the asset to the database
|
||||
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