FreeTubeSyncer/FreeTubeSyncer/REST/Syncer.cs
Mario Steele a88da7f6d0 Updated Syncer
Added GetLastUpdated() to ISyncer interface.
Updated code to use new /ping endpoint.
Added GetLastUpdated() REST API call to fetch when the database was last
updated.
Clarified logging for when an Update is from a file, or from the REST
API Server.
Added logging for when a nw File Entry is processed, or when a File
Entry is Updated.
2025-07-31 14:26:20 -05:00

241 lines
No EOL
7.4 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using FreeTubeSyncer.Library;
using FreeTubeSyncer.Models.DatabaseModels;
using RestSharp;
namespace FreeTubeSyncer.REST;
public interface ISyncer
{
Task ReadDatabase();
Task FetchDatabase();
void HandleDatabaseChange(string dbName, string entryObject);
void Sync();
bool IsDirty();
void Enable();
void Disable();
bool IsEnabled();
void UpdateBaseUrl(string baseUrl);
void SetEnabled(bool enabled);
Task<bool> PingApi();
Task<DateTime> GetLastUpdated();
}
public class Syncer<T> : ISyncer where T : class, IDataModel, new()
{
private List<T> _entries = new List<T>();
private RestClient _client;
private string _dbPath;
private string _dbName;
private string _restEndpoint;
private DBSyncWatcher _watcher;
private bool _isDirty = false;
private bool _syncing = false;
private bool _enabled = true;
public Syncer(DBSyncWatcher watcher, string dbPath, string dbName, string restBaseUrl, string restEndpoint)
{
_watcher = watcher;
_watcher.WatchFiles[dbName] = typeof(T);
_watcher.OnDatabaseChange += HandleDatabaseChange;
_client = new RestClient(new RestClientOptions(restBaseUrl));
_dbPath = dbPath;
_dbName = dbName;
_restEndpoint = restEndpoint;
}
public bool IsDirty() => _isDirty;
public bool IsEnabled() => _enabled;
public void Enable() => _enabled = true;
public void Disable() => _enabled = false;
public void SetEnabled(bool enabled) => _enabled = enabled;
public void UpdateBaseUrl(string baseUrl)
{
_client.Dispose();
_client = new RestClient(new RestClientOptions(baseUrl));
}
public async Task<bool> PingApi()
{
// TODO: Replace with Logger
Console.WriteLine($"Pinging API at {_client.BuildUri(new RestRequest("/ping"))}...");
try
{
var res = await _client.GetAsync<Ping>(new RestRequest("/ping"));
if (res == null)
{
// TODO: Replace with Logger
Console.WriteLine($"Ping returned null, not the server we are looking for!");
return false;
}
if (res.AppVersion == "0.1.3")
{
// TODO: Replace with Logger
Console.WriteLine($"Server Online! {res.AppVersion}");
return true;
}
}
catch (Exception ex)
{
// TODO: Replace with Logger
Console.WriteLine($"Network Error: {ex.Message}, API not alive.");
return false;
}
// TODO: Replace with Logger
Console.WriteLine("Responded with something other then 404, API not what we expected.");
return false;
}
public async Task<DateTime> GetLastUpdated()
{
var res = await _client.GetAsync<UpdateCheck>("/ping/lastUpdated");
return res?.LastUpdated ?? DateTime.MinValue;
}
public async Task ReadDatabase()
{
if (!_enabled) return;
var lines = File.ReadAllLines(_dbPath);
foreach (var entry in lines)
{
if (entry == "") continue;
T? item;
try
{
item = JsonSerializer.Deserialize<T>(entry, GlobalJsonOptions.Options);
}
catch (Exception ex)
{
var jobj = JsonSerializer.Deserialize<JsonObject>(entry, GlobalJsonOptions.Options);
item = new T();
item.MarshalData(jobj["_id"].GetValue<string>(), entry);
}
if (item == null) continue;
item.MarshalData(item.Id(), entry);
if (_entries.Any(x => x.EqualId(item.Id())))
_entries.RemoveAll(x => x.EqualId(item.Id()));
_entries.Add(item);
// TODO: Replace with Logger
Console.WriteLine($"Posting to REST: {item.Id()}");
await _client.PostJsonAsync<T>(_restEndpoint, item);
}
}
public async Task FetchDatabase()
{
if (!_enabled) return;
var entries = await _client.GetAsync<List<T>>(_restEndpoint);
if (entries == null) return;
foreach (var entry in entries)
{
if (_entries.Any(x => x.EqualId(entry.Id())))
{
var data = _entries.First(x => x.EqualId(entry.Id()));
if (data.Equals(entry)) continue;
// TODO: Replace with Logger
Console.WriteLine($"Updated Entry from REST for {_dbName} - {entry.Id()}");
_entries.RemoveAll(x => x.EqualId(entry.Id()));
}
else
{
// TODO: Replace with Logger
Console.WriteLine($"New Entry from REST for {_dbName} - {entry.Id()}");
}
_entries.Add(entry);
_isDirty = true;
}
}
public async void HandleDatabaseChange(string dbName, string entryObject)
{
if (!_enabled) return;
if (dbName != _dbName)
return;
if (_syncing)
return;
T? entry;
try
{
entry = JsonSerializer.Deserialize<T>(entryObject, GlobalJsonOptions.Options);
}
catch (Exception ex)
{
try
{
var jobj = JsonSerializer.Deserialize<JsonObject>(entryObject, GlobalJsonOptions.Options);
entry = new T();
entry.MarshalData(jobj["_id"].GetValue<string>(), entryObject);
}
catch (Exception iex)
{
// TODO: Replace with Logger
Console.WriteLine($"Failed to parse line: {entryObject}\nMessage: {iex.Message}");
entry = null;
}
}
if (entry == null) return;
entry.MarshalData(entry.Id(), entryObject);
if (_entries.Any(x => x.EqualId(entry.Id())))
{
var data = _entries.First(x => x.EqualId(entry.Id()));
if (data.Equals(entry)) return;
// TODO: Replace with Logger
Console.WriteLine($"File Entry {entry.Id()} updated for {_dbName}");
_entries.RemoveAll(x => x.EqualId(entry.Id()));
}
else
{
// TODO: Replace with Logger
Console.WriteLine($"New File Entry {entry.Id()} for {_dbName}");
}
_entries.Add(entry);
await _client.PostJsonAsync<T>(_restEndpoint, entry);
}
public void Sync()
{
if (!_enabled) return;
if (!_isDirty)
return;
_syncing = true;
// TODO: Replace with Logger
Console.WriteLine($"Syncing {_dbPath}...");
var json = new List<string>();
foreach (var entry in _entries)
json.Add(entry.JsonData());
_watcher.Locked = true;
using (var fh = File.OpenWrite(_dbPath))
{
foreach (var line in json)
fh.Write(Encoding.UTF8.GetBytes(line + "\n"));
fh.Flush();
fh.Close();
}
_watcher.Locked = false;
// TODO: Replace with Logger
Console.WriteLine($"Updated {_dbPath}.");
_isDirty = false;
_syncing = false;
}
}