//******************************************************************************************************
// FtpDirectory.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 05/22/2003 - J. Ritchie Carroll
// Generated original version of source code.
// 08/06/2009 - Josh L. Patterson
// Edited Comments.
// 09/14/2009 - Stephen C. Wills
// Added new header and license agreement.
// 11/13/2003 - Pinal C. Patel
// Fixed issue in initialization of Name, FullPath and Parent properties.
// Fixed null-reference exception occurrences in comparison methods/overloads.
// 12/17/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
#region [ Contributor License Agreements ]
//*******************************************************************************************************
//
// Code based on the following project:
// http://www.codeproject.com/KB/IP/net_ftp_upload.aspx
//
// Copyright Alex Kwok & Uwe Keim
//
// The Code Project Open License (CPOL):
// http://www.codeproject.com/info/cpol10.aspx
//
//*******************************************************************************************************
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace GSF.Net.Ftp
{
///
/// Represents a FTP directory.
///
public class FtpDirectory : IFtpFile, IComparable
{
#region [ Members ]
// Nested Types
internal class ItemInfo
{
public string Name;
public string FullPath;
public string Permission;
public bool IsDirectory;
public long Size;
public FtpTimeStampParser TimeStamp = new FtpTimeStampParser();
}
// Events
///
/// Raised when new directory line is scanned.
///
[SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")]
public event Action DirectoryListLineScan;
///
/// Raised when there is an exception scanning a directory.
///
[SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")]
public event Action DirectoryScanException;
// Fields
private FtpDirectory m_parent;
private Dictionary m_subDirectories;
private Dictionary m_files;
private bool m_caseInsensitive;
#endregion
#region [ Constructors ]
internal FtpDirectory(FtpSessionConnected s, bool caseInsensitive, string fullPath)
{
Session = s;
m_parent = null;
m_caseInsensitive = caseInsensitive;
if (fullPath.Substring(fullPath.Length - 1, 1) == "/")
fullPath = fullPath.Substring(0, fullPath.Length - 1);
if (fullPath.Length == 0)
{
Name = "";
FullPath = "/";
}
else
{
string[] directories = fullPath.Split('/');
Name = directories[directories.Length - 1];
FullPath = $"{fullPath}/";
}
}
internal FtpDirectory(FtpSessionConnected s, FtpDirectory parent, bool caseInsensitive, ItemInfo info)
{
Session = s;
m_parent = parent;
m_caseInsensitive = caseInsensitive;
if (info.Name.Length > 0)
{
Name = info.Name;
FullPath = parent == null ? $"{Name}/" : $"{parent.FullPath}{Name}/";
}
else
{
Name = "";
FullPath = "/";
}
Size = info.Size;
Permission = info.Permission;
Timestamp = info.TimeStamp.Value;
}
#endregion
#region [ Properties ]
///
/// Gets or sets FTP case sensitivity of file and directory names.
///
///
/// Set to true to not be case sensitive with FTP file and directory names.
///
public bool CaseInsensitive
{
get => m_caseInsensitive;
set
{
m_caseInsensitive = value;
Refresh();
}
}
///
/// Name of directory.
///
public string Name { get; }
///
/// Full path of directory.
///
public string FullPath { get; }
///
/// Returns false for directory entries.
///
public bool IsFile => false;
///
/// Returns true for directory entries.
///
public bool IsDirectory => true;
///
/// Gets or sets size of directory.
///
public long Size { get; set; }
///
/// Gets or sets permission of directory.
///
public string Permission { get; set; }
///
/// Gets or sets timestamp of directory.
///
public DateTime Timestamp { get; set; }
///
/// Gets parent directory of directory.
///
public FtpDirectory Parent
{
get
{
if (string.Compare(FullPath, Session.RootDirectory.FullPath, m_caseInsensitive) == 0)
return null;
if (m_parent != null)
return m_parent;
// If we don't have a reference to parent directory, we try to derive it...
CheckSessionCurrentDirectory();
StringBuilder parentPath = new StringBuilder();
string fullPath = Session.ControlChannel.PWD();
if (fullPath.Substring(fullPath.Length - 1, 1) != "/")
fullPath += "/";
string[] paths = fullPath.Split('/');
int i;
for (i = 0; i < paths.Length - 2; i++)
{
if (paths[i].Length == 0)
{
parentPath.Append("/");
}
else
{
parentPath.Append(paths[i]);
parentPath.Append("/");
}
}
FtpDirectory parentDir = new FtpDirectory(Session, m_caseInsensitive, parentPath.ToString());
if (string.Compare(parentDir.FullPath, Session.RootDirectory.FullPath, m_caseInsensitive) == 0)
m_parent = Session.RootDirectory;
else
m_parent = parentDir;
return m_parent;
}
}
///
/// Gets sub directories of directory.
///
public Dictionary.ValueCollection SubDirectories
{
get
{
InitHashtable();
return m_subDirectories.Values;
}
}
///
/// Gets files of directory.
///
public Dictionary.ValueCollection Files
{
get
{
InitHashtable();
return m_files.Values;
}
}
internal FtpSessionConnected Session { get; }
#endregion
#region [ Methods ]
///
/// Finds matching file name in directory.
///
/// FileName to find in directory.
/// File reference, if found, otherwise null if file is not found.
public FtpFile FindFile(string fileName)
{
InitHashtable();
return m_files.TryGetValue(fileName, out FtpFile file) ? file : null;
}
///
/// Finds matching subdirectory name in directory.
///
/// Subdirectory name to find in directory.
/// Subdirectory reference, if found, otherwise null if subdirectory is not found.
public FtpDirectory FindSubDirectory(string dirName)
{
InitHashtable();
return m_subDirectories.TryGetValue(dirName, out FtpDirectory directory) ? directory : null;
}
///
/// Uploads local file to directory.
///
/// Local file to upload.
public void PutFile(string localFile)
{
PutFile(localFile, null);
}
///
/// Uploads local file to directory using alternate name.
///
/// Local file to upload.
/// Remote filename to use for upload.
public void PutFile(string localFile, string remoteFile)
{
CheckSessionCurrentDirectory();
FileInfo fi = new FileInfo(localFile);
if (remoteFile == null)
remoteFile = fi.Name;
FtpFileTransferer transfer = new FtpFileTransferer(this, localFile, remoteFile, fi.Length, TransferDirection.Upload);
transfer.StartTransfer();
}
///
/// Downloads remote file from directory.
///
/// Remote filename to download.
public void GetFile(string remoteFile)
{
GetFile(remoteFile, remoteFile);
}
///
/// Downloads remote file from directory using alternate local filename.
///
/// Local filename to use for download.
/// Remote filename to download.
public void GetFile(string localFile, string remoteFile)
{
InitHashtable();
if (m_files.TryGetValue(remoteFile, out FtpFile file))
{
FtpFileTransferer transfer = new FtpFileTransferer(this, localFile, remoteFile, file.Size, TransferDirection.Download);
transfer.StartTransfer();
}
else
{
throw new FtpFileNotFoundException(remoteFile);
}
}
///
/// Starts asynchronous local file upload to directory.
///
/// Local file to upload.
public void BeginPutFile(string localFile)
{
BeginPutFile(localFile, null);
}
///
/// Starts asynchronous local file upload to directory using alternate name.
///
/// Local file to upload.
/// Remote filename to use for upload.
public void BeginPutFile(string localFile, string remoteFile)
{
CheckSessionCurrentDirectory();
FileInfo fi = new FileInfo(localFile);
if (remoteFile == null)
remoteFile = fi.Name;
FtpFileTransferer transfer = new FtpFileTransferer(this, localFile, remoteFile, fi.Length, TransferDirection.Upload);
transfer.StartAsyncTransfer();
}
///
/// Starts asynchronous remote file download from directory.
///
/// Remote filename to download.
public void BeginGetFile(string remoteFile)
{
BeginGetFile(remoteFile, remoteFile);
}
///
/// Starts asynchronous remote file download from directory using alternate local filename.
///
/// Local filename to use for download.
/// Remote filename to download.
public void BeginGetFile(string localFile, string remoteFile)
{
InitHashtable();
if (m_files.TryGetValue(remoteFile, out FtpFile file))
{
FtpFileTransferer transfer = new FtpFileTransferer(this, localFile, remoteFile, file.Size, TransferDirection.Download);
transfer.StartAsyncTransfer();
}
else
{
throw new FtpFileNotFoundException(remoteFile);
}
}
///
/// Removes file from directory.
///
/// Remote filename to remove.
public void RemoveFile(string fileName)
{
CheckSessionCurrentDirectory();
Session.ControlChannel.DELE(fileName);
m_files.Remove(fileName);
}
///
/// Removes subdirectory from directory.
///
/// Subdirectory name to remove.
public void RemoveSubDir(string dirName)
{
CheckSessionCurrentDirectory();
Session.ControlChannel.RMD(dirName);
m_subDirectories.Remove(dirName);
}
///
/// Creates a new zero-length remote file in directory.
///
/// New remote file name.
/// File reference to new zero-length remote file.
public FtpFile CreateFile(string newFileName)
{
FtpDataStream stream = CreateFileStream(newFileName);
stream.Close();
return m_files[newFileName];
}
///
/// Creates a new data stream for remote file in directory.
///
/// New remote file name.
/// Output data stream for new remote file.
public FtpOutputDataStream CreateFileStream(string newFileName)
{
InitHashtable();
FtpDataStream stream = Session.ControlChannel.GetDataStream(TransferDirection.Upload);
try
{
Session.ControlChannel.STOR(newFileName);
FtpFile newFile = new FtpFile(this, newFileName);
m_files[newFileName] = newFile;
return (FtpOutputDataStream)stream;
}
catch
{
stream.Close();
throw;
}
}
///
/// Refreshes directory listing.
///
public void Refresh()
{
ClearItems();
InitHashtable();
}
internal void ClearItems()
{
m_subDirectories = null;
m_files = null;
}
internal void CheckSessionCurrentDirectory()
{
if (Session.CurrentDirectory.FullPath != FullPath)
throw new InvalidOperationException($"{FullPath} is not current directory.");
}
private void LoadDirectoryItems()
{
if (Session.CurrentDirectory != this)
throw new InvalidOperationException($"{Name} is not current active directory");
Queue lineQueue = Session.ControlChannel.List(false);
foreach (string line in lineQueue)
{
// We allow users to inspect FTP lineQueue if desired...
DirectoryListLineScan?.Invoke(line);
try
{
ItemInfo info = new ItemInfo();
if (ParseListLine(line, info))
{
if (info.IsDirectory)
m_subDirectories.Add(info.Name, new FtpDirectory(Session, this, m_caseInsensitive, info));
else
m_files.Add(info.Name, new FtpFile(this, info));
}
}
catch (FtpExceptionBase ex)
{
DirectoryScanException?.Invoke(ex);
}
}
}
private void InitHashtable()
{
CheckSessionCurrentDirectory();
if (m_subDirectories != null && m_files != null)
return;
if (m_subDirectories == null)
{
if (m_caseInsensitive)
m_subDirectories = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
else
m_subDirectories = new Dictionary(StringComparer.CurrentCulture);
}
if (m_files == null)
{
if (m_caseInsensitive)
m_files = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
else
m_files = new Dictionary(StringComparer.CurrentCulture);
}
LoadDirectoryItems();
}
private bool ParseListLine(string line, ItemInfo info)
{
Match m = MatchingListLine(line, out info.TimeStamp.Style);
if (m == null)
return false;
info.Name = new string(m.Groups["name"].Value
.TakeWhile(c => c != '\0')
.ToArray());
info.FullPath = FullPath + info.Name;
string dir = m.Groups["dir"].Value;
if (dir.Length > 0 && dir != "-")
{
info.IsDirectory = true;
info.FullPath += "/";
}
else
{
info.Size = long.Parse(m.Groups["size"].Value);
}
info.Permission = m.Groups["permission"].Value;
info.TimeStamp.RawValue = m.Groups["timestamp"].Value;
return true;
}
private Match MatchingListLine(string line, out FtpTimeStampParser.RawDataStyle tsStyle)
{
Match m = s_unixListLineStyle1.Match(line);
if (m.Success)
{
tsStyle = FtpTimeStampParser.RawDataStyle.UnixDate;
return m;
}
m = s_unixListLineStyle3.Match(line);
if (m.Success)
{
tsStyle = FtpTimeStampParser.RawDataStyle.UnixDateTime;
return m;
}
m = s_unixListLineStyle2.Match(line);
if (m.Success)
{
tsStyle = FtpTimeStampParser.RawDataStyle.UnixDate;
return m;
}
m = s_dosListLineStyle1.Match(line);
if (m.Success)
{
tsStyle = FtpTimeStampParser.RawDataStyle.DosDateTime;
return m;
}
m = s_dosListLineStyle2.Match(line);
if (m.Success)
{
tsStyle = FtpTimeStampParser.RawDataStyle.UnixDateTime;
return m;
}
tsStyle = FtpTimeStampParser.RawDataStyle.Undetermined;
return null;
}
///
/// Determines if the two objects are equal.
///
/// Other object to compare.
/// true if both objects are equal.
public override bool Equals(object obj)
{
FtpDirectory other = obj as FtpDirectory;
if (other != null)
return CompareTo(other) == 0;
return false;
}
///
/// Generates hash code for this .
///
/// An value as the result.
public override int GetHashCode()
{
return Name.GetHashCode();
}
int IComparable.CompareTo(FtpDirectory other)
{
// Directories are sorted by name
return string.Compare(Name, other.Name, m_caseInsensitive);
}
int IComparable.CompareTo(IFtpFile other)
{
// Directories are sorted by name
return string.Compare(Name, other.Name, m_caseInsensitive);
}
///
/// Compares directory or file to another.
///
/// An to compare against.
/// An value representing the result. 1 - obj is greater than, 0 - obj is equal to, -1 - obj is less than.
public int CompareTo(object obj)
{
if (!(obj is IFtpFile file))
return 1;
return ((IComparable)this).CompareTo(file);
}
#endregion
#region [ Operators ]
///
/// Compares the two values for equality.
///
/// A left hand operand.
/// A right hand operand.
/// A value indicating the result.
public static bool operator ==(FtpDirectory value1, FtpDirectory value2)
{
if (Equals(value1, null))
return Equals(value2, null);
return value1.CompareTo(value2) == 0;
}
///
/// Compares the two values for inequality.
///
/// A left hand operand.
/// A right hand operand.
/// A value indicating the result.
public static bool operator !=(FtpDirectory value1, FtpDirectory value2)
{
return !(value1 == value2);
}
///
/// Returns true if left value is less than right value.
///
/// A left hand operand.
/// A right hand operand.
/// A value indicating the result.
public static bool operator <(FtpDirectory value1, FtpDirectory value2)
{
if (Equals(value1, null))
return Equals(value2, null);
return value1.CompareTo(value2) < 0;
}
///
/// Returns true if left value is greater than right value.
///
/// A left hand operand.
/// A right hand operand.
/// A value indicating the result.
public static bool operator >(FtpDirectory value1, FtpDirectory value2)
{
return !(value1 <= value2);
}
///
/// Returns true if left value is less or equal to than right value.
///
/// A left hand operand.
/// A right hand operand.
/// A value indicating the result.
public static bool operator <=(FtpDirectory value1, FtpDirectory value2)
{
if (Equals(value1, null))
return Equals(value2, null);
return value1.CompareTo(value2) <= 0;
}
///
/// Returns true if left value is greater than or equal to right value.
///
/// A left hand operand.
/// A right hand operand.
/// A value indicating the result.
public static bool operator >=(FtpDirectory value1, FtpDirectory value2)
{
return !(value1 < value2);
}
#endregion
#region [ Static ]
// Static Fields
private static readonly Regex s_unixListLineStyle1 = new Regex("(?[\\-d])(?([\\-r][\\-w][\\-xs]){3})\\s+\\d+\\s+\\w+\\s+\\w+\\s+(?\\d+)\\s+(?\\w+\\s+\\d+\\s+\\d{4})\\s+(?.+)");
private static readonly Regex s_unixListLineStyle2 = new Regex("(?[\\-d])(?([\\-r][\\-w][\\-xs]){3})\\s+\\d+\\s+\\d+\\s+(?\\d+)\\s+(?\\w+\\s+\\d+\\s+\\d{4})\\s+(?.+)");
private static readonly Regex s_unixListLineStyle3 = new Regex("(?[\\-d])(?([\\-r][\\-w][\\-xs]){3})\\s+\\d+\\s+\\d+\\s+(?\\d+)\\s+(?\\w+\\s+\\d+\\s+\\d+:\\d+)\\s+(?.+)");
private static readonly Regex s_dosListLineStyle1 = new Regex("(?\\d{2}\\-\\d{2}\\-\\d{2}\\s+\\d{2}:\\d{2}[Aa|Pp][mM])\\s+(?\\<\\w+\\>){0,1}(?\\d+){0,1}\\s+(?.+)"); // IIS FTP Service
private static readonly Regex s_dosListLineStyle2 = new Regex("(?[\\-d])(?([\\-r][\\-w][\\-xs]){3})\\s+\\d+\\s+\\w+\\s+\\w+\\s+(?\\d+)\\s+(?\\w+\\s+\\d+\\s+\\d+:\\d+)\\s+(?.+)"); // IIS FTP Service in Unix Mode
#endregion
}
}