From 3cfb2c17e6b35ff25296c32bb68369970b0c4315 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 30 Jul 2013 20:42:16 +0200 Subject: [PATCH] First implementation of CachingFileStorage + some tests to test loading and saving to cached storage --- .../Io/BuiltInFileStorage.cs | 2 +- .../Io/CachingFileStorage.cs | 325 ++++++++++++++++ src/Kp2aBusinessLogic/Io/IoUtil.cs | 34 ++ .../Kp2aBusinessLogic.csproj | 2 + src/Kp2aBusinessLogic/database/Database.cs | 4 +- src/Kp2aUnitTests/Kp2aUnitTests.csproj | 1 + src/Kp2aUnitTests/MainActivity.cs | 5 +- src/Kp2aUnitTests/TestCachingFileStorage.cs | 358 ++++++++++++++++++ src/Kp2aUnitTests/TestSaveDb.cs | 12 +- .../AttachmentContentProvider.cs | 6 +- src/keepass2android/EntryActivity.cs | 25 +- src/keepass2android/keepass2android.csproj | 4 +- 12 files changed, 741 insertions(+), 37 deletions(-) create mode 100644 src/Kp2aBusinessLogic/Io/CachingFileStorage.cs create mode 100644 src/Kp2aBusinessLogic/Io/IoUtil.cs create mode 100644 src/Kp2aUnitTests/TestCachingFileStorage.cs diff --git a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs index 13579079..e2fcb571 100644 --- a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs @@ -36,7 +36,7 @@ namespace keepass2android.Io TimeSpan diff = currentModificationDate - previousDate; return diff > TimeSpan.FromSeconds(1); //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); } diff --git a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs new file mode 100644 index 00000000..2db1155f --- /dev/null +++ b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs @@ -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 +{ + /// + /// Interface for classes which can handle certain Cache events on a higher level (e.g. by user interaction) + /// + public interface ICacheSupervisor + { + /// + /// called when a save operation only updated the cache but not the remote file + /// + /// The file which we tried to write + /// The exception why the remote file couldn't be updated + void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e); + + /// + /// Called when only the local file could be opened during an open operation. + /// + void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex); + + /// + /// 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 + /// + /// + void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc); + } + + /// + /// 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. + /// + 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)); + } + } +} diff --git a/src/Kp2aBusinessLogic/Io/IoUtil.cs b/src/Kp2aBusinessLogic/Io/IoUtil.cs new file mode 100644 index 00000000..c4a83a79 --- /dev/null +++ b/src/Kp2aBusinessLogic/Io/IoUtil.cs @@ -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(); + } + + + } +} diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index a39cd67f..9b110957 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -52,7 +52,9 @@ + + diff --git a/src/Kp2aBusinessLogic/database/Database.cs b/src/Kp2aBusinessLogic/database/Database.cs index dd3cf5c5..93e2a542 100644 --- a/src/Kp2aBusinessLogic/database/Database.cs +++ b/src/Kp2aBusinessLogic/database/Database.cs @@ -124,7 +124,9 @@ namespace keepass2android try { IFileStorage fileStorage = _app.GetFileStorage(iocInfo); + var fileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo); pwDatabase.Open(fileStorage.OpenFileForRead(iocInfo), fileStorage.GetFilenameWithoutPathAndExt(iocInfo), iocInfo, compositeKey, status); + LastFileVersion = fileVersion; } catch (Exception) { @@ -140,8 +142,6 @@ namespace keepass2android status.UpdateSubMessage(""); - LastFileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo); - Root = pwDatabase.RootGroup; PopulateGlobals(Root); diff --git a/src/Kp2aUnitTests/Kp2aUnitTests.csproj b/src/Kp2aUnitTests/Kp2aUnitTests.csproj index 0ad98d38..a6d9e58d 100644 --- a/src/Kp2aUnitTests/Kp2aUnitTests.csproj +++ b/src/Kp2aUnitTests/Kp2aUnitTests.csproj @@ -66,6 +66,7 @@ + diff --git a/src/Kp2aUnitTests/MainActivity.cs b/src/Kp2aUnitTests/MainActivity.cs index cce4f45a..0a1a684b 100644 --- a/src/Kp2aUnitTests/MainActivity.cs +++ b/src/Kp2aUnitTests/MainActivity.cs @@ -19,8 +19,9 @@ namespace Kp2aUnitTests TestRunner runner = new TestRunner(); // Run all tests from this assembly //runner.AddTests(Assembly.GetExecutingAssembly()); - //runner.AddTests(new List { typeof(TestSaveDb)}); - runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadEditSaveWithSyncKdbp")); + runner.AddTests(new List { typeof(TestCachingFileStorage) }); + //runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote")); + //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly")); //runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles")); return runner; } diff --git a/src/Kp2aUnitTests/TestCachingFileStorage.cs b/src/Kp2aUnitTests/TestCachingFileStorage.cs new file mode 100644 index 00000000..581c72ed --- /dev/null +++ b/src/Kp2aUnitTests/TestCachingFileStorage.cs @@ -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); + } + } + + /// + /// Tests correct behavior in case that either remote or cache are not available + /// + [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(); + } + + /// + /// tests correct behaviour after modifiying the local cache (with the remote file being unchanged) and remote being + /// either unavailable or available + /// + [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); + } + + /// + /// tests correct behaviour after modifiying both the local cache and the remote file + /// + [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 + } +} diff --git a/src/Kp2aUnitTests/TestSaveDb.cs b/src/Kp2aUnitTests/TestSaveDb.cs index b3316575..3a8d3b84 100644 --- a/src/Kp2aUnitTests/TestSaveDb.cs +++ b/src/Kp2aUnitTests/TestSaveDb.cs @@ -232,26 +232,18 @@ namespace Kp2aUnitTests } - + /* [TestMethod] public void TestSaveAsWhenReadOnly() { 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] public void TestSaveAsWhenSyncError() { Assert.Fail("TODO: Test "); - } + }*/ [TestMethod] public void TestLoadAndSave_TestIdenticalFiles() diff --git a/src/keepass2android/AttachmentContentProvider.cs b/src/keepass2android/AttachmentContentProvider.cs index 59b0378d..6326ef96 100644 --- a/src/keepass2android/AttachmentContentProvider.cs +++ b/src/keepass2android/AttachmentContentProvider.cs @@ -11,7 +11,8 @@ namespace keepass2android /// [ContentProvider(new[]{"keepass2android."+AppNames.PackagePart+".provider"})] public class AttachmentContentProvider : ContentProvider { - + public const string AttachmentCacheSubDir = "AttachmentCache"; + private const String ClassName = "AttachmentContentProvider"; // The authority is the symbolic name for the provider class @@ -37,7 +38,8 @@ namespace keepass2android // E.g. // 'content://keepass2android.provider/Test.txt' // 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; // Create & return a ParcelFileDescriptor pointing to the file diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index fe7923ec..48125078 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -35,6 +35,7 @@ using KeePassLib.Security; using Android.Webkit; using Android.Graphics; using Java.IO; +using keepass2android.Io; namespace keepass2android { @@ -230,7 +231,7 @@ namespace keepass2android ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this); string binaryDirectory = prefs.GetString(GetString(Resource.String.BinaryDirectory_key), GetString(Resource.String.BinaryDirectory_default)); if (writeToCacheDirectory) - binaryDirectory = CacheDir.Path; + binaryDirectory = CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir; string filepart = key; if (writeToCacheDirectory) @@ -528,30 +529,16 @@ namespace keepass2android public void ClearCache() { try { - File dir = CacheDir; - if (dir != null && dir.IsDirectory) { - DeleteDir(dir); + File dir = new File(CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir); + if (dir.IsDirectory) { + IoUtil.DeleteDir(dir); } } 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) { switch ( item.ItemId ) { case Resource.Id.menu_donate: diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index 0a63549a..84ba4e08 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -150,7 +150,9 @@ False - + + Designer + False