mirror of
https://github.com/moparisthebest/keepass2android
synced 2024-11-26 03:02:24 -05:00
First implementation of CachingFileStorage
+ some tests to test loading and saving to cached storage
This commit is contained in:
parent
89eb4b0a34
commit
3cfb2c17e6
@ -36,7 +36,7 @@ namespace keepass2android.Io
|
|||||||
TimeSpan diff = currentModificationDate - previousDate;
|
TimeSpan diff = currentModificationDate - previousDate;
|
||||||
return diff > TimeSpan.FromSeconds(1);
|
return diff > TimeSpan.FromSeconds(1);
|
||||||
//don't use > operator because milliseconds are truncated
|
//don't use > operator because milliseconds are truncated
|
||||||
return File.GetLastWriteTimeUtc(ioc.Path) - previousDate >= TimeSpan.FromSeconds(1);
|
//return File.GetLastWriteTimeUtc(ioc.Path) - previousDate >= TimeSpan.FromSeconds(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
325
src/Kp2aBusinessLogic/Io/CachingFileStorage.cs
Normal file
325
src/Kp2aBusinessLogic/Io/CachingFileStorage.cs
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using KeePassLib.Cryptography;
|
||||||
|
using KeePassLib.Serialization;
|
||||||
|
using KeePassLib.Utility;
|
||||||
|
|
||||||
|
namespace keepass2android.Io
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for classes which can handle certain Cache events on a higher level (e.g. by user interaction)
|
||||||
|
/// </summary>
|
||||||
|
public interface ICacheSupervisor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// called when a save operation only updated the cache but not the remote file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ioc">The file which we tried to write</param>
|
||||||
|
/// <param name="e">The exception why the remote file couldn't be updated</param>
|
||||||
|
void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when only the local file could be opened during an open operation.
|
||||||
|
/// </summary>
|
||||||
|
void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called to notify the supervisor that the file described by ioc is opened from the cache because there's a conflict
|
||||||
|
/// with local and remote changes
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ioc"></param>
|
||||||
|
void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the IFileStorage interface as a proxy: A base storage is used as a remote storage. Local files are used to cache the
|
||||||
|
/// files on remote.
|
||||||
|
/// </summary>
|
||||||
|
public class CachingFileStorage: IFileStorage
|
||||||
|
{
|
||||||
|
private readonly IFileStorage _cachedStorage;
|
||||||
|
private readonly ICacheSupervisor _cacheSupervisor;
|
||||||
|
private readonly string _streamCacheDir;
|
||||||
|
|
||||||
|
public CachingFileStorage(IFileStorage cachedStorage, string cacheDir, ICacheSupervisor cacheSupervisor)
|
||||||
|
{
|
||||||
|
_cachedStorage = cachedStorage;
|
||||||
|
_cacheSupervisor = cacheSupervisor;
|
||||||
|
_streamCacheDir = cacheDir + Java.IO.File.Separator + "OfflineCache" + Java.IO.File.Separator;
|
||||||
|
if (!Directory.Exists(_streamCacheDir))
|
||||||
|
Directory.CreateDirectory(_streamCacheDir);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearCache()
|
||||||
|
{
|
||||||
|
IoUtil.DeleteDir(new Java.IO.File(_streamCacheDir), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
if (IsCached(ioc))
|
||||||
|
{
|
||||||
|
File.Delete(CachedFilePath(ioc));
|
||||||
|
File.Delete(VersionFilePath(ioc));
|
||||||
|
File.Delete(BaseVersionFilePath(ioc));
|
||||||
|
}
|
||||||
|
|
||||||
|
_cachedStorage.DeleteFile(ioc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CachedFilePath(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
SHA256Managed sha256 = new SHA256Managed();
|
||||||
|
string iocAsHexString = MemUtil.ByteArrayToHexString(sha256.ComputeHash(Encoding.Unicode.GetBytes(ioc.Path.ToCharArray())))+".cache";
|
||||||
|
return _streamCacheDir + iocAsHexString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCached(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
return File.Exists(CachedFilePath(ioc))
|
||||||
|
&& File.Exists(VersionFilePath(ioc))
|
||||||
|
&& File.Exists(BaseVersionFilePath(ioc));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
|
||||||
|
{
|
||||||
|
//see comment in GetCurrentFileVersionFast
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
//fast file version checking is not supported by CachingFileStorage:
|
||||||
|
//it's hard to return good versions in cases that the base source is offline
|
||||||
|
//or after modifying the cache.
|
||||||
|
//It's probably not relevant because fast file version checking is meant for local storage
|
||||||
|
//which is not cached.
|
||||||
|
return String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string VersionFilePath(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
return CachedFilePath(ioc)+".version";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BaseVersionFilePath(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
return CachedFilePath(ioc) + ".baseversion";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream OpenFileForRead(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
string cachedFilePath = CachedFilePath(ioc);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!IsCached(ioc)
|
||||||
|
|| File.ReadAllText(VersionFilePath(ioc)) == File.ReadAllText(BaseVersionFilePath(ioc)))
|
||||||
|
{
|
||||||
|
return OpenFileForReadWhenNoLocalChanges(ioc, cachedFilePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return OpenFileForReadWhenLocalChanges(ioc, cachedFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Kp2aLog.Log("couldn't open from remote " + ioc.Path);
|
||||||
|
Kp2aLog.Log(ex.ToString());
|
||||||
|
|
||||||
|
_cacheSupervisor.CouldntOpenFromRemote(ioc, ex);
|
||||||
|
return File.OpenRead(cachedFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream OpenFileForReadWhenLocalChanges(IOConnectionInfo ioc, string cachedFilePath)
|
||||||
|
{
|
||||||
|
//file is cached but has local modifications
|
||||||
|
//try to upload the changes if remote file doesn't have changes as well:
|
||||||
|
var hash = Calculate(ioc);
|
||||||
|
|
||||||
|
if (File.ReadAllText(BaseVersionFilePath(ioc)) == hash)
|
||||||
|
{
|
||||||
|
//no changes in remote file -> upload
|
||||||
|
using (Stream localData = File.OpenRead(CachedFilePath(ioc)))
|
||||||
|
{
|
||||||
|
TryUpdateRemoteFile(localData, ioc, true, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//conflict: both files changed.
|
||||||
|
//signal that we're loading from local
|
||||||
|
_cacheSupervisor.NotifyOpenFromLocalDueToConflict(ioc);
|
||||||
|
}
|
||||||
|
return File.OpenRead(cachedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Calculate(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
MemoryStream remoteData = new MemoryStream();
|
||||||
|
string hash;
|
||||||
|
using (
|
||||||
|
HashingStreamEx hashingRemoteStream = new HashingStreamEx(_cachedStorage.OpenFileForRead(ioc), false,
|
||||||
|
new SHA256Managed()))
|
||||||
|
{
|
||||||
|
hashingRemoteStream.CopyTo(remoteData);
|
||||||
|
hashingRemoteStream.Close();
|
||||||
|
hash = MemUtil.ByteArrayToHexString(hashingRemoteStream.Hash);
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream OpenFileForReadWhenNoLocalChanges(IOConnectionInfo ioc, string cachedFilePath)
|
||||||
|
{
|
||||||
|
//open stream:
|
||||||
|
using (Stream file = _cachedStorage.OpenFileForRead(ioc))
|
||||||
|
{
|
||||||
|
//copy to cache:
|
||||||
|
//note: we might use the file version to check if it's already in the cache and if copying is required.
|
||||||
|
//However, this is safer.
|
||||||
|
string fileHash;
|
||||||
|
using (HashingStreamEx cachedFile = new HashingStreamEx(File.Create(cachedFilePath), true, new SHA256Managed()))
|
||||||
|
{
|
||||||
|
file.CopyTo(cachedFile);
|
||||||
|
cachedFile.Close();
|
||||||
|
fileHash = MemUtil.ByteArrayToHexString(cachedFile.Hash);
|
||||||
|
}
|
||||||
|
//save hash in cache files:
|
||||||
|
File.WriteAllText(VersionFilePath(ioc), fileHash);
|
||||||
|
File.WriteAllText(BaseVersionFilePath(ioc), fileHash);
|
||||||
|
|
||||||
|
return File.OpenRead(cachedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryUpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//try to write to remote:
|
||||||
|
using (
|
||||||
|
IWriteTransaction remoteTrans = _cachedStorage.OpenWriteTransaction(ioc, useFileTransaction))
|
||||||
|
{
|
||||||
|
Stream remoteStream = remoteTrans.OpenFile();
|
||||||
|
cachedData.CopyTo(remoteStream);
|
||||||
|
remoteStream.Close();
|
||||||
|
remoteTrans.CommitWrite();
|
||||||
|
}
|
||||||
|
//success. Update base-version of cache:
|
||||||
|
File.WriteAllText(VersionFilePath(ioc), hash);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Kp2aLog.Log("couldn't save to remote " + ioc.Path);
|
||||||
|
Kp2aLog.Log(e.ToString());
|
||||||
|
//notify the supervisor so it might display a warning or schedule a retry
|
||||||
|
_cacheSupervisor.CouldntSaveToRemote(ioc, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CachedWriteTransaction: IWriteTransaction
|
||||||
|
{
|
||||||
|
private class CachedWriteMemoryStream : MemoryStream
|
||||||
|
{
|
||||||
|
private readonly IOConnectionInfo ioc;
|
||||||
|
private readonly CachingFileStorage _cachingFileStorage;
|
||||||
|
private readonly bool _useFileTransaction;
|
||||||
|
|
||||||
|
public CachedWriteMemoryStream(IOConnectionInfo ioc, CachingFileStorage cachingFileStorage, bool useFileTransaction)
|
||||||
|
{
|
||||||
|
this.ioc = ioc;
|
||||||
|
_cachingFileStorage = cachingFileStorage;
|
||||||
|
_useFileTransaction = useFileTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override void Close()
|
||||||
|
{
|
||||||
|
//write file to cache:
|
||||||
|
//(note: this might overwrite local changes. It's assumed that a sync operation or check was performed before
|
||||||
|
string hash;
|
||||||
|
using (var hashingStream = new HashingStreamEx(File.Create(_cachingFileStorage.CachedFilePath(ioc)), true, new SHA256Managed()))
|
||||||
|
{
|
||||||
|
Position = 0;
|
||||||
|
CopyTo(hashingStream);
|
||||||
|
|
||||||
|
hashingStream.Close();
|
||||||
|
hash = MemUtil.ByteArrayToHexString(hashingStream.Hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(_cachingFileStorage.VersionFilePath(ioc), hash);
|
||||||
|
//update file on remote. This might overwrite changes there as well, see above.
|
||||||
|
Position = 0;
|
||||||
|
_cachingFileStorage.TryUpdateRemoteFile(this, ioc, _useFileTransaction, hash);
|
||||||
|
|
||||||
|
base.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IOConnectionInfo _ioc;
|
||||||
|
private readonly bool _useFileTransaction;
|
||||||
|
private readonly CachingFileStorage _cachingFileStorage;
|
||||||
|
private MemoryStream _memoryStream;
|
||||||
|
private bool _committed;
|
||||||
|
|
||||||
|
public CachedWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction, CachingFileStorage cachingFileStorage)
|
||||||
|
{
|
||||||
|
_ioc = ioc;
|
||||||
|
_useFileTransaction = useFileTransaction;
|
||||||
|
_cachingFileStorage = cachingFileStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_committed)
|
||||||
|
_memoryStream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream OpenFile()
|
||||||
|
{
|
||||||
|
_memoryStream = new CachedWriteMemoryStream(_ioc, _cachingFileStorage, _useFileTransaction);
|
||||||
|
return _memoryStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CommitWrite()
|
||||||
|
{
|
||||||
|
//the transaction is committed in the stream's Close
|
||||||
|
_committed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
|
||||||
|
{
|
||||||
|
//create a transaction which writes to memory stream
|
||||||
|
//on close: write to cache. If possible, write to online
|
||||||
|
//update versions
|
||||||
|
return new CachedWriteTransaction(ioc, useFileTransaction, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CompleteIoId()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool? FileExists()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
return UrlUtil.StripExtension(
|
||||||
|
UrlUtil.GetFileName(ioc.Path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/Kp2aBusinessLogic/Io/IoUtil.cs
Normal file
34
src/Kp2aBusinessLogic/Io/IoUtil.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Java.IO;
|
||||||
|
|
||||||
|
namespace keepass2android.Io
|
||||||
|
{
|
||||||
|
public static class IoUtil
|
||||||
|
{
|
||||||
|
public static bool DeleteDir(File dir, bool contentsOnly=false)
|
||||||
|
{
|
||||||
|
if (dir != null && dir.IsDirectory)
|
||||||
|
{
|
||||||
|
String[] children = dir.List();
|
||||||
|
for (int i = 0; i < children.Length; i++)
|
||||||
|
{
|
||||||
|
bool success = DeleteDir(new File(dir, children[i]));
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentsOnly)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// The directory is now empty so delete it
|
||||||
|
return dir.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -52,7 +52,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Io\BuiltInFileStorage.cs" />
|
<Compile Include="Io\BuiltInFileStorage.cs" />
|
||||||
|
<Compile Include="Io\CachingFileStorage.cs" />
|
||||||
<Compile Include="Io\IFileStorage.cs" />
|
<Compile Include="Io\IFileStorage.cs" />
|
||||||
|
<Compile Include="Io\IoUtil.cs" />
|
||||||
<Compile Include="IProgressDialog.cs" />
|
<Compile Include="IProgressDialog.cs" />
|
||||||
<Compile Include="PreferenceKey.cs" />
|
<Compile Include="PreferenceKey.cs" />
|
||||||
<Compile Include="UiStringKey.cs" />
|
<Compile Include="UiStringKey.cs" />
|
||||||
|
@ -124,7 +124,9 @@ namespace keepass2android
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
IFileStorage fileStorage = _app.GetFileStorage(iocInfo);
|
IFileStorage fileStorage = _app.GetFileStorage(iocInfo);
|
||||||
|
var fileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
|
||||||
pwDatabase.Open(fileStorage.OpenFileForRead(iocInfo), fileStorage.GetFilenameWithoutPathAndExt(iocInfo), iocInfo, compositeKey, status);
|
pwDatabase.Open(fileStorage.OpenFileForRead(iocInfo), fileStorage.GetFilenameWithoutPathAndExt(iocInfo), iocInfo, compositeKey, status);
|
||||||
|
LastFileVersion = fileVersion;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@ -140,8 +142,6 @@ namespace keepass2android
|
|||||||
|
|
||||||
status.UpdateSubMessage("");
|
status.UpdateSubMessage("");
|
||||||
|
|
||||||
LastFileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
|
|
||||||
|
|
||||||
Root = pwDatabase.RootGroup;
|
Root = pwDatabase.RootGroup;
|
||||||
PopulateGlobals(Root);
|
PopulateGlobals(Root);
|
||||||
|
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
<Compile Include="TestKp2aApp.cs" />
|
<Compile Include="TestKp2aApp.cs" />
|
||||||
<Compile Include="TestLoadDb.cs" />
|
<Compile Include="TestLoadDb.cs" />
|
||||||
<Compile Include="TestLoadDbCredentials.cs" />
|
<Compile Include="TestLoadDbCredentials.cs" />
|
||||||
|
<Compile Include="TestCachingFileStorage.cs" />
|
||||||
<Compile Include="TestSaveDb.cs" />
|
<Compile Include="TestSaveDb.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -19,8 +19,9 @@ namespace Kp2aUnitTests
|
|||||||
TestRunner runner = new TestRunner();
|
TestRunner runner = new TestRunner();
|
||||||
// Run all tests from this assembly
|
// Run all tests from this assembly
|
||||||
//runner.AddTests(Assembly.GetExecutingAssembly());
|
//runner.AddTests(Assembly.GetExecutingAssembly());
|
||||||
//runner.AddTests(new List<Type> { typeof(TestSaveDb)});
|
runner.AddTests(new List<Type> { typeof(TestCachingFileStorage) });
|
||||||
runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadEditSaveWithSyncKdbp"));
|
//runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote"));
|
||||||
|
//runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly"));
|
||||||
//runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles"));
|
//runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles"));
|
||||||
return runner;
|
return runner;
|
||||||
}
|
}
|
||||||
|
358
src/Kp2aUnitTests/TestCachingFileStorage.cs
Normal file
358
src/Kp2aUnitTests/TestCachingFileStorage.cs
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Android.App;
|
||||||
|
using KeePassLib.Serialization;
|
||||||
|
using KeePassLib.Utility;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using keepass2android.Io;
|
||||||
|
|
||||||
|
namespace Kp2aUnitTests
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
class TestCachingFileStorage: TestBase
|
||||||
|
{
|
||||||
|
private TestFileStorage _testFileStorage;
|
||||||
|
private CachingFileStorage _fileStorage;
|
||||||
|
private static readonly string CachingTestFile = DefaultDirectory + "cachingTestFile.txt";
|
||||||
|
private string _defaultCacheFileContents = "default contents";
|
||||||
|
private TestCacheSupervisor _testCacheSupervisor;
|
||||||
|
|
||||||
|
class TestCacheSupervisor: ICacheSupervisor
|
||||||
|
{
|
||||||
|
public bool CouldntOpenFromRemoteCalled { get; set; }
|
||||||
|
public bool CouldntSaveToRemoteCalled { get; set; }
|
||||||
|
public bool NotifyOpenFromLocalDueToConflictCalled { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e)
|
||||||
|
{
|
||||||
|
CouldntSaveToRemoteCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex)
|
||||||
|
{
|
||||||
|
CouldntOpenFromRemoteCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
NotifyOpenFromLocalDueToConflictCalled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestFileStorage: IFileStorage
|
||||||
|
{
|
||||||
|
private BuiltInFileStorage _builtIn = new BuiltInFileStorage();
|
||||||
|
|
||||||
|
public bool Offline { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public void DeleteFile(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
if (Offline)
|
||||||
|
throw new IOException("offline");
|
||||||
|
_builtIn.DeleteFile(ioc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
|
||||||
|
{
|
||||||
|
if (Offline)
|
||||||
|
return false;
|
||||||
|
return _builtIn.CheckForFileChangeFast(ioc, previousFileVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
if (Offline)
|
||||||
|
throw new IOException("offline");
|
||||||
|
return _builtIn.GetCurrentFileVersionFast(ioc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream OpenFileForRead(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
if (Offline)
|
||||||
|
throw new IOException("offline");
|
||||||
|
return _builtIn.OpenFileForRead(ioc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
|
||||||
|
{
|
||||||
|
if (Offline)
|
||||||
|
throw new IOException("offline");
|
||||||
|
return new TestFileTransaction(ioc, useFileTransaction, Offline);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestFileTransaction : IWriteTransaction
|
||||||
|
{
|
||||||
|
private readonly bool _offline;
|
||||||
|
private readonly FileTransactionEx _transaction;
|
||||||
|
|
||||||
|
public TestFileTransaction(IOConnectionInfo ioc, bool useFileTransaction, bool offline)
|
||||||
|
{
|
||||||
|
_offline = offline;
|
||||||
|
_transaction = new FileTransactionEx(ioc, useFileTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream OpenFile()
|
||||||
|
{
|
||||||
|
if (_offline)
|
||||||
|
throw new IOException("offline");
|
||||||
|
return _transaction.OpenWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CommitWrite()
|
||||||
|
{
|
||||||
|
if (_offline)
|
||||||
|
throw new IOException("offline");
|
||||||
|
_transaction.CommitWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CompleteIoId()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool? FileExists()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
return _builtIn.GetFilenameWithoutPathAndExt(ioc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests correct behavior in case that either remote or cache are not available
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TestMakeAccessibleWhenOffline()
|
||||||
|
{
|
||||||
|
SetupFileStorage();
|
||||||
|
|
||||||
|
//read the file once. Should now be in the cache.
|
||||||
|
MemoryStream fileContents = ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
|
||||||
|
//check it's the correct data:
|
||||||
|
Assert.AreEqual(MemoryStreamToString(fileContents), _defaultCacheFileContents);
|
||||||
|
|
||||||
|
//let the base file storage go offline:
|
||||||
|
_testFileStorage.Offline = true;
|
||||||
|
|
||||||
|
//now try to read the file again:
|
||||||
|
MemoryStream fileContents2 = ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
|
||||||
|
AssertEqual(fileContents, fileContents2);
|
||||||
|
|
||||||
|
Assert.IsTrue(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MemoryStreamToString(MemoryStream stream)
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
StreamReader r = new StreamReader(stream);
|
||||||
|
return r.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// tests correct behaviour after modifiying the local cache (with the remote file being unchanged) and remote being
|
||||||
|
/// either unavailable or available
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TestSyncOnLoadWhenLocalFileChanged()
|
||||||
|
{
|
||||||
|
SetupFileStorage();
|
||||||
|
|
||||||
|
|
||||||
|
//read the file once. Should now be in the cache.
|
||||||
|
ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
|
||||||
|
//let the base file storage go offline:
|
||||||
|
_testFileStorage.Offline = true;
|
||||||
|
|
||||||
|
//write something to the cache:
|
||||||
|
string newContent = "new content";
|
||||||
|
WriteContentToCacheFile(newContent);
|
||||||
|
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsTrue(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
_testCacheSupervisor.CouldntSaveToRemoteCalled = false;
|
||||||
|
|
||||||
|
//now try to read the file again:
|
||||||
|
MemoryStream fileContents2 = ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
|
||||||
|
Assert.IsTrue(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
_testCacheSupervisor.CouldntOpenFromRemoteCalled = false;
|
||||||
|
|
||||||
|
//should return the written content:
|
||||||
|
Assert.AreEqual(MemoryStreamToString(fileContents2), newContent);
|
||||||
|
|
||||||
|
//now go online and read again. This should trigger a sync and the modified data must be returned
|
||||||
|
_testFileStorage.Offline = false;
|
||||||
|
MemoryStream fileContents3 = ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
Assert.AreEqual(MemoryStreamToString(fileContents3), newContent);
|
||||||
|
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
|
||||||
|
//ensure the data on the remote was synced:
|
||||||
|
MemoryStream fileContents4 = ReadToMemoryStream(_testFileStorage, CachingTestFile);
|
||||||
|
Assert.AreEqual(MemoryStreamToString(fileContents4), newContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// tests correct behaviour after modifiying both the local cache and the remote file
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TestLoadLocalWhenBothFilesChanged()
|
||||||
|
{
|
||||||
|
SetupFileStorage();
|
||||||
|
|
||||||
|
//read the file once. Should now be in the cache.
|
||||||
|
ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
|
||||||
|
//let the base file storage go offline:
|
||||||
|
_testFileStorage.Offline = true;
|
||||||
|
|
||||||
|
//write something to the cache:
|
||||||
|
string newLocalContent = "new local content";
|
||||||
|
WriteContentToCacheFile(newLocalContent);
|
||||||
|
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsTrue(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
_testCacheSupervisor.CouldntSaveToRemoteCalled = false;
|
||||||
|
|
||||||
|
//write something to the remote file:
|
||||||
|
File.WriteAllText(CachingTestFile, "new remote content");
|
||||||
|
|
||||||
|
//go online again:
|
||||||
|
_testFileStorage.Offline = false;
|
||||||
|
|
||||||
|
//now try to read the file again:
|
||||||
|
MemoryStream fileContents2 = ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
|
||||||
|
//should return the local content:
|
||||||
|
Assert.AreEqual(MemoryStreamToString(fileContents2), newLocalContent);
|
||||||
|
|
||||||
|
//but a notification about the conflict should be made:
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
Assert.IsTrue(_testCacheSupervisor.NotifyOpenFromLocalDueToConflictCalled);
|
||||||
|
_testCacheSupervisor.NotifyOpenFromLocalDueToConflictCalled = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestSaveToRemote()
|
||||||
|
{
|
||||||
|
SetupFileStorage();
|
||||||
|
|
||||||
|
//read the file once. Should now be in the cache.
|
||||||
|
ReadToMemoryStream(_fileStorage, CachingTestFile);
|
||||||
|
|
||||||
|
//write something to the cache:
|
||||||
|
string newContent = "new content";
|
||||||
|
WriteContentToCacheFile(newContent);
|
||||||
|
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
|
||||||
|
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
|
||||||
|
|
||||||
|
Assert.AreEqual(newContent, File.ReadAllText(CachingTestFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteContentToCacheFile(string newContent)
|
||||||
|
{
|
||||||
|
|
||||||
|
using (var trans = _fileStorage.OpenWriteTransaction(IocForCacheFile, true))
|
||||||
|
{
|
||||||
|
StreamWriter sw = new StreamWriter(trans.OpenFile());
|
||||||
|
sw.Write(newContent);
|
||||||
|
sw.Flush();
|
||||||
|
sw.Close();
|
||||||
|
trans.CommitWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IOConnectionInfo IocForCacheFile
|
||||||
|
{
|
||||||
|
get { return new IOConnectionInfo() { Path = CachingTestFile }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupFileStorage()
|
||||||
|
{
|
||||||
|
_testFileStorage = new TestFileStorage();
|
||||||
|
_testCacheSupervisor = new TestCacheSupervisor();
|
||||||
|
_fileStorage = new CachingFileStorage(_testFileStorage, Application.Context.CacheDir.Path, _testCacheSupervisor);
|
||||||
|
_fileStorage.ClearCache();
|
||||||
|
File.WriteAllText(CachingTestFile, _defaultCacheFileContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MemoryStream ReadToMemoryStream(IFileStorage fileStorage, string filename)
|
||||||
|
{
|
||||||
|
Stream fileStream = fileStorage.OpenFileForRead(new IOConnectionInfo() {Path = filename});
|
||||||
|
MemoryStream fileContents = new MemoryStream();
|
||||||
|
fileStream.CopyTo(fileContents);
|
||||||
|
fileStream.Close();
|
||||||
|
return fileContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool StreamEquals(Stream stream1, Stream stream2)
|
||||||
|
{
|
||||||
|
const int bufferSize = 2048;
|
||||||
|
byte[] buffer1 = new byte[bufferSize]; //buffer size
|
||||||
|
byte[] buffer2 = new byte[bufferSize];
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int count1 = stream1.Read(buffer1, 0, bufferSize);
|
||||||
|
int count2 = stream2.Read(buffer2, 0, bufferSize);
|
||||||
|
|
||||||
|
if (count1 != count2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (count1 == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// You might replace the following with an efficient "memcmp"
|
||||||
|
if (!buffer1.Take(count1).SequenceEqual(buffer2.Take(count2)))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertEqual(MemoryStream s1, MemoryStream s2)
|
||||||
|
{
|
||||||
|
s1.Seek(0,0);
|
||||||
|
s2.Seek(0, 0);
|
||||||
|
Assert.AreEqual(s1.Length, s2.Length);
|
||||||
|
Assert.AreEqual(0, s1.Position);
|
||||||
|
Assert.AreEqual(0, s2.Position);
|
||||||
|
Assert.IsTrue(StreamEquals(s1, s2));
|
||||||
|
}
|
||||||
|
|
||||||
|
//todo test delete
|
||||||
|
}
|
||||||
|
}
|
@ -232,26 +232,18 @@ namespace Kp2aUnitTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestSaveAsWhenReadOnly()
|
public void TestSaveAsWhenReadOnly()
|
||||||
{
|
{
|
||||||
Assert.Fail("TODO: Test ");
|
Assert.Fail("TODO: Test ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void TestReloadWhenCancelSync()
|
|
||||||
{
|
|
||||||
//when a change is detected and the user cancels saving, the app should display the "file was modified - reload?" question.
|
|
||||||
Assert.Fail("TODO: Test ");
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestSaveAsWhenSyncError()
|
public void TestSaveAsWhenSyncError()
|
||||||
{
|
{
|
||||||
Assert.Fail("TODO: Test ");
|
Assert.Fail("TODO: Test ");
|
||||||
}
|
}*/
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestLoadAndSave_TestIdenticalFiles()
|
public void TestLoadAndSave_TestIdenticalFiles()
|
||||||
|
@ -11,7 +11,8 @@ namespace keepass2android
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[ContentProvider(new[]{"keepass2android."+AppNames.PackagePart+".provider"})]
|
[ContentProvider(new[]{"keepass2android."+AppNames.PackagePart+".provider"})]
|
||||||
public class AttachmentContentProvider : ContentProvider {
|
public class AttachmentContentProvider : ContentProvider {
|
||||||
|
public const string AttachmentCacheSubDir = "AttachmentCache";
|
||||||
|
|
||||||
private const String ClassName = "AttachmentContentProvider";
|
private const String ClassName = "AttachmentContentProvider";
|
||||||
|
|
||||||
// The authority is the symbolic name for the provider class
|
// The authority is the symbolic name for the provider class
|
||||||
@ -37,7 +38,8 @@ namespace keepass2android
|
|||||||
// E.g.
|
// E.g.
|
||||||
// 'content://keepass2android.provider/Test.txt'
|
// 'content://keepass2android.provider/Test.txt'
|
||||||
// Take this and build the path to the file
|
// Take this and build the path to the file
|
||||||
String fileLocation = Context.CacheDir + File.Separator
|
|
||||||
|
String fileLocation = Context.CacheDir + File.Separator + AttachmentCacheSubDir + File.Separator
|
||||||
+ uri.LastPathSegment;
|
+ uri.LastPathSegment;
|
||||||
|
|
||||||
// Create & return a ParcelFileDescriptor pointing to the file
|
// Create & return a ParcelFileDescriptor pointing to the file
|
||||||
|
@ -35,6 +35,7 @@ using KeePassLib.Security;
|
|||||||
using Android.Webkit;
|
using Android.Webkit;
|
||||||
using Android.Graphics;
|
using Android.Graphics;
|
||||||
using Java.IO;
|
using Java.IO;
|
||||||
|
using keepass2android.Io;
|
||||||
|
|
||||||
namespace keepass2android
|
namespace keepass2android
|
||||||
{
|
{
|
||||||
@ -230,7 +231,7 @@ namespace keepass2android
|
|||||||
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
|
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
|
||||||
string binaryDirectory = prefs.GetString(GetString(Resource.String.BinaryDirectory_key), GetString(Resource.String.BinaryDirectory_default));
|
string binaryDirectory = prefs.GetString(GetString(Resource.String.BinaryDirectory_key), GetString(Resource.String.BinaryDirectory_default));
|
||||||
if (writeToCacheDirectory)
|
if (writeToCacheDirectory)
|
||||||
binaryDirectory = CacheDir.Path;
|
binaryDirectory = CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir;
|
||||||
|
|
||||||
string filepart = key;
|
string filepart = key;
|
||||||
if (writeToCacheDirectory)
|
if (writeToCacheDirectory)
|
||||||
@ -528,30 +529,16 @@ namespace keepass2android
|
|||||||
|
|
||||||
public void ClearCache() {
|
public void ClearCache() {
|
||||||
try {
|
try {
|
||||||
File dir = CacheDir;
|
File dir = new File(CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir);
|
||||||
if (dir != null && dir.IsDirectory) {
|
if (dir.IsDirectory) {
|
||||||
DeleteDir(dir);
|
IoUtil.DeleteDir(dir);
|
||||||
}
|
}
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool DeleteDir(File dir) {
|
|
||||||
if (dir != null && dir.IsDirectory) {
|
|
||||||
String[] children = dir.List();
|
|
||||||
for (int i = 0; i < children.Length; i++) {
|
|
||||||
bool success = DeleteDir(new File(dir, children[i]));
|
|
||||||
if (!success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The directory is now empty so delete it
|
|
||||||
return dir.Delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool OnOptionsItemSelected(IMenuItem item) {
|
public override bool OnOptionsItemSelected(IMenuItem item) {
|
||||||
switch ( item.ItemId ) {
|
switch ( item.ItemId ) {
|
||||||
case Resource.Id.menu_donate:
|
case Resource.Id.menu_donate:
|
||||||
|
@ -150,7 +150,9 @@
|
|||||||
<None Include="Resources\drawable\Thumbs.db">
|
<None Include="Resources\drawable\Thumbs.db">
|
||||||
<Visible>False</Visible>
|
<Visible>False</Visible>
|
||||||
</None>
|
</None>
|
||||||
<None Include="Properties\AndroidManifest.xml" />
|
<None Include="Properties\AndroidManifest.xml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</None>
|
||||||
<None Include="filelist.txt">
|
<None Include="filelist.txt">
|
||||||
<Visible>False</Visible>
|
<Visible>False</Visible>
|
||||||
</None>
|
</None>
|
||||||
|
Loading…
Reference in New Issue
Block a user