diff --git a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs index e2fcb571..0fb391aa 100644 --- a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net; using System.Text; using Android.App; @@ -57,7 +58,19 @@ namespace keepass2android.Io public Stream OpenFileForRead(IOConnectionInfo ioc) { - return IOConnection.OpenRead(ioc); + try + { + return IOConnection.OpenRead(ioc); + } + catch (WebException ex) + { + if ((ex.Response is HttpWebResponse) && (((HttpWebResponse)ex.Response).StatusCode == HttpStatusCode.NotFound)) + { + throw new FileNotFoundException("404!", ioc.Path, ex); + } + throw; + } + } public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) diff --git a/src/Kp2aBusinessLogic/UiStringKey.cs b/src/Kp2aBusinessLogic/UiStringKey.cs index 4d97d0f6..e595e373 100644 --- a/src/Kp2aBusinessLogic/UiStringKey.cs +++ b/src/Kp2aBusinessLogic/UiStringKey.cs @@ -39,6 +39,7 @@ namespace keepass2android DownloadingRemoteFile, UploadingFile, FilesInSync, - SynchronizedDatabaseSuccessfully + SynchronizedDatabaseSuccessfully, + RestoringRemoteFile } } \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/database/SynchronizeCachedDatabase.cs b/src/Kp2aBusinessLogic/database/SynchronizeCachedDatabase.cs index 70670f78..f2a5292b 100644 --- a/src/Kp2aBusinessLogic/database/SynchronizeCachedDatabase.cs +++ b/src/Kp2aBusinessLogic/database/SynchronizeCachedDatabase.cs @@ -13,6 +13,7 @@ namespace keepass2android { private readonly Context _context; private readonly IKp2aApp _app; + private SaveDb _saveDb; public SynchronizeCachedDatabase(Context context, IKp2aApp app, OnFinish finish) : base(finish) @@ -37,10 +38,19 @@ namespace keepass2android //download file from remote location and calculate hash: StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.DownloadingRemoteFile)); string hash; - //todo: catch filenotfound and upload then - MemoryStream remoteData = cachingFileStorage.GetRemoteDataAndHash(ioc, out hash); - - //todo: what happens if something fails here? + + MemoryStream remoteData; + try + { + remoteData = cachingFileStorage.GetRemoteDataAndHash(ioc, out hash); + } + catch (FileNotFoundException) + { + StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.RestoringRemoteFile)); + cachingFileStorage.UpdateRemoteFile(ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions)); + Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); + return; + } //check if remote file was modified: if (cachingFileStorage.GetBaseVersionHash(ioc) != hash) @@ -49,7 +59,7 @@ namespace keepass2android if (cachingFileStorage.HasLocalChanges(ioc)) { //conflict! need to merge - SaveDb saveDb = new SaveDb(_context, _app, new ActionOnFinish((success, result) => + _saveDb = new SaveDb(_context, _app, new ActionOnFinish((success, result) => { if (!success) { @@ -59,8 +69,9 @@ namespace keepass2android { Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); } + _saveDb = null; }), false, remoteData); - saveDb.Run(); + _saveDb.Run(); } else { @@ -94,5 +105,11 @@ namespace keepass2android } } + + public void JoinWorkerThread() + { + if (_saveDb != null) + _saveDb.JoinWorkerThread(); + } } } diff --git a/src/Kp2aUnitTests/MainActivity.cs b/src/Kp2aUnitTests/MainActivity.cs index 5017bbc3..bf437fba 100644 --- a/src/Kp2aUnitTests/MainActivity.cs +++ b/src/Kp2aUnitTests/MainActivity.cs @@ -20,6 +20,8 @@ namespace Kp2aUnitTests // Run all tests from this assembly runner.AddTests(Assembly.GetExecutingAssembly()); //runner.AddTests(new List { typeof(TestSynchronizeCachedDatabase) }); + //runner.AddTests(new List { typeof(TestLoadDb) });}} + //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")); diff --git a/src/Kp2aUnitTests/TestCachingFileStorage.cs b/src/Kp2aUnitTests/TestCachingFileStorage.cs index 97f043dc..6dfe7c18 100644 --- a/src/Kp2aUnitTests/TestCachingFileStorage.cs +++ b/src/Kp2aUnitTests/TestCachingFileStorage.cs @@ -168,7 +168,32 @@ namespace Kp2aUnitTests Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled); Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled); - Assert.AreEqual(newContent, File.ReadAllText(CachingTestFile)); + Assert.AreEqual(newContent, File.ReadAllText(CachingTestFile)); + } + + + [TestMethod] + public void TestLoadFromRemoteWhenRemoteDeleted() + { + SetupFileStorage(); + + //read the file once. Should now be in the cache. + ReadToMemoryStream(_fileStorage, CachingTestFile); + + //delete remote file: + _testFileStorage.DeleteFile(IocForCacheFile); + + //read again. shouldn't throw and give the same result: + var memStream = ReadToMemoryStream(_fileStorage, CachingTestFile); + + //check if we received the correct content: + Assert.AreEqual(_defaultCacheFileContents, MemoryStreamToString(memStream)); + + Assert.IsTrue(_testCacheSupervisor.CouldntOpenFromRemoteCalled); + Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled); + Assert.IsFalse(_testCacheSupervisor.RestoredRemoteCalled); + + } private void WriteContentToCacheFile(string newContent) diff --git a/src/Kp2aUnitTests/TestLoadDb.cs b/src/Kp2aUnitTests/TestLoadDb.cs index 29de807e..4e4d9ef9 100644 --- a/src/Kp2aUnitTests/TestLoadDb.cs +++ b/src/Kp2aUnitTests/TestLoadDb.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System; +using System.IO; +using System.Linq; using System.Threading; using Android.App; using Android.OS; using KeePassLib.Serialization; using Microsoft.VisualStudio.TestTools.UnitTesting; using keepass2android; +using keepass2android.Io; namespace Kp2aUnitTests { @@ -91,6 +94,135 @@ namespace Kp2aUnitTests } + [TestMethod] + public void LoadFromRemote1and1() + { + var ioc = RemoteIoc1and1; //note: this property is defined in "TestLoadDbCredentials.cs" which is deliberately excluded from Git because the credentials are not public! + IKp2aApp app = new TestKp2aApp(); + app.CreateNewDatabase(); + + bool loadSuccesful = false; + LoadDb task = new LoadDb(app, ioc, "test", null, new ActionOnFinish((success, message) => + { + if (!success) + Android.Util.Log.Debug("KP2ATest", "error loading db: " + message); + loadSuccesful = success; + }) + ); + ProgressTask pt = new ProgressTask(app, Application.Context, task); + Android.Util.Log.Debug("KP2ATest", "Running ProgressTask"); + pt.Run(); + pt.JoinWorkerThread(); + Android.Util.Log.Debug("KP2ATest", "PT.run finished"); + Assert.IsTrue(loadSuccesful, "didn't succesfully load database :-("); + + } + + + [TestMethod] + public void LoadFromRemote1and1NonExisting() + { + var ioc = RemoteIoc1and1NonExisting; //note: this property is defined in "TestLoadDbCredentials.cs" which is deliberately excluded from Git because the credentials are not public! + IKp2aApp app = new TestKp2aApp(); + app.CreateNewDatabase(); + + bool loadSuccesful = false; + bool gotError = false; + LoadDb task = new LoadDb(app, ioc, "test", null, new ActionOnFinish((success, message) => + { + if (!success) + { + Android.Util.Log.Debug("KP2ATest", "error loading db: " + message); + gotError = true; + } + loadSuccesful = success; + }) + ); + ProgressTask pt = new ProgressTask(app, Application.Context, task); + Android.Util.Log.Debug("KP2ATest", "Running ProgressTask"); + pt.Run(); + pt.JoinWorkerThread(); + Android.Util.Log.Debug("KP2ATest", "PT.run finished"); + Assert.IsFalse(loadSuccesful); + Assert.IsTrue(gotError); + } + + [TestMethod] + public void LoadFromRemote1and1WrongCredentials() + { + var ioc = RemoteIoc1and1WrongCredentials; //note: this property is defined in "TestLoadDbCredentials.cs" which is deliberately excluded from Git because the credentials are not public! + IKp2aApp app = new TestKp2aApp(); + app.CreateNewDatabase(); + + bool loadSuccesful = false; + bool gotError = false; + LoadDb task = new LoadDb(app, ioc, "test", null, new ActionOnFinish((success, message) => + { + if (!success) + { + Android.Util.Log.Debug("KP2ATest", "error loading db: " + message); + gotError = true; + } + loadSuccesful = success; + }) + ); + ProgressTask pt = new ProgressTask(app, Application.Context, task); + Android.Util.Log.Debug("KP2ATest", "Running ProgressTask"); + pt.Run(); + pt.JoinWorkerThread(); + Android.Util.Log.Debug("KP2ATest", "PT.run finished"); + Assert.IsFalse(loadSuccesful); + Assert.IsTrue(gotError); + + } + + [TestMethod] + public void FileNotFoundExceptionWithWebDav() + { + var fileStorage = new BuiltInFileStorage(); + + //should work: + using (var stream = fileStorage.OpenFileForRead(RemoteIoc1and1)) + { + stream.CopyTo(new MemoryStream()); + } + + //shouldn't give FileNotFound: + bool gotException = false; + try + { + using (var stream = fileStorage.OpenFileForRead(RemoteIoc1and1WrongCredentials)) + { + stream.CopyTo(new MemoryStream()); + } + } + catch (FileNotFoundException) + { + Assert.Fail("shouldn't get FileNotFound with wrong credentials"); + } + catch (Exception e) + { + Kp2aLog.Log("received "+e); + gotException = true; + } + Assert.IsTrue(gotException); + //should give FileNotFound: + gotException = false; + try + { + using (var stream = fileStorage.OpenFileForRead(RemoteIoc1and1NonExisting)) + { + stream.CopyTo(new MemoryStream()); + } + } + catch (FileNotFoundException) + { + gotException = true; + } + Assert.IsTrue(gotException); + } + + [TestMethod] public void TestLoadKdbpWithPasswordOnly() { diff --git a/src/Kp2aUnitTests/TestSynchronizeCachedDatabase.cs b/src/Kp2aUnitTests/TestSynchronizeCachedDatabase.cs index f4279727..e0f543f3 100644 --- a/src/Kp2aUnitTests/TestSynchronizeCachedDatabase.cs +++ b/src/Kp2aUnitTests/TestSynchronizeCachedDatabase.cs @@ -22,15 +22,6 @@ namespace Kp2aUnitTests private TestCacheSupervisor _testCacheSupervisor = new TestCacheSupervisor(); private TestFileStorage _testFileStorage = new TestFileStorage(); - [TestMethod] - public void TestTodos() - { - Assert.IsFalse(true, "Wird immer ManagedTransform benutzt??"); - Assert.IsFalse(true, "TODOs in SyncDb"); - Assert.IsFalse(true, "FileNotFound"); - Assert.IsFalse(true, "Test merge files"); - } - protected override TestKp2aApp CreateTestKp2aApp() { TestKp2aApp app = base.CreateTestKp2aApp(); @@ -100,6 +91,87 @@ namespace Kp2aUnitTests AssertDatabasesAreEqual(app.GetDb().KpDatabase, appRemoteLoaded.GetDb().KpDatabase); } + [TestMethod] + public void TestSyncWhenRemoteDeleted() + { + //create the default database: + TestKp2aApp app = SetupAppWithDefaultDatabase(); + + IOConnection.DeleteFile(new IOConnectionInfo {Path = DefaultFilename}); + //save it and reload it so we have a base version ("remote" and in the cache) + SaveDatabase(app); + app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + //delete remote: + IOConnection.DeleteFile(new IOConnectionInfo { Path = DefaultFilename }); + + string resultMessage; + bool wasSuccessful; + + //sync: + Synchronize(app, out wasSuccessful, out resultMessage); + Assert.IsTrue(wasSuccessful); + Assert.AreEqual(resultMessage, app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); + + //ensure the file is back here: + var app2 = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + AssertDatabasesAreEqual(app.GetDb().KpDatabase, app2.GetDb().KpDatabase); + } + + [TestMethod] + public void TestSyncWhenConflict() + { + //create the default database: + TestKp2aApp app = SetupAppWithDefaultDatabase(); + + IOConnection.DeleteFile(new IOConnectionInfo {Path = DefaultFilename}); + //save it and reload it so we have a base version ("remote" and in the cache) + SaveDatabase(app); + app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + var app2 = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + app2.FileStorage = _testFileStorage; //give app2 direct access to the remote file + + //go offline: + _testFileStorage.Offline = true; + + + string resultMessage; + bool wasSuccessful; + + //modify the database by adding a group in both apps: + PwGroup newGroup1 = new PwGroup(true, true, "TestGroup", PwIcon.Apple); + app.GetDb().KpDatabase.RootGroup.AddGroup(newGroup1, true); + PwGroup newGroup2 = new PwGroup(true, true, "TestGroupApp2", PwIcon.Apple); + app2.GetDb().KpDatabase.RootGroup.AddGroup(newGroup2, true); + //save the database again (will be saved locally only for "app") + SaveDatabase(app); + Assert.IsTrue(_testCacheSupervisor.CouldntSaveToRemoteCalled); + _testCacheSupervisor.CouldntSaveToRemoteCalled = false; + + //go online again: + _testFileStorage.Offline = false; + + //...and remote only for "app2": + SaveDatabase(app2); + + //try to sync: + Synchronize(app, out wasSuccessful, out resultMessage); + + Assert.IsTrue(wasSuccessful); + Assert.AreEqual(UiStringKey.SynchronizedDatabaseSuccessfully.ToString(), resultMessage); + + //build app2 with the newGroup1: + app2.GetDb().KpDatabase.RootGroup.AddGroup(newGroup1, true); + + var app3 = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + AssertDatabasesAreEqual(app.GetDb().KpDatabase, app2.GetDb().KpDatabase); + AssertDatabasesAreEqual(app.GetDb().KpDatabase, app3.GetDb().KpDatabase); + + + + } + private void Synchronize(TestKp2aApp app, out bool wasSuccessful, out string resultMessage) { bool success = false; @@ -110,6 +182,7 @@ namespace Kp2aUnitTests result = _result; })); sync.Run(); + sync.JoinWorkerThread(); wasSuccessful = success; resultMessage = result; } diff --git a/src/keepass2android/Resources/Resource.designer.cs b/src/keepass2android/Resources/Resource.designer.cs index ff9bd497..99abc84d 100644 --- a/src/keepass2android/Resources/Resource.designer.cs +++ b/src/keepass2android/Resources/Resource.designer.cs @@ -1302,32 +1302,32 @@ namespace keepass2android // aapt resource value: 0x7f0800fa public const int BinaryDirectory_title = 2131230970; + // aapt resource value: 0x7f080143 + public const int ChangeLog = 2131231043; + + // aapt resource value: 0x7f080142 + public const int ChangeLog_0_7 = 2131231042; + + // aapt resource value: 0x7f080140 + public const int ChangeLog_0_8 = 2131231040; + + // aapt resource value: 0x7f08013f + public const int ChangeLog_0_8_1 = 2131231039; + + // aapt resource value: 0x7f08013e + public const int ChangeLog_0_8_2 = 2131231038; + // aapt resource value: 0x7f08013d - public const int ChangeLog = 2131231037; + public const int ChangeLog_0_8_3 = 2131231037; // aapt resource value: 0x7f08013c - public const int ChangeLog_0_7 = 2131231036; + public const int ChangeLog_0_8_4 = 2131231036; - // aapt resource value: 0x7f08013a - public const int ChangeLog_0_8 = 2131231034; - - // aapt resource value: 0x7f080139 - public const int ChangeLog_0_8_1 = 2131231033; - - // aapt resource value: 0x7f080138 - public const int ChangeLog_0_8_2 = 2131231032; - - // aapt resource value: 0x7f080137 - public const int ChangeLog_0_8_3 = 2131231031; - - // aapt resource value: 0x7f080136 - public const int ChangeLog_0_8_4 = 2131231030; + // aapt resource value: 0x7f080141 + public const int ChangeLog_keptDonate = 2131231041; // aapt resource value: 0x7f08013b - public const int ChangeLog_keptDonate = 2131231035; - - // aapt resource value: 0x7f080135 - public const int ChangeLog_title = 2131231029; + public const int ChangeLog_title = 2131231035; // aapt resource value: 0x7f08002a public const int CheckForFileChangesOnSave_key = 2131230762; @@ -1359,9 +1359,15 @@ namespace keepass2android // aapt resource value: 0x7f080129 public const int DeletingGroup = 2131231017; + // aapt resource value: 0x7f080136 + public const int DownloadingRemoteFile = 2131231030; + // aapt resource value: 0x7f080083 public const int FileNotFound = 2131230851; + // aapt resource value: 0x7f080139 + public const int FilesInSync = 2131231033; + // aapt resource value: 0x7f080096 public const int InvalidPassword = 2131230870; @@ -1437,6 +1443,9 @@ namespace keepass2android // aapt resource value: 0x7f0800e5 public const int RememberRecentFiles_title = 2131230949; + // aapt resource value: 0x7f080138 + public const int RestoringRemoteFile = 2131231032; + // aapt resource value: 0x7f0800ff public const int SaveAttachmentDialog_open = 2131230975; @@ -1482,6 +1491,12 @@ namespace keepass2android // aapt resource value: 0x7f08002c public const int SuggestionsURL = 2131230764; + // aapt resource value: 0x7f08013a + public const int SynchronizedDatabaseSuccessfully = 2131231034; + + // aapt resource value: 0x7f080135 + public const int SynchronizingCachedDatabase = 2131231029; + // aapt resource value: 0x7f080132 public const int SynchronizingDatabase = 2131231026; @@ -1506,6 +1521,9 @@ namespace keepass2android // aapt resource value: 0x7f08012b public const int UndoingChanges = 2131231019; + // aapt resource value: 0x7f080137 + public const int UploadingFile = 2131231031; + // aapt resource value: 0x7f080027 public const int UsageCount_key = 2131230759; diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index b4882d7f..bfaea71f 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -264,7 +264,14 @@ Yes, merge No, overwrite - Change log + Synchronizing cached database... + Downloading remote file... + Uploading file... + Restoring remote file... + Files are in sync. + Database synchronized successfully! + + Change log Version 0.8.4\n * External database changes are detected and merged when saving\n