Skip to content

Instantly share code, notes, and snippets.

@jonpryor
Last active December 10, 2015 08:19
Show Gist options
  • Save jonpryor/4407260 to your computer and use it in GitHub Desktop.
Save jonpryor/4407260 to your computer and use it in GitHub Desktop.
File system abstraction for use in PCL apps. `FileSystemOperations.cs` is usable in a PCL library that targets .NET 4.5 and .NET for Windows Store Apps. (Yes, this is _crazy_, but .NET 4.5 is required to access `System.IO.StreamWriter` (!) and other useful `System.IO` types. (Seriously, we need to target .NET 4.5 to use types present in .NET 1.0…
// "Port"/rewrite of: http://blog.neteril.org/blog/2012/12/17/memory-efficient-bitmap-caching-with-mono-for-android/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Cadenza.IO
{
public class DiskCache
{
enum JournalOp
{
Created = 'c',
Modified = 'm',
Deleted = 'd'
}
const string JournalFileName = ".journal";
const string Magic = "MONOID";
readonly Encoding encoding = Encoding.UTF8;
string basePath;
string journalPath;
string version;
FileSystemOperations ops;
struct CacheEntry
{
public DateTime Origin;
public TimeSpan TimeToLive;
public CacheEntry(DateTime o, TimeSpan ttl)
{
Origin = o;
TimeToLive = ttl;
}
}
Dictionary<string, CacheEntry> entries = new Dictionary<string, CacheEntry>();
DiskCache (string basePath, string version, FileSystemOperations ops)
{
this.basePath = basePath;
this.journalPath = Path.Combine(basePath, JournalFileName);
this.version = version;
this.ops = ops ?? FileSystemOperations.Current;
}
public static async Task<DiskCache> CreateCacheAsync (string cachePath, string cacheName, string version = "1.0", FileSystemOperations ops = null)
{
/*string cachePath = Env.ExternalStorageState == Env.MediaMounted
|| !Env.IsExternalStorageRemovable ? ctx.ExternalCacheDir.AbsolutePath : ctx.CacheDir.AbsolutePath;*/
var cache = new DiskCache (Path.Combine(cachePath, cacheName), version, ops);
await cache.InitializeWithJournalAsync ();
return cache;
}
async Task InitializeWithJournalAsync ()
{
await ops.CreateDirectoryAsync (basePath);
bool createJournal = false;
using (var s = await ops.OpenFileForReadAsync (journalPath)) {
if (s.Length == 0) {
createJournal = true;
}
else {
ReadJournal (s);
}
}
if (createJournal) {
using (var s = await ops.CreateFileAsync (journalPath))
CreateDefaultJournal (s);
}
await Task.Run (() => CleanCallback ());
}
void CreateDefaultJournal (Stream stream)
{
using (var writer = CreateWriter (stream)) {
writer.WriteLine (Magic);
writer.WriteLine (version);
}
}
StreamWriter CreateWriter (Stream s)
{
return new StreamWriter (s, encoding) {
NewLine = "\n",
};
}
async Task<StreamWriter> AppendJournal ()
{
var stream = await ops.OpenFileForWriteAsync (journalPath, StreamWriteOptions.Append);
return CreateWriter (stream);
}
StreamReader CreateReader (Stream s)
{
return new StreamReader (s, encoding);
}
void ReadJournal (Stream stream)
{
using (var reader = CreateReader (stream)) {
if (!EnsureHeader (reader))
throw new InvalidOperationException ("Invalid header");
string line;
while ((line = reader.ReadLine ()) != null) {
try {
var op = ParseOp (line);
string key;
DateTime origin;
TimeSpan duration;
switch (op) {
case JournalOp.Created:
ParseEntry (line, out key, out origin, out duration);
entries.Add (key, new CacheEntry (origin, duration));
break;
case JournalOp.Modified:
ParseEntry (line, out key, out origin, out duration);
entries [key] = new CacheEntry (origin, duration);
break;
case JournalOp.Deleted:
ParseEntry (line, out key);
entries.Remove (key);
break;
}
}
catch {
break;
}
}
}
}
void CleanCallback ()
{
KeyValuePair<string, CacheEntry>[] kvps;
lock (entries)
{
var now = DateTime.UtcNow;
kvps = entries.Where (kvp => kvp.Value.Origin + kvp.Value.TimeToLive < now)
.Take (10)
.ToArray ();
foreach (var kvp in kvps)
{
entries.Remove(kvp.Key);
try
{
ops.DeleteFileAsync (Path.Combine (basePath, kvp.Key)).Wait ();
}
catch { }
}
}
}
bool EnsureHeader (StreamReader reader)
{
var m = reader.ReadLine();
var v = reader.ReadLine();
return m == Magic && v == version;
}
JournalOp ParseOp (string line)
{
return (JournalOp)line[0];
}
void ParseEntry (string line, out string key)
{
key = line.Substring(2);
}
void ParseEntry (string line, out string key, out DateTime origin, out TimeSpan duration)
{
key = null;
origin = DateTime.MinValue;
duration = TimeSpan.MinValue;
var parts = line.Substring (2).Split (' ');
if (parts.Length != 3)
throw new InvalidOperationException ("Invalid entry");
key = parts [0];
long dateTime, timespan;
if (!long.TryParse (parts [1], out dateTime))
throw new InvalidOperationException ("Corrupted origin");
else
origin = new DateTime (dateTime);
if (!long.TryParse (parts [2], out timespan))
throw new InvalidOperationException ("Corrupted duration");
else
duration = TimeSpan.FromMilliseconds (timespan);
}
public Task AddOrUpdateAsync (string key, Stream source, TimeSpan duration)
{
key = SanitizeKey (key);
return Task.Run (() => {
lock (entries) {
bool existed = entries.ContainsKey (key);
using (var copy = ops.CreateFileAsync (
Path.Combine (basePath, key)).Result) {
source.CopyTo (copy);
}
AppendToJournalAsync (existed ? JournalOp.Modified : JournalOp.Created,
key,
DateTime.UtcNow,
duration).Wait ();
entries [key] = new CacheEntry (DateTime.UtcNow, duration);
}
});
}
public Task<Stream> OpenAsync (string key)
{
key = SanitizeKey (key);
lock (entries) {
if (!entries.ContainsKey (key))
return Task<Stream>.FromResult ((Stream) null);
}
return ops.OpenFileForReadAsync (
Path.Combine (basePath, key));
}
async Task AppendToJournalAsync (JournalOp op, string key)
{
using (var writer = await AppendJournal ())
{
writer.Write ((char)op);
writer.Write (' ');
writer.Write (key);
writer.WriteLine ();
}
}
async Task AppendToJournalAsync (JournalOp op, string key, DateTime origin, TimeSpan ttl)
{
using (var writer = await AppendJournal ()) {
writer.Write ((char)op);
writer.Write (' ');
writer.Write (key);
writer.Write (' ');
writer.Write (origin.Ticks);
writer.Write (' ');
writer.Write ((long)ttl.TotalMilliseconds);
writer.WriteLine ();
}
}
string S
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
namespace Cadenza.IO
{
public enum StreamWriteOptions
{
None = 0,
ReplaceExisting = 1,
FailIfExists = 2,
Append = 4,
}
public abstract class FileSystemOperations
{
static FileSystemOperations current = CreateDefaultFileSystemOperations ();
public static FileSystemOperations Current {
get {
if (current == null)
throw new NotSupportedException ("No default FileSystemOperations implementatino is known for this platform.");
return current;
}
}
static FileSystemOperations CreateDefaultFileSystemOperations ()
{
try {
var provider = Assembly.Load (new AssemblyName ("Cadenza.IO-Full"));
return (FileSystemOperations) Activator.CreateInstance (provider.GetType ("Cadenza.IO.FullFileSystemOperations"));
}
catch {
// ignore
}
try {
var provider = Assembly.Load (new AssemblyName ("Cadenza.IO-Metro"));
return (FileSystemOperations) Activator.CreateInstance (provider.GetType ("Cadenza.IO.MetroFileSystemOperations"));
}
catch {
// ignore
}
return null;
}
public static void SetCurrent (FileSystemOperations newDefault)
{
if (newDefault == null)
throw new ArgumentNullException ("newDefault");
current = newDefault;
}
public Task<Stream> CreateFileAsync (string name)
{
return OpenFileForWriteAsync (name, StreamWriteOptions.ReplaceExisting);
}
public abstract Task<Stream> OpenFileForReadAsync (string path);
public abstract Task<Stream> OpenFileForWriteAsync (string path, StreamWriteOptions options = 0);
public abstract Task DeleteFileAsync (string path);
public abstract Task CreateDirectoryAsync (string path);
public abstract Task DeleteDirectoryAsync (string path);
}
}
using System;
using System.IO;
using System.Threading.Tasks;
namespace Cadenza.IO
{
public class FullFileSystemOperations : FileSystemOperations
{
public override Task<Stream> OpenFileForReadAsync (string path)
{
return Task<Stream>.Factory.StartNew (() => File.OpenRead (path));
}
public override Task<Stream> OpenFileForWriteAsync (string path, StreamWriteOptions options = 0)
{
return Task<Stream>.Factory.StartNew (() => File.Open (path, ToFileMode (options), FileAccess.Write));
}
static FileMode ToFileMode (StreamWriteOptions options)
{
switch (options) {
case StreamWriteOptions.ReplaceExisting: return FileMode.Truncate;
case StreamWriteOptions.FailIfExists: return FileMode.CreateNew;
case StreamWriteOptions.Append: return FileMode.Append;
}
return FileMode.OpenOrCreate;
}
public override Task DeleteFileAsync (string path)
{
return Task.Factory.StartNew (() => File.Delete (path));
}
public override Task CreateDirectoryAsync (string path)
{
return Task.Factory.StartNew (() => Directory.CreateDirectory (path));
}
public override Task DeleteDirectoryAsync (string path)
{
return Task.Factory.StartNew (() => Directory.Delete (path, true));
}
}
}
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
namespace Cadenza.IO
{
public class MetroFileSystemOperations : FileSystemOperations
{
public override async Task<Stream> OpenFileForReadAsync (string path)
{
var file = await GetStorageFileAsync (path, CreationCollisionOption.OpenIfExists);
return await file.OpenStreamForReadAsync ();
}
static async Task<StorageFile> GetStorageFileAsync (string path, CreationCollisionOption option)
{
string dir = Path.GetDirectoryName (path);
string f = Path.GetFileName (path);
var parent = await StorageFolder.GetFolderFromPathAsync (dir);
return await parent.CreateFileAsync (f, option);
}
public override async Task<Stream> OpenFileForWriteAsync (string path, StreamWriteOptions options = 0)
{
var file = await GetStorageFileAsync (path, ToCreationCollisionOption (options));
Stream s = await file.OpenStreamForWriteAsync ();
if (options == StreamWriteOptions.Append)
s.Position = s.Length;
return s;
}
static CreationCollisionOption ToCreationCollisionOption (StreamWriteOptions options)
{
switch (options) {
case StreamWriteOptions.FailIfExists: return CreationCollisionOption.FailIfExists;
case StreamWriteOptions.ReplaceExisting: return CreationCollisionOption.ReplaceExisting;
}
return CreationCollisionOption.OpenIfExists;
}
public override async Task DeleteFileAsync (string name)
{
var file = await StorageFile.GetFileFromPathAsync (name);
await file.DeleteAsync ();
}
public override async Task CreateDirectoryAsync (string name)
{
var parent = Path.GetDirectoryName (name);
var dir = Path.GetFileName (name);
var folder = await StorageFolder.GetFolderFromPathAsync (parent);
await folder.CreateFolderAsync (dir, CreationCollisionOption.OpenIfExists);
}
public override async Task DeleteDirectoryAsync (string name)
{
var folder = await StorageFolder.GetFolderFromPathAsync (name);
await folder.DeleteAsync ();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment