diff --git a/src/KeePass.sln b/src/KeePass.sln index d4e8f78f..2402aa78 100644 --- a/src/KeePass.sln +++ b/src/KeePass.sln @@ -51,6 +51,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FtpClientTest", "FtpClientT EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.FtpClient.Android", "netftpandroid\System.Net.FtpClient\System.Net.FtpClient.Android.csproj", "{146FD497-BA03-4740-B6C5-5C84EA8FCDE2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kp2aUnitTests2", "Kp2aUnitTests2\Kp2aUnitTests2.csproj", "{B6062CC3-C238-40F3-B4BE-DD647300F96C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebDavAndroid", "WebDavAndroid\WebDavAndroid.csproj", "{B6B866AF-D69C-4317-9838-51CF743E24B3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -571,6 +575,48 @@ Global {146FD497-BA03-4740-B6C5-5C84EA8FCDE2}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU {146FD497-BA03-4740-B6C5-5C84EA8FCDE2}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU {146FD497-BA03-4740-B6C5-5C84EA8FCDE2}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|Mixed Platforms.Deploy.0 = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|Win32.ActiveCfg = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Debug|x64.ActiveCfg = Debug|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|Any CPU.Build.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|Any CPU.Deploy.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|Mixed Platforms.Deploy.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|Win32.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.Release|x64.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|Any CPU.Deploy.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|Mixed Platforms.Deploy.0 = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU + {B6062CC3-C238-40F3-B4BE-DD647300F96C}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Debug|Win32.ActiveCfg = Debug|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Release|Any CPU.Build.0 = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Release|Win32.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.Release|x64.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU + {B6B866AF-D69C-4317-9838-51CF743E24B3}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs index b3bf4245..eb4973ce 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.Net; +using System.Net.Http; using System.Security; using Android; using Android.App; @@ -12,6 +13,7 @@ using Android.OS; using Java.IO; using KeePassLib.Serialization; using KeePassLib.Utility; +using ModernHttpClient; using File = System.IO.File; using FileNotFoundException = System.IO.FileNotFoundException; using IOException = System.IO.IOException; diff --git a/src/Kp2aBusinessLogic/Io/HttpFileStorage.cs b/src/Kp2aBusinessLogic/Io/HttpFileStorage.cs new file mode 100644 index 00000000..9b867978 --- /dev/null +++ b/src/Kp2aBusinessLogic/Io/HttpFileStorage.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using KeePassLib; +using KeePassLib.Serialization; +using KeePassLib.Utility; +using ModernHttpClient; + +namespace keepass2android.Io +{ + public class HttpFileStorage: IFileStorage + { + public IEnumerable SupportedProtocols + { + get + { + yield return "http"; + yield return "https"; + } + } + + HttpClient GetHttpClient(IOConnectionInfo ioc) + { + var handler = new NativeMessageHandler(); + //var handler = new HttpClientHandler(); + + if ((ioc.UserName.Length > 0) || (ioc.Password.Length > 0)) + { + int backslashPos = ioc.UserName.IndexOf("\\", StringComparison.Ordinal); + if (backslashPos > 0) + { + string domain = ioc.UserName.Substring(0, backslashPos); + string user = ioc.UserName.Substring(backslashPos + 1); + handler.Credentials = new NetworkCredential(user, ioc.Password, domain); + } + else + { + handler.PreAuthenticate = true; + handler.Credentials = new NetworkCredential(ioc.UserName, ioc.Password); + } + } + + return new HttpClient(handler); + + } + + public void Delete(IOConnectionInfo ioc) + { + GetHttpClient(ioc).DeleteAsync(ioc.Path).Result.EnsureSuccessStatusCode(); + } + + public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion) + { + return false; + } + + public string GetCurrentFileVersionFast(IOConnectionInfo ioc) + { + return null; + } + + public Stream OpenFileForRead(IOConnectionInfo ioc) + { + return GetHttpClient(ioc).GetStreamAsync(ioc.Path).Result; + } + + + class UploadOnCloseMemoryStream : MemoryStream + { + IOConnectionInfo ioc; + private HttpClient client; + + + public UploadOnCloseMemoryStream(IOConnectionInfo _ioc, HttpClient _client) + { + this.ioc = _ioc; + this.client = _client; + } + + public override void Close() + { + base.Close(); + var msg = new HttpRequestMessage(HttpMethod.Put, ioc.Path); + msg.Headers.Add("Translate", "f"); + msg.Content = new StreamContent(new MemoryStream(this.ToArray())); + + client.SendAsync(msg).Result.EnsureSuccessStatusCode(); + } + + } + public class UntransactedWrite : IWriteTransaction + { + private readonly IOConnectionInfo _ioc; + private readonly HttpClient _client; + + + public UntransactedWrite(IOConnectionInfo ioc, HttpClient client) + { + _ioc = ioc; + _client = client; + } + + public void Dispose() + { + + } + + public Stream OpenFile() + { + return new UploadOnCloseMemoryStream(_ioc, _client); + } + + public void CommitWrite() + { + + } + } + + public class TransactedWrite : IWriteTransaction + { + private readonly IOConnectionInfo _ioc; + private readonly HttpFileStorage _fileStorage; + private readonly IOConnectionInfo _iocTemp; + + + + public TransactedWrite(IOConnectionInfo ioc, HttpFileStorage fileStorage) + { + _ioc = ioc; + _iocTemp = _ioc.CloneDeep(); + _iocTemp.Path += "." + new PwUuid(true).ToHexString().Substring(0, 6) + ".tmp"; + + _fileStorage = fileStorage; + } + + public void Dispose() + { + + } + + public Stream OpenFile() + { + return new UploadOnCloseMemoryStream(_ioc, _fileStorage.GetHttpClient(_ioc)); + } + + public void CommitWrite() + { + var client = _fileStorage.GetHttpClient(_ioc); + client.DeleteAsync(_ioc.Path).Result.EnsureSuccessStatusCode(); + var msg = new HttpRequestMessage(new HttpMethod("MOVE"), _iocTemp.Path); + msg.Headers.Add("Destination", _ioc.Path); + client.SendAsync(msg).Result.EnsureSuccessStatusCode(); + } + } + + public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) + { + if (useFileTransaction) + return new TransactedWrite(ioc, this); + else + return new UntransactedWrite(ioc, GetHttpClient(ioc)); + } + + public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) + { + return UrlUtil.StripExtension( + UrlUtil.GetFileName(ioc.Path)); + } + + public bool RequiresCredentials(IOConnectionInfo ioc) + { + return ioc.CredSaveMode != IOCredSaveMode.SaveCred; + } + + public void CreateDirectory(IOConnectionInfo ioc, string newDirName) + { + throw new NotImplementedException(); + } + + public IEnumerable ListContents(IOConnectionInfo ioc) + { + throw new NotImplementedException(); + } + + public FileDescription GetFileDescription(IOConnectionInfo ioc) + { + throw new NotImplementedException(); + } + + public bool RequiresSetup(IOConnectionInfo ioConnection) + { + return false; + } + + public string IocToPath(IOConnectionInfo ioc) + { + return ioc.Path; + } + + public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId) + { + activity.PerformManualFileSelect(isForSave, requestCode, protocolId); + } + + public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, + bool alwaysReturnSuccess) + { + Intent intent = new Intent(); + activity.IocToIntent(intent, ioc); + activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent); + } + + public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) + { + + } + + public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState) + { + + } + + public void OnResume(IFileStorageSetupActivity activity) + { + } + + public void OnStart(IFileStorageSetupActivity activity) + { + } + + public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) + { + } + + public string GetDisplayName(IOConnectionInfo ioc) + { + return ioc.GetDisplayName(); + } + + public string CreateFilePath(string parent, string newFilename) + { + if (!parent.EndsWith("/")) + parent += "/"; + return parent + newFilename; + } + + public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) + { + return IoUtil.GetParentPath(ioc); + } + + public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) + { + IOConnectionInfo res = folderPath.CloneDeep(); + if (!res.Path.EndsWith("/")) + res.Path += "/"; + res.Path += filename; + return res; + } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + return true; + } + + public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut reason = null) + { + return false; + } + + public void ResolveAccount(IOConnectionInfo ioc) + { + + } + } +} \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs new file mode 100644 index 00000000..9fbbc503 --- /dev/null +++ b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.FtpClient; +using Android.Content; +using Android.OS; +using Android.Preferences; +using KeePassLib; +using KeePassLib.Serialization; +using KeePassLib.Utility; + +namespace keepass2android.Io +{ + public class NetFtpFileStorage: IFileStorage + { + struct ConnectionSettings + { + public FtpEncryptionMode EncryptionMode {get; set; } + + public static ConnectionSettings FromIoc(IOConnectionInfo ioc) + { + if (ioc.Path.StartsWith(Kp2AAccountPathPrefix)) + throw new InvalidOperationException("cannot extract settings from account-path"); + string path = ioc.Path; + int schemeLength = path.IndexOf("://", StringComparison.Ordinal); + path = path.Substring(schemeLength + 3); + string settings = path.Substring(0, path.IndexOf("/", StringComparison.Ordinal)); + return new ConnectionSettings() + { + EncryptionMode = (FtpEncryptionMode) int.Parse(settings) + }; + + } + } + + private const string Kp2AAccountPathPrefix = "__kp2a_account__"; + private readonly Context _context; + + public NetFtpFileStorage(Context context) + { + _context = context; + } + + public IEnumerable SupportedProtocols + { + get { yield return "ftp"; } + } + + public void Delete(IOConnectionInfo ioc) + { + using (FtpClient client = GetClient(ioc)) + { + client.DeleteFile(IocPathToUri(ioc.Path).PathAndQuery); + } + } + + + internal FtpClient GetClient(IOConnectionInfo ioc, bool enableCloneClient = true) + { + FtpClient client = new FtpClient(); + if ((ioc.UserName.Length > 0) || (ioc.Password.Length > 0)) + client.Credentials = new NetworkCredential(ioc.UserName, ioc.Password); + else + client.Credentials = new NetworkCredential("anonymous", ""); //TODO TEST + + Uri uri = IocPathToUri(ioc.Path); + client.Host = uri.Host; + if (!uri.IsDefaultPort) //TODO test + client.Port = uri.Port; + + //TODO Validate + //client.ValidateCertificate += app... + + // we don't need to be thread safe in a classic sense, but OpenRead and OpenWrite don't + //perform the actual stream operation so we'd need to wrap the stream (or just enable this:) + client.EnableThreadSafeDataConnections = enableCloneClient; + + client.EncryptionMode = ConnectionSettings.FromIoc(ioc).EncryptionMode; + + client.Connect(); + return client; + } + + internal Uri IocPathToUri(string path) + { + //remove addition stuff like TLS param + int schemeLength = path.IndexOf("://", StringComparison.Ordinal); + string scheme = path.Substring(0, schemeLength); + path = path.Substring(schemeLength + 3); + string settings = path.Substring(0, path.IndexOf("/", StringComparison.Ordinal)); + path = path.Substring(settings.Length + 1); + return new Uri(scheme + "://" + path); + } + + private string IocPathFromUri(IOConnectionInfo baseIoc, Uri uri) + { + string basePath = baseIoc.Path; + int schemeLength = basePath.IndexOf("://", StringComparison.Ordinal); + string scheme = basePath.Substring(0, schemeLength); + basePath = basePath.Substring(schemeLength + 3); + string baseSettings = basePath.Substring(0, basePath.IndexOf("/", StringComparison.Ordinal)); + return scheme + "://" + baseSettings + "/" + uri.AbsolutePath; //TODO does this contain Query? + } + + + public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion) + { + return false; + } + + public string GetCurrentFileVersionFast(IOConnectionInfo ioc) + { + return null; + } + + public Stream OpenFileForRead(IOConnectionInfo ioc) + { + using (var cl = GetClient(ioc)) + { + return cl.OpenRead(IocPathToUri(ioc.Path).PathAndQuery, FtpDataType.Binary, 0); + } + } + + public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) + { + if (useFileTransaction) + return new UntransactedWrite(ioc, this); + else + return new TransactedWrite(ioc, this); + } + + public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) + { + //TODO does this work when flags are encoded in the iocPath? + return UrlUtil.StripExtension( + UrlUtil.GetFileName(ioc.Path)); + } + + public bool RequiresCredentials(IOConnectionInfo ioc) + { + return ioc.CredSaveMode != IOCredSaveMode.SaveCred; + } + + public void CreateDirectory(IOConnectionInfo ioc, string newDirName) + { + using (var client = GetClient(ioc)) + { + client.CreateDirectory(IocPathToUri(ioc.Path).PathAndQuery); + } + } + + public IEnumerable ListContents(IOConnectionInfo ioc) + { + using (var client = GetClient(ioc)) + { + List files = new List(); + foreach (FtpListItem item in client.GetListing(IocPathToUri(ioc.Path).PathAndQuery, + FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks)) + { + + switch (item.Type) + { + case FtpFileSystemObjectType.Directory: + files.Add(new FileDescription() + { + CanRead = true, + CanWrite = true, + DisplayName = item.Name, + IsDirectory = true, + LastModified = item.Modified, + Path = IocPathFromUri(ioc, new Uri(item.FullName)) + }); + break; + case FtpFileSystemObjectType.File: + files.Add(new FileDescription() + { + CanRead = true, + CanWrite = true, + DisplayName = item.Name, + IsDirectory = false, + LastModified = item.Modified, + Path = IocPathFromUri(ioc, new Uri(item.FullName)), + SizeInBytes = item.Size + }); + break; + + } + } + return files; + } + } + + + public FileDescription GetFileDescription(IOConnectionInfo ioc) + { + //TODO when is this called? + //is it very inefficient to connect for each description? + + using (FtpClient client = GetClient(ioc)) + { + var uri = IocPathToUri(ioc.Path); + string path = uri.PathAndQuery; + return new FileDescription() + { + CanRead = true, + CanWrite = true, + Path = ioc.Path, + LastModified = client.GetModifiedTime(path), + SizeInBytes = client.GetFileSize(path), + DisplayName = UrlUtil.GetFileName(path), + IsDirectory = false + }; + } + } + + public bool RequiresSetup(IOConnectionInfo ioConnection) + { + return false; + } + + public string IocToPath(IOConnectionInfo ioc) + { + return ioc.Path; + } + + public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId) + { + throw new NotImplementedException(); + } + + public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, + bool alwaysReturnSuccess) + { + Intent intent = new Intent(); + activity.IocToIntent(intent, ioc); + activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent); + } + + public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) + { + + } + + public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState) + { + + } + + public void OnResume(IFileStorageSetupActivity activity) + { + + } + + public void OnStart(IFileStorageSetupActivity activity) + { + + } + + public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) + { + + } + + public string GetDisplayName(IOConnectionInfo ioc) + { + var uri = IocPathToUri(ioc.Path); + return uri.ToString(); //TODO is this good? + } + + public string CreateFilePath(string parent, string newFilename) + { + if (!parent.EndsWith("/")) + parent += "/"; + return parent + newFilename; + } + + public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) + { + return IoUtil.GetParentPath(ioc); + } + + public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) + { + IOConnectionInfo res = folderPath.CloneDeep(); + if (!res.Path.EndsWith("/")) + res.Path += "/"; + res.Path += filename; + return res; + } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + return true; + } + + public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut reason = null) + { + return false; + } + + public void ResolveAccount(IOConnectionInfo ioc) + { + string path = ioc.Path; + int schemeLength = path.IndexOf("://", StringComparison.Ordinal); + string scheme = path.Substring(0, schemeLength); + path = path.Substring(schemeLength+3); + if (path.StartsWith(Kp2AAccountPathPrefix)) + { + string accountId = path.Substring(0, path.IndexOf("/", StringComparison.Ordinal)); + path = path.Substring(accountId.Length + 1); + + var prefs = PreferenceManager.GetDefaultSharedPreferences(_context); + string host = prefs.GetString(accountId + "_Host", null); + int port = prefs.GetInt(accountId + "_Port", 0 /* auto*/); + string initialPath = prefs.GetString(accountId + "_InitPath", ""); + if (initialPath.StartsWith("/")) + initialPath = initialPath.Substring(1); + if ((!initialPath.EndsWith("/") && (initialPath != ""))) + initialPath += "/"; + int encMode = prefs.GetInt(accountId + "_EncMode", (int) FtpEncryptionMode.None); + string settings = encMode.ToString(); + ioc.Path = scheme + "://" + settings + "/" + host + (port == 0 ? "" : (":" + port)) + "/" + initialPath + path; + } + } + + public Stream OpenWrite(IOConnectionInfo ioc) + { + using (var client = GetClient(ioc)) + { + return client.OpenWrite(IocPathToUri(ioc.Path).PathAndQuery); + } + } + } + + public class TransactedWrite : IWriteTransaction + { + private readonly IOConnectionInfo _ioc; + private readonly NetFtpFileStorage _fileStorage; + private readonly IOConnectionInfo _iocTemp; + private FtpClient _client; + + + public TransactedWrite(IOConnectionInfo ioc, NetFtpFileStorage fileStorage) + { + _ioc = ioc; + _iocTemp = _ioc.CloneDeep(); + _iocTemp.Path += "." + new PwUuid(true).ToHexString().Substring(0, 6) + ".tmp"; + + _fileStorage = fileStorage; + } + + public void Dispose() + { + if (_client != null) + _client.Dispose(); + _client = null; + } + + public Stream OpenFile() + { + _client = _fileStorage.GetClient(_ioc, false); + return _client.OpenWrite(_fileStorage.IocPathToUri(_iocTemp.Path).PathAndQuery); + } + + public void CommitWrite() + { + _client.DeleteFile(_fileStorage.IocPathToUri(_ioc.Path).PathAndQuery); + _client.Rename(_fileStorage.IocPathToUri(_iocTemp.Path).PathAndQuery, _fileStorage.IocPathToUri(_ioc.Path).PathAndQuery); + } + } + + public class UntransactedWrite : IWriteTransaction + { + private readonly IOConnectionInfo _ioc; + private readonly NetFtpFileStorage _fileStorage; + + public UntransactedWrite(IOConnectionInfo ioc, NetFtpFileStorage fileStorage) + { + _ioc = ioc; + _fileStorage = fileStorage; + } + + public void Dispose() + { + + } + + public Stream OpenFile() + { + return _fileStorage.OpenWrite(_ioc); + } + + public void CommitWrite() + { + + } + } +} \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index 8aed0d4f..d3b7422b 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -47,11 +47,18 @@ 4 + + ..\Components\modernhttpclient-2.4.2\lib\android\ModernHttpClient.dll + + + ..\Components\modernhttpclient-2.4.2\lib\android\OkHttp.dll + + @@ -83,6 +90,7 @@ + @@ -149,6 +157,12 @@ + + + False + 2.4.2 + +