Initial check-in

This commit is contained in:
Robert McRackan 2019-10-04 16:14:04 -04:00
parent c080c9e51d
commit 2cc93078d2
282 changed files with 24387 additions and 0 deletions

View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Dinah.Core;
using Dinah.Core.Collections.Generic;
namespace FileManager
{
// could add images here, but for now images are stored in a well-known location
public enum FileType { Unknown, Audio, AAX, PDF }
/// <summary>
/// Files are large. File contents are never read by app.
/// Paths are varied.
/// Files are written during download/decrypt/backup/liberate.
/// Paths are read at app launch and during download/decrypt/backup/liberate.
/// Many files are often looked up at once
/// </summary>
public sealed class AudibleFileStorage : Enumeration<AudibleFileStorage>
{
#region static
// centralize filetype mappings to ensure uniqueness
private static Dictionary<string, FileType> extensionMap => new Dictionary<string, FileType>
{
[".m4b"] = FileType.Audio,
[".mp3"] = FileType.Audio,
[".aac"] = FileType.Audio,
[".mp4"] = FileType.Audio,
[".m4a"] = FileType.Audio,
[".aax"] = FileType.AAX,
[".pdf"] = FileType.PDF,
[".zip"] = FileType.PDF,
};
public static AudibleFileStorage Audio { get; }
public static AudibleFileStorage AAX { get; }
public static AudibleFileStorage PDF { get; }
public static string DownloadsInProgress { get; }
public static string DecryptInProgress { get; }
public static string BooksDirectory => Configuration.Instance.Books;
// not customizable. don't move to config
public static string DownloadsFinal { get; }
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
static AudibleFileStorage()
{
#region init DecryptInProgress
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
var M4bRootDir
= Configuration.Instance.DecryptInProgressEnum == "WinTemp" // else "LibationFiles"
? Configuration.Instance.WinTemp
: Configuration.Instance.LibationFiles;
DecryptInProgress = Path.Combine(M4bRootDir, "DecryptInProgress");
Directory.CreateDirectory(DecryptInProgress);
#endregion
#region init DownloadsInProgress
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
var AaxRootDir
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp" // else "LibationFiles"
? Configuration.Instance.WinTemp
: Configuration.Instance.LibationFiles;
DownloadsInProgress = Path.Combine(AaxRootDir, "DownloadsInProgress");
Directory.CreateDirectory(DownloadsInProgress);
#endregion
#region init BooksDirectory
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
Directory.CreateDirectory(Configuration.Instance.Books);
#endregion
// must do this in static ctor, not w/inline properties
// static properties init before static ctor so these dir.s would still be null
Audio = new AudibleFileStorage(FileType.Audio, BooksDirectory);
AAX = new AudibleFileStorage(FileType.AAX, DownloadsFinal);
PDF = new AudibleFileStorage(FileType.PDF, BooksDirectory);
}
#endregion
#region instance
public FileType FileType => (FileType)Value;
public string StorageDirectory => DisplayName;
public IEnumerable<string> Extensions => extensionMap.Where(kvp => kvp.Value == FileType).Select(kvp => kvp.Key);
private AudibleFileStorage(FileType fileType, string storageDirectory) : base((int)fileType, storageDirectory) { }
/// <summary>
/// Example for full books:
/// Search recursively in _books directory. Full book exists if either are true
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public async Task<bool> ExistsAsync(string productId)
=> (await GetAsync(productId).ConfigureAwait(false)) != null;
public async Task<string> GetAsync(string productId)
=> await getAsync(productId).ConfigureAwait(false);
private async Task<string> getAsync(string productId)
{
{
var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
}
// this is how files are saved by default. check this method first
{
var diskFile_byDirName = (await Task.Run(() => getFile_checkDirName(productId)).ConfigureAwait(false));
if (diskFile_byDirName != null)
{
FilePathCache.Upsert(productId, FileType, diskFile_byDirName);
return diskFile_byDirName;
}
}
{
var diskFile_byFileName = (await Task.Run(() => getFile_checkFileName(productId, StorageDirectory, SearchOption.AllDirectories)).ConfigureAwait(false));
if (diskFile_byFileName != null)
{
FilePathCache.Upsert(productId, FileType, diskFile_byFileName);
return diskFile_byFileName;
}
}
return null;
}
// returns audio file if there is a directory where both are true
// - the directory name contains the productId
// - the directory contains an audio file in it's top dir (not recursively)
private string getFile_checkDirName(string productId)
{
foreach (var d in Directory.EnumerateDirectories(StorageDirectory, "*.*", SearchOption.AllDirectories))
{
if (!fileHasId(d, productId))
continue;
var firstAudio = Directory
.EnumerateFiles(d, "*.*", SearchOption.TopDirectoryOnly)
.FirstOrDefault(f => IsFileTypeMatch(f));
if (firstAudio != null)
return firstAudio;
}
return null;
}
// returns audio file if there is an file where both are true
// - the file name contains the productId
// - the file is an audio type
private string getFile_checkFileName(string productId, string dir, SearchOption searchOption)
=> Directory
.EnumerateFiles(dir, "*.*", searchOption)
.FirstOrDefault(f => fileHasId(f, productId) && IsFileTypeMatch(f));
public bool IsFileTypeMatch(string filename)
=> Extensions.ContainsInsensative(Path.GetExtension(filename));
public bool IsFileTypeMatch(FileInfo fileInfo)
=> Extensions.ContainsInsensative(fileInfo.Extension);
// use GetFileName, NOT GetFileNameWithoutExtension. This tests files AND directories. if the dir has a dot in the final part of the path, it will be treated like the file extension
private static bool fileHasId(string file, string productId)
=> Path.GetFileName(file).ContainsInsensitive(productId);
#endregion
}
}

View file

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Dinah.Core;
namespace FileManager
{
public class Configuration
{
// settings will be persisted when all are true
// - property (not field)
// - string
// - public getter
// - public setter
#region // properties to test reflection
/*
// field should NOT be populated
public string TestField;
// int should NOT be populated
public int TestInt { get; set; }
// read-only should NOT be populated
public string TestGet { get; } // get only: should NOT get auto-populated
// set-only should NOT be populated
public string TestSet { private get; set; }
// get and set: SHOULD be auto-populated
public string TestGetSet { get; set; }
*/
#endregion
private const string configFilename = "LibationSettings.json";
private PersistentDictionary persistentDictionary { get; }
[Description("Location of the configuration file where these settings are saved. Please do not edit this file directly while Libation is running.")]
public string Filepath { get; }
[Description("Your user-specific key used to decrypt your audible files (*.aax) into audio files you can use anywhere (*.m4b)")]
public string DecryptKey
{
get => persistentDictionary[nameof(DecryptKey)];
set => persistentDictionary[nameof(DecryptKey)] = value;
}
[Description("Location for book storage. Includes destination of newly liberated books")]
public string Books
{
get => persistentDictionary[nameof(Books)];
set => persistentDictionary[nameof(Books)] = value;
}
public string WinTemp { get; } = Path.Combine(Path.GetTempPath(), "Libation");
[Description("Location for storage of program-created files")]
public string LibationFiles
{
get => persistentDictionary[nameof(LibationFiles)];
set => persistentDictionary[nameof(LibationFiles)] = value;
}
// default setting and directory creation occur in class responsible for files.
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
// temp/working dir(s) should be outside of dropbox
[Description("Temporary location of files while they're in process of being downloaded.\r\nWhen download is complete, the final file will be in [LibationFiles]\\DownloadsFinal")]
public string DownloadsInProgressEnum
{
get => persistentDictionary[nameof(DownloadsInProgressEnum)];
set => persistentDictionary[nameof(DownloadsInProgressEnum)] = value;
}
// temp/working dir(s) should be outside of dropbox
[Description("Temporary location of files while they're in process of being decrypted.\r\nWhen decryption is complete, the final file will be in Books location")]
public string DecryptInProgressEnum
{
get => persistentDictionary[nameof(DecryptInProgressEnum)];
set => persistentDictionary[nameof(DecryptInProgressEnum)] = value;
}
// singleton stuff
public static Configuration Instance { get; } = new Configuration();
private Configuration()
{
Filepath = getPath();
// load json values into memory
persistentDictionary = new PersistentDictionary(Filepath);
ensureDictionaryEntries();
// setUserFilesDirectoryDefault
// don't create dir. dir creation is the responsibility of places that use the dir
if (string.IsNullOrWhiteSpace(LibationFiles))
LibationFiles = Path.Combine(Path.GetDirectoryName(Exe.FileLocationOnDisk), "Libation");
}
public static string GetDescription(string propertyName)
{
var attribute = typeof(Configuration)
.GetProperty(propertyName)
?.GetCustomAttributes(typeof(DescriptionAttribute), true)
.SingleOrDefault()
as DescriptionAttribute;
return attribute?.Description;
}
private static string getPath()
{
// search folders for config file. accept the first match
var defaultdir = Path.GetDirectoryName(Exe.FileLocationOnDisk);
var baseDirs = new HashSet<string>
{
defaultdir,
getNonDevelopmentDir(defaultdir),
Environment.GetFolderPath(Environment.SpecialFolder.Personal)
};
var subDirs = baseDirs.Select(dir => Path.Combine(dir, "Libation"));
var dirs = baseDirs.Concat(subDirs).ToList();
foreach (var dir in dirs)
{
var f = Path.Combine(dir, configFilename);
if (File.Exists(f))
return f;
}
return Path.Combine(defaultdir, configFilename);
}
private static string getNonDevelopmentDir(string path)
{
// examples:
// \Libation\Core2_0\bin\Debug\netcoreapp2.1
// \Libation\StndLib\bin\Debug\netstandard2.0
// \Libation\MyWnfrm\bin\Debug
// \Libation\Core2_0\bin\Release\netcoreapp2.1
// \Libation\StndLib\bin\Release\netstandard2.0
// \Libation\MyWnfrm\bin\Release
var curr = new DirectoryInfo(path);
if (!curr.Name.EqualsInsensitive("debug") && !curr.Name.EqualsInsensitive("release") && !curr.Name.StartsWithInsensitive("netcoreapp") && !curr.Name.StartsWithInsensitive("netstandard"))
return path;
// get out of netcore/standard dir => debug
if (curr.Name.StartsWithInsensitive("netcoreapp") || curr.Name.StartsWithInsensitive("netstandard"))
curr = curr.Parent;
if (!curr.Name.EqualsInsensitive("debug") && !curr.Name.EqualsInsensitive("release"))
return path;
// get out of debug => bin
curr = curr.Parent;
if (!curr.Name.EqualsInsensitive("bin"))
return path;
// get out of bin
curr = curr.Parent;
// get out of csproj-level dir
curr = curr.Parent;
// curr should now be sln-level dir
return curr.FullName;
}
private void ensureDictionaryEntries()
{
var stringProperties = getDictionaryProperties().Select(p => p.Name).ToList();
var missingKeys = stringProperties.Except(persistentDictionary.Keys).ToArray();
persistentDictionary.AddKeys(missingKeys);
}
private IEnumerable<System.Reflection.PropertyInfo> dicPropertiesCache;
private IEnumerable<System.Reflection.PropertyInfo> getDictionaryProperties()
{
if (dicPropertiesCache == null)
dicPropertiesCache = PersistentDictionary.GetPropertiesToPersist(this.GetType());
return dicPropertiesCache;
}
}
}

View file

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace FileManager
{
public static class FilePathCache
{
internal class CacheEntry
{
public string Id { get; set; }
public FileType FileType { get; set; }
public string Path { get; set; }
}
static List<CacheEntry> inMemoryCache = new List<CacheEntry>();
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
static FilePathCache()
{
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (FileUtility.FileExists(JsonFile))
inMemoryCache = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(JsonFile));
}
public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
public static string GetPath(string id, FileType type)
{
var entry = inMemoryCache.SingleOrDefault(i => i.Id == id && i.FileType == type);
if (entry == null)
return null;
if (!FileUtility.FileExists(entry.Path))
{
remove(entry);
return null;
}
return entry.Path;
}
private static object locker { get; } = new object();
private static void remove(CacheEntry entry)
{
lock (locker)
{
inMemoryCache.Remove(entry);
save();
}
}
public static void Upsert(string id, FileType type, string path)
{
if (!FileUtility.FileExists(path))
throw new FileNotFoundException("Cannot add path to cache. File not found");
lock (locker)
{
var entry = inMemoryCache.SingleOrDefault(i => i.Id == id && i.FileType == type);
if (entry != null)
entry.Path = path;
else
{
entry = new CacheEntry { Id = id, FileType = type, Path = path };
inMemoryCache.Add(entry);
}
save();
}
}
// ONLY call this within lock()
private static void save()
{
// create json if not exists
void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(inMemoryCache, Formatting.Indented));
try { resave(); }
catch (IOException)
{
try { resave(); }
catch (IOException)
{
Console.WriteLine("...that's not good");
throw;
}
}
}
}
}

View file

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace FileManager
{
public static class FileUtility
{
// a replacement for File.Exists() which allows long paths
// not needed in .net-core
public static bool FileExists(string path)
{
var basic = File.Exists(path);
if (basic)
return true;
// character cutoff is usually 269 but this isn't a hard number. there are edgecases which shorted the threshold
if (path.Length < 260)
return false;
// try long name prefix:
// \\?\
// https://blogs.msdn.microsoft.com/jeremykuhne/2016/06/21/more-on-new-net-path-handling/
path = @"\\?\" + path;
return File.Exists(path);
}
/// <param name="proposedPath">acceptable inputs:
/// example.txt
/// C:\Users\username\Desktop\example.txt</param>
/// <returns>Returns full name and path of unused filename. including (#)</returns>
public static string GetValidFilename(string proposedPath)
=> GetValidFilename(Path.GetDirectoryName(proposedPath), Path.GetFileNameWithoutExtension(proposedPath), Path.GetExtension(proposedPath));
public static string GetValidFilename(string dirFullPath, string filename, string extension, params string[] metadataSuffixes)
{
if (string.IsNullOrWhiteSpace(dirFullPath))
throw new ArgumentException($"{nameof(dirFullPath)} may not be null or whitespace", nameof(dirFullPath));
// file max length = 255. dir max len = 247
// sanitize
filename = GetAsciiTag(filename);
// manage length
if (filename.Length > 50)
filename = filename.Substring(0, 50) + "[...]";
// append id. it is 10 or 14 char in the common cases
if (metadataSuffixes != null && metadataSuffixes.Length > 0)
filename += " [" + string.Join("][", metadataSuffixes) + "]";
// this method may also be used for directory names, so no guarantee of extension
if (!string.IsNullOrWhiteSpace(extension))
extension = '.' + extension.Trim('.');
// ensure uniqueness
var fullfilename = Path.Combine(dirFullPath, filename + extension);
var i = 0;
while (FileExists(fullfilename))
fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
return fullfilename;
}
public static string GetAsciiTag(string property)
{
if (property == null)
return "";
// omit characters which are invalid. EXCEPTION: change colon to underscore
property = property.Replace(':', '_');
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
foreach (var ch in Path.GetInvalidFileNameChars())
property = property.Replace(ch.ToString(), "");
return property;
}
public static string Declaw(string str)
=> str
.Replace("<script", "<sxcript")
.Replace(".net", ".nxet")
.Replace(".com", ".cxom")
.Replace("<link", "<lxink")
.Replace("http", "hxttp");
public static string RestoreDeclawed(string str)
=> str
?.Replace("<sxcript", "<script")
.Replace(".nxet", ".net")
.Replace(".cxom", ".com")
.Replace("<lxink", "<link")
.Replace("hxttp", "http");
public static string TitleCompressed(string title)
=> new string(title
.Where(c => (char.IsLetterOrDigit(c)))
.ToArray());
}
}

View file

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace FileManager
{
public class PersistentDictionary
{
public string Filepath { get; }
// forgiving -- doesn't drop settings. old entries will continue to be persisted even if not publicly visible
private Dictionary<string, string> settingsDic { get; }
public string this[string key]
{
get => settingsDic[key];
set
{
if (settingsDic.ContainsKey(key) && settingsDic[key] == value)
return;
settingsDic[key] = value;
// auto-save to file
save();
}
}
public PersistentDictionary(string filepath)
{
Filepath = filepath;
// not found. create blank file
if (!File.Exists(Filepath))
{
File.WriteAllText(Filepath, "{}");
// give system time to create file before first use
System.Threading.Thread.Sleep(100);
}
settingsDic = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(Filepath));
}
public IEnumerable<string> Keys => settingsDic.Keys.Cast<string>();
public void AddKeys(params string[] keys)
{
if (keys == null || keys.Length == 0)
return;
foreach (var key in keys)
settingsDic.Add(key, null);
save();
}
private object locker { get; } = new object();
private void save()
{
lock (locker)
File.WriteAllText(Filepath, JsonConvert.SerializeObject(settingsDic, Formatting.Indented));
}
public static IEnumerable<System.Reflection.PropertyInfo> GetPropertiesToPersist(Type type)
=> type
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(p =>
// string properties only
p.PropertyType == typeof(string)
// exclude indexer
&& p.GetIndexParameters().Length == 0
// exclude read-only, write-only
&& p.GetGetMethod(false) != null
&& p.GetSetMethod(false) != null
).ToList();
}
}

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace FileManager
{
/// <summary>
/// Files are small. Entire file is read from disk every time. No volitile storage. Paths are well known
/// </summary>
public static class PictureStorage
{
public enum PictureSize { _80x80, _300x300, _500x500 }
// not customizable. don't move to config
private static string ImagesDirectory { get; }
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("Images").FullName;
private static string getPath(string pictureId, PictureSize size)
=> Path.Combine(ImagesDirectory, $"{pictureId}{size}.jpg");
public static byte[] GetImage(string pictureId, PictureSize size)
{
var path = getPath(pictureId, size);
if (!FileUtility.FileExists(path))
DownloadImages(pictureId);
return File.ReadAllBytes(path);
}
public static void DownloadImages(string pictureId)
{
var path80 = getPath(pictureId, PictureSize._80x80);
var path300 = getPath(pictureId, PictureSize._300x300);
var path500 = getPath(pictureId, PictureSize._500x500);
int retry = 0;
do
{
try
{
using (var webClient = new System.Net.WebClient())
{
// download any that don't exist
{
if (!FileUtility.FileExists(path80))
{
var bytes = webClient.DownloadData(
"https://images-na.ssl-images-amazon.com/images/I/" + pictureId + "._SL80_.jpg");
File.WriteAllBytes(path80, bytes);
}
}
{
if (!FileUtility.FileExists(path300))
{
var bytes = webClient.DownloadData(
"https://images-na.ssl-images-amazon.com/images/I/" + pictureId + "._SL300_.jpg");
File.WriteAllBytes(path300, bytes);
}
}
{
if (!FileUtility.FileExists(path500))
{
var bytes = webClient.DownloadData(
"https://m.media-amazon.com/images/I/" + pictureId + "._SL500_.jpg");
File.WriteAllBytes(path500, bytes);
}
}
break;
}
}
catch { retry++; }
}
while (retry < 3);
}
}
}

View file

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core.Collections.Generic;
using Newtonsoft.Json;
namespace FileManager
{
public static class QuickFilters
{
internal class FilterState
{
public bool UseDefault { get; set; }
public List<string> Filters { get; set; } = new List<string>();
}
static FilterState inMemoryState { get; } = new FilterState();
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "QuickFilters.json");
static QuickFilters()
{
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (FileUtility.FileExists(JsonFile))
inMemoryState = JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile));
}
public static bool UseDefault
{
get => inMemoryState.UseDefault;
set
{
lock (locker)
{
inMemoryState.UseDefault = value;
save();
}
}
}
public static IEnumerable<string> Filters => inMemoryState.Filters.AsReadOnly();
public static void Add(string filter)
{
if (string.IsNullOrWhiteSpace(filter))
return;
filter = filter.Trim();
lock (locker)
{
// check for duplicate
if (inMemoryState.Filters.ContainsInsensative(filter))
return;
inMemoryState.Filters.Add(filter);
save();
}
}
public static void Remove(string filter)
{
lock (locker)
{
inMemoryState.Filters.Remove(filter);
save();
}
}
public static void Edit(string oldFilter, string newFilter)
{
lock (locker)
{
var index = inMemoryState.Filters.IndexOf(oldFilter);
if (index < 0)
return;
inMemoryState.Filters = inMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList();
save();
}
}
public static void ReplaceAll(IEnumerable<string> filters)
{
filters = filters
.Where(f => !string.IsNullOrWhiteSpace(f))
.Distinct()
.Select(f => f.Trim());
lock (locker)
{
inMemoryState.Filters = new List<string>(filters);
save();
}
}
private static object locker { get; } = new object();
// ONLY call this within lock()
private static void save()
{
// create json if not exists
void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(inMemoryState, Formatting.Indented));
try { resave(); }
catch (IOException)
{
try { resave(); }
catch (IOException)
{
Console.WriteLine("...that's not good");
throw;
}
}
}
}
}

View file

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace FileManager
{
/// <summary>
/// Tags must also be stored in db for search performance. Stored in json file to survive a db reset.
/// json is only read when a product is first loaded
/// json is only written to when tags are edited
/// json access is infrequent and one-off
/// all other reads happen against db. No volitile storage
/// </summary>
public static class TagsPersistence
{
public static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
private static object locker { get; } = new object();
public static void Save(string productId, string tags)
=> System.Threading.Tasks.Task.Run(() => save_fireAndForget(productId, tags));
private static void save_fireAndForget(string productId, string tags)
{
lock (locker)
{
// get all
var allDictionary = retrieve();
// update/upsert tag list
allDictionary[productId] = tags;
// re-save:
// this often fails the first time with
// The requested operation cannot be performed on a file with a user-mapped section open.
// 2nd immediate attempt failing was rare. So I added sleep. We'll see...
void resave() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(allDictionary, Formatting.Indented));
try { resave(); }
catch (IOException debugEx)
{
// 1000 was always reliable but very slow. trying other values
var waitMs = 100;
System.Threading.Thread.Sleep(waitMs);
resave();
}
}
}
public static string GetTags(string productId)
{
var dic = retrieve();
return dic.ContainsKey(productId) ? dic[productId] : null;
}
private static Dictionary<string, string> retrieve()
{
if (!FileUtility.FileExists(TagsFile))
return new Dictionary<string, string>();
lock (locker)
return JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
}
}
}

View file

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace FileManager
{
public static class WebpageStorage
{
// not customizable. don't move to config
private static string PagesDirectory { get; }
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("Pages").FullName;
private static string BookDetailsDirectory { get; }
= new DirectoryInfo(PagesDirectory).CreateSubdirectory("Book Details").FullName;
public static string GetLibraryBatchName() => "Library_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
public static string SavePageToBatch(string contents, string batchName, string extension)
{
var batch_dir = Path.Combine(PagesDirectory, batchName);
Directory.CreateDirectory(batch_dir);
var file = Path.Combine(batch_dir, batchName + '.' + extension.Trim('.'));
var filename = FileUtility.GetValidFilename(file);
File.WriteAllText(filename, contents);
return filename;
}
public static List<FileInfo> GetJsonFiles(DirectoryInfo libDir)
=> libDir == null
? new List<FileInfo>()
: Directory
.EnumerateFiles(libDir.FullName, "*.json")
.Select(f => new FileInfo(f))
.ToList();
public static DirectoryInfo GetMostRecentLibraryDir()
{
var dir = Directory
.EnumerateDirectories(PagesDirectory, "Library_*")
.OrderBy(a => a)
.LastOrDefault();
if (string.IsNullOrWhiteSpace(dir))
return null;
return new DirectoryInfo(dir);
}
public static FileInfo GetBookDetailHtmFileInfo(string productId)
{
var path = Path.Combine(BookDetailsDirectory, $"BookDetail-{productId}.htm");
return new FileInfo(path);
}
public static FileInfo GetBookDetailJsonFileInfo(string productId)
{
var path = Path.Combine(BookDetailsDirectory, $"BookDetail-{productId}.json");
return new FileInfo(path);
}
public static FileInfo SaveBookDetailsToHtm(string productId, string contents)
{
var fi = GetBookDetailHtmFileInfo(productId);
File.WriteAllText(fi.FullName, contents);
return fi;
}
}
}