Last active
December 10, 2015 08:19
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// "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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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