//****************************************************************************************************** // 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 } }