229 lines
6.7 KiB
C#
229 lines
6.7 KiB
C#
using System.Text.Json;
|
|
using Journal.Core.Models;
|
|
|
|
namespace Journal.Core.Repositories;
|
|
|
|
public class FileFragmentRepository : IFragmentRepository
|
|
{
|
|
private readonly Lock _lock = new();
|
|
private readonly string _storagePath;
|
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
|
{
|
|
WriteIndented = true
|
|
};
|
|
private readonly List<Fragment> _store;
|
|
|
|
public FileFragmentRepository() : this(storagePath: null)
|
|
{
|
|
}
|
|
|
|
public FileFragmentRepository(string? storagePath)
|
|
{
|
|
_storagePath = ResolveStoragePath(storagePath);
|
|
_store = LoadStore(_storagePath);
|
|
}
|
|
|
|
public Task<List<Fragment>> GetAllAsync()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return Task.FromResult(_store.ToList());
|
|
}
|
|
}
|
|
|
|
public Task<Fragment?> GetByIdAsync(Guid id)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return Task.FromResult(_store.FirstOrDefault(f => f.Id == id));
|
|
}
|
|
}
|
|
|
|
public Task AddAsync(Fragment fragment)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(fragment);
|
|
lock (_lock)
|
|
{
|
|
Normalize(fragment);
|
|
_store.Add(fragment);
|
|
SaveStoreLocked();
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<bool> RemoveAsync(Guid id)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var item = _store.FirstOrDefault(f => f.Id == id);
|
|
if (item is null)
|
|
return Task.FromResult(false);
|
|
|
|
var removed = _store.Remove(item);
|
|
if (removed)
|
|
SaveStoreLocked();
|
|
return Task.FromResult(removed);
|
|
}
|
|
}
|
|
|
|
public Task<bool> UpdateAsync(
|
|
Guid id,
|
|
string? type = null,
|
|
string? description = null,
|
|
IEnumerable<string>? tags = null,
|
|
DateTimeOffset? time = null)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var item = _store.FirstOrDefault(f => f.Id == id);
|
|
if (item is null)
|
|
return Task.FromResult(false);
|
|
|
|
if (type != null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(type))
|
|
throw new ArgumentException("Type cannot be empty", nameof(type));
|
|
item.Type = type.Trim();
|
|
}
|
|
|
|
if (description != null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(description))
|
|
throw new ArgumentException("Description cannot be empty", nameof(description));
|
|
item.Description = description.Trim();
|
|
}
|
|
|
|
if (tags != null)
|
|
{
|
|
item.Tags = [..
|
|
tags.Where(t => !string.IsNullOrWhiteSpace(t))
|
|
.Select(t => t.Trim())];
|
|
}
|
|
|
|
if (time.HasValue)
|
|
item.Time = time.Value;
|
|
|
|
SaveStoreLocked();
|
|
return Task.FromResult(true);
|
|
}
|
|
}
|
|
|
|
public Task<List<Fragment>> GetByTagAsync(string tag)
|
|
{
|
|
var q = tag?.Trim();
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
return Task.FromResult(new List<Fragment>());
|
|
|
|
lock (_lock)
|
|
{
|
|
var items = _store
|
|
.Where(f => f.Tags.Any(t => t.Contains(q, StringComparison.OrdinalIgnoreCase)))
|
|
.ToList();
|
|
return Task.FromResult(items);
|
|
}
|
|
}
|
|
|
|
public Task<List<Fragment>> GetByTypeAsync(string type)
|
|
{
|
|
var q = type?.Trim();
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
return Task.FromResult(new List<Fragment>());
|
|
|
|
lock (_lock)
|
|
{
|
|
var items = _store
|
|
.Where(f => f.Type.Contains(q, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
return Task.FromResult(items);
|
|
}
|
|
}
|
|
|
|
public Task<List<Fragment>> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
|
{
|
|
var qType = type?.Trim();
|
|
var qTag = tag?.Trim();
|
|
|
|
lock (_lock)
|
|
{
|
|
IEnumerable<Fragment> results = _store;
|
|
|
|
if (!string.IsNullOrWhiteSpace(qType))
|
|
results = results.Where(f => f.Type.Contains(qType, StringComparison.OrdinalIgnoreCase));
|
|
if (!string.IsNullOrWhiteSpace(qTag))
|
|
results = results.Where(f => f.Tags.Any(t => t.Contains(qTag, StringComparison.OrdinalIgnoreCase)));
|
|
if (timeAfter.HasValue)
|
|
results = results.Where(f => f.Time > timeAfter.Value);
|
|
|
|
return Task.FromResult(results.ToList());
|
|
}
|
|
}
|
|
|
|
private static string ResolveStoragePath(string? storagePath)
|
|
{
|
|
var configured = storagePath;
|
|
if (string.IsNullOrWhiteSpace(configured))
|
|
configured = Environment.GetEnvironmentVariable("JOURNAL_FRAGMENT_STORE_PATH");
|
|
if (string.IsNullOrWhiteSpace(configured))
|
|
configured = Path.Combine(Environment.CurrentDirectory, ".journal-sidecar", "fragments.json");
|
|
|
|
return Path.GetFullPath(configured);
|
|
}
|
|
|
|
private List<Fragment> LoadStore(string path)
|
|
{
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrWhiteSpace(directory))
|
|
Directory.CreateDirectory(directory);
|
|
|
|
if (!File.Exists(path))
|
|
return [];
|
|
|
|
var json = File.ReadAllText(path);
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
return [];
|
|
|
|
var docs = JsonSerializer.Deserialize<List<FragmentDocument>>(json, _jsonOptions) ?? [];
|
|
return [.. docs.Select(d => new Fragment(d.Id, d.Type, d.Description, d.Time, d.Tags))];
|
|
}
|
|
|
|
private void SaveStoreLocked()
|
|
{
|
|
var directory = Path.GetDirectoryName(_storagePath);
|
|
if (!string.IsNullOrWhiteSpace(directory))
|
|
Directory.CreateDirectory(directory);
|
|
|
|
var docs = _store.Select(f => new FragmentDocument
|
|
{
|
|
Id = f.Id,
|
|
Type = f.Type,
|
|
Description = f.Description,
|
|
Time = f.Time,
|
|
Tags = [.. f.Tags]
|
|
}).ToList();
|
|
var json = JsonSerializer.Serialize(docs, _jsonOptions);
|
|
|
|
var tempPath = _storagePath + ".tmp";
|
|
File.WriteAllText(tempPath, json);
|
|
File.Copy(tempPath, _storagePath, overwrite: true);
|
|
File.Delete(tempPath);
|
|
}
|
|
|
|
private static void Normalize(Fragment fragment)
|
|
{
|
|
fragment.Type = fragment.Type.Trim();
|
|
fragment.Description = fragment.Description.Trim();
|
|
fragment.Tags = [..
|
|
fragment.Tags.Where(t => !string.IsNullOrWhiteSpace(t))
|
|
.Select(t => t.Trim())];
|
|
}
|
|
|
|
private sealed class FragmentDocument
|
|
{
|
|
public Guid Id { get; init; }
|
|
public string Type { get; init; } = "";
|
|
public string Description { get; init; } = "";
|
|
public DateTimeOffset Time { get; init; }
|
|
public List<string> Tags { get; init; } = [];
|
|
}
|
|
}
|