2009-01-14 17:29:30 +00:00
#region Disclaimer / License
2011-05-09 19:52:28 +00:00
// Copyright (C) 2011, Kenneth Skovhede
2009-01-14 17:29:30 +00:00
// http://www.hexad.dk, opensource@hexad.dk
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System ;
using System.Collections.Generic ;
using System.Text ;
using System.Text.RegularExpressions ;
2010-06-19 21:08:21 +00:00
using Duplicati.Library.Interface ;
2009-01-14 17:29:30 +00:00
namespace Duplicati.Library.Backend
{
2013-05-05 17:07:24 +02:00
public class FTP : IBackend , IStreamingBackend
2009-01-14 17:29:30 +00:00
{
2009-01-15 12:57:52 +00:00
private System . Net . NetworkCredential m_userInfo ;
2012-01-19 19:04:56 +00:00
private readonly string m_url ;
2009-01-14 17:29:30 +00:00
2012-01-19 19:04:56 +00:00
private readonly bool m_useSSL = false ;
private readonly bool m_defaultPassive = true ;
private readonly bool m_passive = false ;
private readonly bool m_listVerify = true ;
2010-02-10 20:44:48 +00:00
2009-01-14 17:29:30 +00:00
public FTP ()
{
}
public FTP ( string url , Dictionary < string , string > options )
{
2011-09-24 11:39:50 +00:00
//This can be made better by keeping a single ftp stream open,
//unfortunately the .Net model does not allow this as the request is
//bound to a single url (path+file).
//
//To fix this, a thirdparty FTP library is required,
//this would also allow a fix for the FTP servers
//that only support SSL during authentication, not during transfers
//
//If you have experience with a stable open source .Net FTP library,
//please let the Duplicati authors know
2013-05-05 23:46:00 +02:00
var u = new Utility . Uri ( url );
2013-05-06 20:36:26 +02:00
u . RequireHost ();
2009-01-15 12:57:52 +00:00
2013-05-05 23:46:00 +02:00
if (! string . IsNullOrEmpty ( u . Username ))
2009-01-15 12:57:52 +00:00
{
m_userInfo = new System . Net . NetworkCredential ();
2013-05-05 23:46:00 +02:00
m_userInfo . UserName = u . Username ;
if (! string . IsNullOrEmpty ( u . Password ))
m_userInfo . Password = u . Password ;
else if ( options . ContainsKey ( "auth-password" ))
m_userInfo . Password = options [ "auth-password" ];
2009-01-15 12:57:52 +00:00
}
2009-07-16 20:36:49 +00:00
else
{
2013-05-05 16:40:37 +02:00
if ( options . ContainsKey ( "auth-username" ))
2009-07-16 20:36:49 +00:00
{
m_userInfo = new System . Net . NetworkCredential ();
2013-05-05 16:40:37 +02:00
m_userInfo . UserName = options [ "auth-username" ];
if ( options . ContainsKey ( "auth-password" ))
m_userInfo . Password = options [ "auth-password" ];
2009-07-16 20:36:49 +00:00
}
}
2009-01-15 12:57:52 +00:00
2012-04-19 18:49:37 +00:00
//Bugfix, see http://connect.microsoft.com/VisualStudio/feedback/details/695227/networkcredential-default-constructor-leaves-domain-null-leading-to-null-object-reference-exceptions-in-framework-code
if ( m_userInfo != null )
m_userInfo . Domain = "" ;
2013-05-05 23:46:00 +02:00
m_url = u . SetScheme ( "ftp" ). SetQuery ( null ). SetCredentials ( null , null ). ToString ();
2009-01-14 17:29:30 +00:00
if (! m_url . EndsWith ( "/" ))
m_url += "/" ;
2011-06-26 10:22:03 +00:00
2011-10-19 05:42:32 +00:00
m_useSSL = Utility . Utility . ParseBoolOption ( options , "use-ssl" );
2012-01-19 19:04:56 +00:00
m_listVerify = ! Utility . Utility . ParseBoolOption ( options , "disable-upload-verify" );
2011-06-26 10:22:03 +00:00
2011-10-19 05:42:32 +00:00
if ( Utility . Utility . ParseBoolOption ( options , "ftp-passive" ))
2011-09-27 06:59:17 +00:00
{
m_defaultPassive = false ;
m_passive = true ;
}
2011-10-19 05:42:32 +00:00
if ( Utility . Utility . ParseBoolOption ( options , "ftp-regular" ))
2011-09-27 06:59:17 +00:00
{
m_defaultPassive = false ;
m_passive = false ;
}
2009-01-14 17:29:30 +00:00
}
#region Regular expression to parse list lines
//Regexps found here: http://www.dotnetfunda.com/articles/article125.aspx
2010-06-09 10:54:58 +00:00
//Modified to allow hyphens in username and groupname
2009-01-14 17:29:30 +00:00
internal readonly static Regex [] PARSEFORMATS =
{
2010-06-09 10:54:58 +00:00
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+(?<groupname>\S+)\s+(?<username>\S+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)" ),
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+(?<groupname>\d+)\s+(?<username>\d+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)" ),
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+(?<groupname>\d+)\s+(?<username>\d+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)" ),
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+(?<groupname>\S+)\s+(?<username>\S+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)" ),
2009-01-14 17:29:30 +00:00
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})(\s+)(?<size>(\d+))(\s+)(?<ctbit>(\w+\s\w+))(\s+)(?<size2>(\d+))\s+(?<timestamp>\w+\s+\d+\s+\d{2}:\d{2})\s+(?<name>.+)" ),
new Regex ( @"(?<timestamp>\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+(?<dir>\<\w+\>){0,1}(?<size>\d+){0,1}\s+(?<name>.+)" ),
new Regex ( @"([<timestamp>]*\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+([<dir>]*\<\w+\>){0,1}([<size>]*\d+){0,1}\s+([<name>]*.+)" )
};
#endregion
2010-06-09 10:54:58 +00:00
2009-01-15 14:47:05 +00:00
private static Match MatchLine ( string line )
2009-01-14 17:29:30 +00:00
{
Match m = null ;
foreach ( Regex s in PARSEFORMATS )
if (( m = s . Match ( line )). Success )
return m ;
return null ;
}
2009-01-15 14:47:05 +00:00
public static FileEntry ParseLine ( string line )
2009-01-14 17:29:30 +00:00
{
Match m = MatchLine ( line );
if ( m == null )
return null ;
FileEntry f = new FileEntry ( m . Groups [ "name" ]. Value );
string time = m . Groups [ "timestamp" ]. Value ;
string dir = m . Groups [ "dir" ]. Value ;
2009-07-25 09:03:37 +00:00
//Unused
2009-08-27 20:40:31 +00:00
//string permission = m.Groups["permission"].Value;
2009-07-25 09:03:37 +00:00
2009-01-14 17:29:30 +00:00
if ( dir != "" && dir != "-" )
f . IsFolder = true ;
else
f . Size = long . Parse ( m . Groups [ "size" ]. Value );
DateTime t ;
if ( DateTime . TryParse ( time , out t ))
f . LastAccess = f . LastModification = t ;
return f ;
}
#region IBackendInterface Members
public string DisplayName
{
2009-06-14 21:51:06 +00:00
get { return Strings . FTPBackend . DisplayName ; }
2009-01-14 17:29:30 +00:00
}
public string ProtocolKey
{
get { return "ftp" ; }
}
2009-01-25 19:33:36 +00:00
public bool SupportsStreaming
{
get { return true ; }
}
2011-03-10 19:08:16 +00:00
2010-06-19 21:08:21 +00:00
public List < IFileEntry > List ()
2009-01-14 17:29:30 +00:00
{
2012-01-19 19:04:56 +00:00
return List ( "" );
}
public List < IFileEntry > List ( string filename )
{
System . Net . FtpWebRequest req = CreateRequest ( filename );
2011-03-10 19:08:16 +00:00
req . Method = System . Net . WebRequestMethods . Ftp . ListDirectoryDetails ;
req . UseBinary = false ;
2010-02-10 20:44:48 +00:00
2011-03-10 19:08:16 +00:00
try
{
List < IFileEntry > lst = new List < IFileEntry >();
2012-06-06 12:55:48 +00:00
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
using ( System . Net . WebResponse resp = areq . GetResponse ())
2014-07-11 01:07:48 +02:00
using ( System . IO . Stream rs = areq . GetResponseStream ())
2011-03-10 19:08:16 +00:00
using ( System . IO . StreamReader sr = new System . IO . StreamReader ( new StreamReadHelper ( rs )))
2010-02-10 20:44:48 +00:00
{
2011-03-10 19:08:16 +00:00
string line ;
while (( line = sr . ReadLine ()) != null )
2010-10-23 11:40:43 +00:00
{
2011-03-10 19:08:16 +00:00
FileEntry f = ParseLine ( line );
if ( f != null )
lst . Add ( f );
2010-02-14 22:27:51 +00:00
}
2010-02-10 20:44:48 +00:00
}
2011-03-10 19:08:16 +00:00
return lst ;
}
catch ( System . Net . WebException wex )
{
if ( wex . Response as System . Net . FtpWebResponse != null && ( wex . Response as System . Net . FtpWebResponse ). StatusCode == System . Net . FtpStatusCode . ActionNotTakenFileUnavailable )
throw new Interface . FolderMissingException ( string . Format ( Strings . FTPBackend . MissingFolderError , req . RequestUri . PathAndQuery , wex . Message ), wex );
else
throw ;
2010-01-17 20:22:14 +00:00
}
2009-01-14 17:29:30 +00:00
}
2009-01-25 19:33:36 +00:00
public void Put ( string remotename , System . IO . Stream input )
2009-01-14 17:29:30 +00:00
{
2010-10-23 11:40:43 +00:00
System . Net . FtpWebRequest req = null ;
try
2010-02-10 20:44:48 +00:00
{
2011-03-10 19:08:16 +00:00
req = CreateRequest ( remotename );
req . Method = System . Net . WebRequestMethods . Ftp . UploadFile ;
req . UseBinary = true ;
2012-01-19 19:04:56 +00:00
long streamLen = - 1 ;
try { streamLen = input . Length ; }
catch {}
2010-10-23 11:40:43 +00:00
2012-06-06 12:55:48 +00:00
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
using ( System . IO . Stream rs = areq . GetRequestStream ())
2011-03-10 19:08:16 +00:00
Utility . Utility . CopyStream ( input , rs , true );
2012-01-19 19:04:56 +00:00
if ( m_listVerify )
{
List < IFileEntry > files = List ( remotename );
StringBuilder sb = new StringBuilder ();
foreach ( IFileEntry fe in files )
2012-02-03 07:30:42 +00:00
if ( fe . Name . Equals ( remotename ) || fe . Name . EndsWith ( "/" + remotename ) || fe . Name . EndsWith ( "\\" + remotename ))
2012-01-19 19:04:56 +00:00
{
if ( fe . Size < 0 || streamLen < 0 || fe . Size == streamLen )
return ;
throw new Exception ( string . Format ( Strings . FTPBackend . ListVerifySizeFailure , remotename , fe . Size , streamLen ));
}
else
sb . AppendLine ( fe . Name );
throw new Exception ( string . Format ( Strings . FTPBackend . ListVerifyFailure , remotename , sb . ToString ()));
}
2010-10-23 11:40:43 +00:00
}
catch ( System . Net . WebException wex )
{
if ( req != null && wex . Response as System . Net . FtpWebResponse != null && ( wex . Response as System . Net . FtpWebResponse ). StatusCode == System . Net . FtpStatusCode . ActionNotTakenFileUnavailable )
throw new Interface . FolderMissingException ( string . Format ( Strings . FTPBackend . MissingFolderError , req . RequestUri . PathAndQuery , wex . Message ), wex );
else
throw ;
2010-02-10 20:44:48 +00:00
}
2009-01-14 17:29:30 +00:00
}
2009-01-25 19:33:36 +00:00
public void Put ( string remotename , string localname )
{
using ( System . IO . FileStream fs = System . IO . File . Open ( localname , System . IO . FileMode . Open , System . IO . FileAccess . Read , System . IO . FileShare . Read ))
Put ( remotename , fs );
}
public void Get ( string remotename , System . IO . Stream output )
2009-01-14 17:29:30 +00:00
{
2011-03-10 19:08:16 +00:00
System . Net . FtpWebRequest req = CreateRequest ( remotename );
req . Method = System . Net . WebRequestMethods . Ftp . DownloadFile ;
req . UseBinary = true ;
2009-01-15 12:57:52 +00:00
2012-06-06 12:55:48 +00:00
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
using ( System . Net . WebResponse resp = areq . GetResponse ())
2014-07-11 01:07:48 +02:00
using ( System . IO . Stream rs = areq . GetResponseStream ())
2011-03-10 19:08:16 +00:00
Utility . Utility . CopyStream ( rs , output , false );
2009-01-25 19:33:36 +00:00
}
public void Get ( string remotename , string localname )
{
using ( System . IO . FileStream fs = System . IO . File . Open ( localname , System . IO . FileMode . Create , System . IO . FileAccess . Write , System . IO . FileShare . None ))
Get ( remotename , fs );
2009-01-14 17:29:30 +00:00
}
public void Delete ( string remotename )
{
2011-03-10 19:08:16 +00:00
System . Net . FtpWebRequest req = CreateRequest ( remotename );
req . Method = System . Net . WebRequestMethods . Ftp . DeleteFile ;
2012-06-06 12:55:48 +00:00
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
using ( areq . GetResponse ())
2011-03-10 19:08:16 +00:00
{ }
2009-01-14 17:29:30 +00:00
}
2009-03-21 12:46:54 +00:00
public IList < ICommandLineArgument > SupportedCommands
{
get
{
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
2009-06-14 21:51:06 +00:00
new CommandLineArgument ( "ftp-passive" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionFTPPassiveShort , Strings . FTPBackend . DescriptionFTPPassiveLong , "false" ),
new CommandLineArgument ( "ftp-regular" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionFTPActiveShort , Strings . FTPBackend . DescriptionFTPActiveLong , "true" ),
2013-05-05 16:40:37 +02:00
new CommandLineArgument ( "auth-password" , CommandLineArgument . ArgumentType . Password , Strings . FTPBackend . DescriptionAuthPasswordShort , Strings . FTPBackend . DescriptionAuthPasswordLong ),
new CommandLineArgument ( "auth-username" , CommandLineArgument . ArgumentType . String , Strings . FTPBackend . DescriptionAuthUsernameShort , Strings . FTPBackend . DescriptionAuthUsernameLong ),
2010-02-10 20:44:48 +00:00
new CommandLineArgument ( "use-ssl" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionUseSSLShort , Strings . FTPBackend . DescriptionUseSSLLong ),
2012-01-19 19:04:56 +00:00
new CommandLineArgument ( "disable-upload-verify" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionDisableUploadVerifyShort , Strings . FTPBackend . DescriptionDisableUploadVerifyLong ),
2009-03-21 12:46:54 +00:00
});
}
}
public string Description
{
get
{
2009-06-14 21:51:06 +00:00
return Strings . FTPBackend . Description ;
2009-03-21 12:46:54 +00:00
}
}
2013-05-05 17:07:24 +02:00
public void Test ()
{
List ();
}
public void CreateFolder ()
{
System . Net . FtpWebRequest req = CreateRequest ( "" , true );
req . Method = System . Net . WebRequestMethods . Ftp . MakeDirectory ;
req . KeepAlive = false ;
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
using ( areq . GetResponse ())
{ }
}
2009-01-14 17:29:30 +00:00
#endregion
2009-01-25 19:33:36 +00:00
#region IDisposable Members
public void Dispose ()
{
if ( m_userInfo != null )
m_userInfo = null ;
}
#endregion
2012-01-19 19:04:56 +00:00
2009-01-14 17:29:30 +00:00
private System . Net . FtpWebRequest CreateRequest ( string remotename )
{
2012-01-19 19:04:56 +00:00
return CreateRequest ( remotename , false );
}
private System . Net . FtpWebRequest CreateRequest ( string remotename , bool createFolder )
{
string url = m_url ;
if ( createFolder && url . EndsWith ( "/" ))
url = url . Substring ( 0 , url . Length - 1 );
System . Net . FtpWebRequest req = ( System . Net . FtpWebRequest ) System . Net . FtpWebRequest . Create ( url + remotename );
2009-03-21 12:46:54 +00:00
2009-03-28 22:33:44 +00:00
if ( m_userInfo != null )
req . Credentials = m_userInfo ;
2009-03-11 20:56:01 +00:00
req . KeepAlive = false ;
2009-01-15 12:57:52 +00:00
2011-09-27 06:59:17 +00:00
if (! m_defaultPassive )
req . UsePassive = m_passive ;
2009-01-15 12:57:52 +00:00
2010-02-10 20:44:48 +00:00
if ( m_useSSL )
req . EnableSsl = m_useSSL ;
2009-01-14 17:29:30 +00:00
return req ;
}
2010-05-27 19:37:14 +00:00
/// <summary>
/// Private helper class to fix a bug with the StreamReader
/// </summary>
2010-11-21 11:08:15 +00:00
private class StreamReadHelper : Utility . OverrideableStream
2010-05-27 19:37:14 +00:00
{
/// <summary>
/// Once the stream has returned 0 as the read count it is disposed
/// in the FtpRequest, and subsequent read requests will throw an ObjectDisposedException
/// </summary>
private bool m_empty = false ;
/// <summary>
/// Basic initialization, just pass the stream to the super class
/// </summary>
/// <param name="stream"></param>
public StreamReadHelper ( System . IO . Stream stream )
: base ( stream )
{
}
/// <summary>
/// Override the read function to make sure that we only return less than the requested amount of data if the stream is exhausted
/// </summary>
/// <param name="buffer">The buffer to place data in</param>
/// <param name="offset">The offset into the buffer to start at</param>
/// <param name="count">The number of bytes to read</param>
/// <returns>The number of bytes read</returns>
public override int Read ( byte [] buffer , int offset , int count )
{
int readCount = 0 ;
int a ;
while (! m_empty && count > 0 )
{
a = base . Read ( buffer , offset , count );
readCount += a ;
count -= a ;
offset += a ;
m_empty = a == 0 ;
}
return readCount ;
}
}
2009-01-14 17:29:30 +00:00
}
}