431 lines
16 KiB
C#
431 lines
16 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
using Blazored.LocalStorage;
|
|
using Blog3000.Shared;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Net.Http;
|
|
using Markdig.Extensions.Tables;
|
|
using Blog3000.Client.Services;
|
|
using System.Security.Cryptography;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
namespace Blog3000.Client
|
|
{
|
|
/// <summary>
|
|
/// TODO: Add all localStorage Ops regarding BlogsPosts here
|
|
/// - Updater
|
|
/// - Topic-Fetcher
|
|
/// - ...
|
|
/// </summary>
|
|
public class BlogDb: IDisposable
|
|
{
|
|
public static string DBKEY_PREFIX_HDR = "blogdbhdr-";
|
|
public static string DBKEY_PREFIX_CONF = "blogdbconf-";
|
|
public static string DBKEY_PREFIX_BLOGCACHE = "blogcache-";
|
|
public static string DBKEY_PREFIX_IMGCACHE = "imgcached-";
|
|
|
|
public event EventHandler BlogPostsChanged;
|
|
|
|
private readonly ILocalStorageService localStorage;
|
|
private readonly ISyncLocalStorageService syncLocalStorage;
|
|
private readonly NetworkStatus networkStatus;
|
|
private readonly HttpClient httpClient;
|
|
private readonly ImgCacheStorage cache;
|
|
|
|
|
|
|
|
public BlogDb(ILocalStorageService ls, ISyncLocalStorageService sls, ImgCacheStorage cache, NetworkStatus ns, HttpClient http)
|
|
{
|
|
this.localStorage = ls;
|
|
this.syncLocalStorage = sls;
|
|
this.networkStatus = ns;
|
|
this.cache = cache;
|
|
this.httpClient = http;
|
|
|
|
localStorage.Changed += LocalStorage_Changed;
|
|
}
|
|
|
|
|
|
public void Dispose()
|
|
{
|
|
localStorage.Changed -= LocalStorage_Changed;
|
|
}
|
|
|
|
|
|
public enum FetchStatus
|
|
{
|
|
OK,
|
|
NotFound,
|
|
NetError,
|
|
Offline
|
|
}
|
|
|
|
|
|
|
|
private bool? _isEagerLoading = null;
|
|
|
|
public async Task<bool> IsEagerLoading()
|
|
{
|
|
if (_isEagerLoading != null) return _isEagerLoading == true;
|
|
_isEagerLoading = await localStorage.GetItemAsync<bool?>($"{DBKEY_PREFIX_CONF}IsEagerLoading") == true;
|
|
return _isEagerLoading == true;
|
|
}
|
|
|
|
|
|
public void SetEagerLoading(bool enabled)
|
|
{
|
|
_isEagerLoading = enabled;
|
|
localStorage.SetItemAsync<bool?>($"{DBKEY_PREFIX_CONF}IsEagerLoading", enabled);
|
|
}
|
|
|
|
|
|
public async Task<bool> AreImagesCached(string postId)
|
|
{
|
|
var imgsLoaded = await localStorage.GetItemAsync<bool?>($"{DBKEY_PREFIX_IMGCACHE}{postId}");
|
|
|
|
if (imgsLoaded != null && true == imgsLoaded)
|
|
{
|
|
// Seems to have loaded once, check if still in cache
|
|
var bh = await GetHeaderAsync(postId);
|
|
if (bh == null) return false; // Should not occur but in case
|
|
if (bh.ImageRefs != null) {
|
|
var urls = new List<string>();
|
|
foreach (var img in bh.ImageRefs)
|
|
{
|
|
// Images are cached by their id
|
|
var url = $"BlogPosts/{postId}/imgref/{img.Key}";
|
|
urls.Add(url);
|
|
}
|
|
var s = await cache.AreCached(urls.ToArray());
|
|
System.Diagnostics.Debug.WriteLine($"ImageCacheCheck: Found {s} for {postId}");
|
|
// Not in cache (anymore)?
|
|
if (!s) return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Get headers of all posts available (previously copied to localStorage)
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public async IAsyncEnumerable<BlogPostHeader> GetHeadersAsync()
|
|
{
|
|
var res = await localStorage.GetItemAsync<List<BlogPostHeader>>(BlogDb.DBKEY_PREFIX_HDR + "remotePosts");
|
|
if (res != null)
|
|
{
|
|
foreach (var b in res)
|
|
{
|
|
yield return b;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Get a header of posts available (previously copied to localStorage)
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <returns></returns>
|
|
public async Task<BlogPostHeader> GetHeaderAsync(string id)
|
|
{
|
|
if (id != null)
|
|
{
|
|
var res = await localStorage.GetItemAsync<List<BlogPostHeader>>(BlogDb.DBKEY_PREFIX_HDR + "remotePosts");
|
|
if (res != null)
|
|
{
|
|
foreach (var b in res)
|
|
{
|
|
if (id.Equals(b.Id)) return b;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Get headers from locally cached posts
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public async IAsyncEnumerable<BlogPostHeader> GetHeadersFromCachedPostsAsync()
|
|
{
|
|
int n = await localStorage.LengthAsync();
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
var key = await localStorage.KeyAsync(i);
|
|
if (key != null && key.StartsWith(BlogDb.DBKEY_PREFIX_BLOGCACHE))
|
|
{
|
|
var bpt = await localStorage.GetItemAsync<BlogPost>(key);
|
|
if (bpt != null)
|
|
{
|
|
var bp = bpt.ToBlogPost();
|
|
yield return bp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Get a locally cached post. Optionally fetch images from remote. Optionally fetch post from remote.
|
|
/// </summary>
|
|
/// <param name="postId"></param>
|
|
/// <param name="tryCacheImgs"></param>
|
|
/// <param name="tryRemote">when true try loading uncached post, or in non-eager-loading mode refresh cached post, which has been changed TBD!! </param>
|
|
/// <returns></returns>
|
|
public async Task<(BlogPost, FetchStatus)> GetCachedPostAsync(string postId, bool tryCacheImgs, bool tryRemote = false)
|
|
{
|
|
var res = await localStorage.GetItemAsync<BlogPost>($"{DBKEY_PREFIX_BLOGCACHE}{postId}");
|
|
bool needsSync = false;
|
|
|
|
if (!(await IsEagerLoading()) && tryRemote && res != null && networkStatus.IsOnline /* makes no sense if offline */)
|
|
{
|
|
var bph = await GetHeaderAsync(postId);
|
|
if (bph != null)
|
|
{
|
|
if (! String.Equals(bph.Checksum, res.Checksum))
|
|
{
|
|
// Invalidate the result
|
|
System.Diagnostics.Debug.WriteLine("Post has been changed, and not refresh via auto-eagerloading, so forceing sync");
|
|
needsSync = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
if (!needsSync)
|
|
{
|
|
//
|
|
// This check is only needed, when the post is cached, since syncing
|
|
// includeds the images anyway if tryCacheImgs==true
|
|
//
|
|
var imgsLoaded = await AreImagesCached(postId);
|
|
if (!imgsLoaded && tryCacheImgs)
|
|
{
|
|
if (networkStatus.IsOnline)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine("Images not chached, forcing sync");
|
|
needsSync = true;
|
|
}
|
|
else
|
|
{
|
|
System.Diagnostics.Debug.WriteLine("Images not chached, but offline, so not forcing sync");
|
|
}
|
|
}
|
|
}
|
|
|
|
var status = FetchStatus.OK;
|
|
|
|
if (needsSync || res == null)
|
|
{
|
|
if (!tryRemote)
|
|
{
|
|
status = FetchStatus.NotFound;
|
|
}
|
|
else
|
|
{
|
|
var (sb, sres) = await SyncPost(postId, tryCacheImgs, false);
|
|
switch (sres)
|
|
{
|
|
case SyncStatus.OK: res = sb; break;
|
|
case SyncStatus.NetError: status = FetchStatus.NetError; break;
|
|
case SyncStatus.Offline: status = FetchStatus.Offline; break;
|
|
case SyncStatus.NotFound: status = FetchStatus.NotFound; break;
|
|
default:
|
|
throw new InvalidOperationException("unknown result");
|
|
}
|
|
|
|
if (status == FetchStatus.NotFound)
|
|
{
|
|
// When remote say not-found, ensure we won't handout cached data anymore
|
|
res = null;
|
|
}
|
|
else if (status != FetchStatus.OK && res != null)
|
|
{
|
|
// We can use the cached version, so return a Offline-Status event in Net-Error-case
|
|
status = FetchStatus.Offline;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (res, status);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Get locally cached posts withText
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public async IAsyncEnumerable<BlogPost> GetCachedPostsAsync()
|
|
{
|
|
int n = await localStorage.LengthAsync();
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
var key = await localStorage.KeyAsync(i);
|
|
if (key != null && key.StartsWith(BlogDb.DBKEY_PREFIX_BLOGCACHE))
|
|
{
|
|
var bpt = await localStorage.GetItemAsync<BlogPost>(key);
|
|
yield return bpt;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public enum SyncStatus
|
|
{
|
|
OK,
|
|
Offline,
|
|
NotFound,
|
|
NetError
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Sync a remote post with the local cache
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <param name="includeImgs"></param>
|
|
/// <param name="removeOld"></param>
|
|
/// <returns></returns>
|
|
public async Task<(BlogPost, SyncStatus)> SyncPost(string id, bool includeImgs, bool removeOld=true)
|
|
{
|
|
var status = SyncStatus.OK;
|
|
BlogPost res = null;
|
|
|
|
if (networkStatus.IsOnline)
|
|
{
|
|
try
|
|
{
|
|
if (removeOld)
|
|
{
|
|
//var oldPost = await localStorage.GetItemAsync<BlogPostWithText>($"{DBKEY_PREFIX}{id}");
|
|
//if (oldPost != null)
|
|
//{
|
|
|
|
// if (oldPost.ImageRefs != null)
|
|
// {
|
|
// foreach (var k in oldPost.ImageRefs.Keys)
|
|
// {
|
|
// await localStorage.RemoveItemAsync($"{DBKEY_IMG_PREFIX}{id}-{k}");
|
|
// }
|
|
// }
|
|
//}
|
|
|
|
var urls = await cache.MatchAll($"BlogPosts/{id}/imgref");
|
|
if (urls != null)
|
|
{
|
|
foreach (var u in urls)
|
|
{
|
|
await cache.Delete(u);
|
|
}
|
|
}
|
|
await Task.Yield();
|
|
await localStorage.RemoveItemAsync($"{DBKEY_PREFIX_IMGCACHE}{id}");
|
|
await localStorage.RemoveItemAsync($"{DBKEY_PREFIX_BLOGCACHE}{id}");
|
|
}
|
|
|
|
System.Diagnostics.Debug.WriteLine($"Syncing post {id}");
|
|
var newPost = await httpClient.GetJsonAsync<BlogPost>($"BlogPosts/{id}");
|
|
await Task.Yield(); // Any effect after non-localhost-network-awaits?
|
|
if (newPost != null)
|
|
{
|
|
if (includeImgs)
|
|
{
|
|
if ((newPost.ImageRefs?.Count ?? 0) > 0)
|
|
{
|
|
|
|
//foreach (var k in newPost.ImageRefs.Keys)
|
|
//{
|
|
// try
|
|
// {
|
|
// System.Diagnostics.Debug.WriteLine($"Fetching {k} for {id}");
|
|
// var f = await httpClient.GetByteArrayAsync($"BlogPosts/{id}/imgref/{k}");
|
|
// await localStorage.SetItemAsync($"{BlogDb.DBKEY_IMG_PREFIX}{id}-{k}", f);
|
|
// }
|
|
// catch (HttpRequestException ex)
|
|
// {
|
|
// System.Diagnostics.Debug.WriteLine($"BlogPosts: error fetching img {ex.GetStatusCode()} from remote");
|
|
// }
|
|
//}
|
|
|
|
var urls = new List<string>();
|
|
foreach (var k in newPost.ImageRefs.Keys)
|
|
{
|
|
var u = $"BlogPosts/{id}/imgref/{k}";
|
|
urls.Add(u);
|
|
}
|
|
System.Diagnostics.Debug.WriteLine($"BlogPosts: caching {urls.Count} imgs from remote... ");
|
|
await cache.AddAll(urls.ToArray());
|
|
await Task.Yield();
|
|
System.Diagnostics.Debug.WriteLine($".... done");
|
|
}
|
|
else
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"BlogPosts: no images to cache done");
|
|
}
|
|
await localStorage.SetItemAsync($"{BlogDb.DBKEY_PREFIX_IMGCACHE}{id}", true);
|
|
}
|
|
|
|
await localStorage.SetItemAsync(BlogDb.DBKEY_PREFIX_BLOGCACHE + id, newPost);
|
|
System.Diagnostics.Debug.WriteLine($"BlogPosts: fetched {id} from remote");
|
|
res= newPost;
|
|
}
|
|
else
|
|
{
|
|
status = SyncStatus.NotFound;
|
|
}
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"BlogPosts: error fetching {ex.GetStatusCode()} from remote");
|
|
// TODO: Find better way to extract statuscode
|
|
if (("" + ex.ToString()).Contains("404")) status = SyncStatus.NotFound;
|
|
else status = SyncStatus.NetError;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
status = SyncStatus.Offline;
|
|
}
|
|
|
|
return (res, status);
|
|
}
|
|
|
|
|
|
private void LocalStorage_Changed(object sender, ChangedEventArgs e)
|
|
{
|
|
// Notify if a cached posts were changed
|
|
if (e.Key.StartsWith(BlogDb.DBKEY_PREFIX_BLOGCACHE)) Task.Run(() => {
|
|
System.Diagnostics.Debug.WriteLine("LocalStorage CachedPosts updated event -> emitting blogDbChanged Event");
|
|
BlogPostsChanged?.Invoke(this, EventArgs.Empty);
|
|
});
|
|
|
|
|
|
// Notify if header-list was changed
|
|
if (e.Key.StartsWith(BlogDb.DBKEY_PREFIX_HDR)) Task.Run(() =>
|
|
{
|
|
System.Diagnostics.Debug.WriteLine("LocalStorage Headers updated event -> emitting blogDbChanged Event");
|
|
BlogPostsChanged?.Invoke(this, EventArgs.Empty);
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|