diff --git a/VirtualFS/Base/BaseFileSystem.cs b/VirtualFS/Base/BaseFileSystem.cs index f560821..fff498e 100644 --- a/VirtualFS/Base/BaseFileSystem.cs +++ b/VirtualFS/Base/BaseFileSystem.cs @@ -49,7 +49,7 @@ namespace VirtualFS.Base public BaseFileSystem(bool readOnly) { IsReadOnly = readOnly; - HighPriority = false; + Priority = FileSystemMountPriority.Normal; } /// Gets reference to root file system. @@ -57,8 +57,8 @@ namespace VirtualFS.Base /// Gets or sets a value indicating whether this file system /// has higher priority than others mounted on the same path. - /// This value must be set corectly before mount. - public virtual bool HighPriority { get; set; } + /// This value must be set correctly before mount. + public virtual FileSystemMountPriority Priority { get; set; } /// /// Gets a value indicating whether this file system is read only. @@ -82,25 +82,42 @@ namespace VirtualFS.Base } } - /// - /// Check if given path exists. - /// + /// Gets or sets name of file system. + public virtual string Name { get; set; } + + /// Check if given path exists. /// Path to check. - /// - /// Returns true if entry does - /// exist, otherwise false. - /// + /// Returns true if entry does + /// exist, otherwise false. public abstract bool Exists(Path path); - /// - /// Get entry located under given path. - /// + /// Get entry located under given path. /// Path to get. - /// - /// Returns entry information. - /// + /// Returns entry information. public abstract Entry GetEntry(Path path); + /// Get directory located under given path. + /// The path. + /// Returns directory entry information. + public Directory GetDirectory(Path path) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't get directory from file path '{0}'", path)); + + return GetEntry(path) as Directory; + } + + /// Get file located under given path. + /// Path to get. + /// Returns file entry information. + public File GetFile(Path path) + { + if (path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't get file from directory path '{0}'", path)); + + return GetEntry(path) as File; + } + /// Get path to physical file containing entry. /// Virtual file system path. /// Real file system path or null if real @@ -156,7 +173,7 @@ namespace VirtualFS.Base if (source == destination) return; - if (!destination.IsDirectory) + if (source.IsDirectory && !destination.IsDirectory) throw new InvalidOperationException(string.Format("Can't copy to non directory path ('{0}').", destination)); if (!source.IsDirectory) @@ -209,7 +226,7 @@ namespace VirtualFS.Base private void InternalCopySingleFile(Path source, Path destination, bool overwrite, Action copySuccess) { - var dst = destination.Append(source.Name); + var dst = destination.IsDirectory ? destination.Append(source.Name) : destination; if (!Exists(source)) throw new InvalidOperationException("Source file doesn't exist."); @@ -217,17 +234,17 @@ namespace VirtualFS.Base if (!overwrite && Exists(dst)) throw new InvalidOperationException("Destination file already exist."); - Directory dir = (Directory)GetEntry(destination); + Directory dir = (Directory)GetEntry(dst.Parent); if (dir.IsReadOnly || dir.FileSystem.IsReadOnly) throw new InvalidOperationException("Can't copy on read only file system."); dir - .FileCreate(source.Name) + .FileCreate(dst.Name) .WriteAllBytes(((File)GetEntry(source)).ReadAllBytes()); if (copySuccess != null) - copySuccess(source, destination.Append(source.Name)); + copySuccess(source, dst); } /// Change name of the entry under specified path. @@ -255,6 +272,36 @@ namespace VirtualFS.Base /// public abstract IEnumerable GetEntries(Path path, Regex mask = null); + /// + /// Get directories located under given path. + /// + /// Path to get. + /// Mask to filter out unwanted entries. + /// + /// Returns entry information. + /// + public virtual IEnumerable GetDirectories(Path path, Regex mask = null) + { + return GetEntries(path, mask) + .Where(e => e is Directory) + .Cast(); + } + + /// + /// Get files located under given path. + /// + /// Path to get. + /// Mask to filter out unwanted entries. + /// + /// Returns entry information. + /// + public virtual IEnumerable GetFiles(Path path, Regex mask = null) + { + return GetEntries(path, mask) + .Where(e => e is File) + .Cast(); + } + /// /// Create Directory and return new entry. /// @@ -305,43 +352,11 @@ namespace VirtualFS.Base throw new InvalidOperationException("Can't delete directory that contains entries."); } - /// Opens an existing file for reading. - /// The file to be opened for reading. - /// A read-only on the specified path. - public virtual Stream FileOpenRead(Path path) - { - if (path.IsDirectory) - throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); - - if (!Exists(path)) - throw new InvalidOperationException("Can't open not existing file."); - - return null; - } - - /// Opens an existing file for writing. - /// The file to be opened for writing. - /// An unshared object on - /// the specified path with write access. - public virtual Stream FileOpenWrite(Path path) - { - if (path.IsDirectory) - throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); - - if (IsReadOnly) - throw new InvalidOperationException(string.Format("Can't write file ('{0}') on read only file system.", path)); - - if (!Exists(path)) - throw new InvalidOperationException("Can't open not existing file."); - - return null; - } - - /// Creates or overwrites a file in the specified path. + /// Creates empty file on file system and returns it's description. /// The path and name of the file to create. - /// A that provides - /// write access to the file specified in path. - public virtual Stream FileCreate(Path path) + /// If file exists replace it. + /// File description. + public virtual File FileTouch(Path path, bool overwrite = false) { if (path.IsDirectory) throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); @@ -352,6 +367,74 @@ namespace VirtualFS.Base if (!Exists(path.Parent)) throw new InvalidOperationException("Can't create file in un existent directory path."); + if (Exists(path)) + { + if (overwrite) + FileDelete(path); + else + throw new InvalidOperationException("Can't create file that already exist."); + } + + return null; + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + public virtual Stream FileOpenRead(Path path) + { + if (path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); + + if (!Exists(path)) + throw new InvalidOperationException(String.Format("Can't open not existing file '{0}'. ", path)); + + return null; + } + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// Append file. + /// An unshared object on + /// the specified path with write access. + public virtual Stream FileOpenWrite(Path path, bool append = false) + { + if (path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); + + if (IsReadOnly) + throw new InvalidOperationException(string.Format("Can't write file ('{0}') on read only file system.", path)); + + if (!Exists(path)) + throw new InvalidOperationException(String.Format("Can't open not existing file '{0}'. ", path)); + + return null; + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// If file exists replace it. + /// A that provides + /// write access to the file specified in path. + public virtual Stream FileCreate(Path path, bool overwrite = false) + { + if (path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); + + if (IsReadOnly) + throw new InvalidOperationException(string.Format("Can't create file ('{0}') on read only file system.", path)); + + if (!Exists(path.Parent)) + throw new InvalidOperationException("Can't create file in un existent directory path."); + + if (Exists(path)) + { + if (overwrite) + FileDelete(path); + else + throw new InvalidOperationException("Can't create file that already exist."); + } + return null; } diff --git a/VirtualFS/Compressed/ZipReadFileSystem.cs b/VirtualFS/Compressed/ZipReadFileSystem.cs index 7fc7b08..9b1dd8e 100644 --- a/VirtualFS/Compressed/ZipReadFileSystem.cs +++ b/VirtualFS/Compressed/ZipReadFileSystem.cs @@ -43,7 +43,7 @@ namespace VirtualFS.Compressed /// Gets the Zip file. public ZipFile File { get; private set; } - private Dictionary _pathToEntry; + private Dictionary _pathToEntry = new Dictionary(); /// Gets the assembly on which file system operates. public Assembly Assembly { get; private set; } @@ -55,6 +55,12 @@ namespace VirtualFS.Compressed public ZipReadFileSystem(System.IO.Stream stream, Path root = null, Regex filter = null) : base(true) { + if (stream == null) + throw new ArgumentNullException("stream"); + + if (!stream.CanSeek) + throw new InvalidOperationException("Zip steram must be capable of seeking."); + File = new ZipFile(stream); foreach (ZipEntry zipEntry in File) @@ -91,6 +97,7 @@ namespace VirtualFS.Compressed CreationTime = zipEntry.DateTime, LastAccessTime = zipEntry.DateTime, LastWriteTime = zipEntry.DateTime, + Size = zipEntry.Size, }); } } @@ -208,7 +215,7 @@ namespace VirtualFS.Compressed { base.FileOpenRead(path); - return new VirtualStream(this, File.GetInputStream((ZipEntry)_pathToEntry[path].Data)); + return new VirtualStream(this, File.GetInputStream((ZipEntry)_pathToEntry[path].Data), path); } /// Performs application-defined tasks associated with diff --git a/VirtualFS/Directory.cs b/VirtualFS/Directory.cs index f59ef4a..f1d77f6 100644 --- a/VirtualFS/Directory.cs +++ b/VirtualFS/Directory.cs @@ -49,7 +49,7 @@ namespace VirtualFS /// Force usage of local file /// system, to which directory is bound. /// Enumeration of entries in this directory. - public IEnumerable GetEntries(Regex mask = null, bool forceLocal = false) + public virtual IEnumerable GetEntries(Regex mask = null, bool forceLocal = false) { if (FileSystem == null) throw new InvalidOperationException("This entry has no relation with any file system."); @@ -60,12 +60,119 @@ namespace VirtualFS FileSystem.GetEntries(Path, mask); } + /// Enumerate directories in this directory. + /// Mask to filter out unwanted entries. + /// Force usage of local file + /// system, to which directory is bound. + /// Enumeration of entries in this directory. + public virtual IEnumerable GetDirectories(Regex mask = null, bool forceLocal = false) + { + if (FileSystem == null) + throw new InvalidOperationException("This entry has no relation with any file system."); + + // If file system is rooted, check root file system + return !forceLocal && FileSystem.RootFileSystem != null ? + FileSystem.RootFileSystem.GetDirectories(FullPath, mask) : + FileSystem.GetDirectories(Path, mask); + } + + /// Enumerate files in this directory. + /// Mask to filter out unwanted entries. + /// Force usage of local file + /// system, to which directory is bound. + /// Enumeration of entries in this directory. + public virtual IEnumerable GetFiles(Regex mask = null, bool forceLocal = false) + { + if (FileSystem == null) + throw new InvalidOperationException("This entry has no relation with any file system."); + + // If file system is rooted, check root file system + return !forceLocal && FileSystem.RootFileSystem != null ? + FileSystem.RootFileSystem.GetFiles(FullPath, mask) : + FileSystem.GetFiles(Path, mask); + } + + /// Check if entry with given name exists. + /// Name to check. + /// If set to true force usage of local file system. + /// Returns true if entry does exist, otherwise false. + /// This entry has no relation with any file system. + public virtual bool Exists(string name, bool forceLocal = false) + { + if (FileSystem == null) + throw new InvalidOperationException("This entry has no relation with any file system."); + + // If file system is rooted, check root file system + return !forceLocal && FileSystem.RootFileSystem != null ? + FileSystem.RootFileSystem.Exists(FullPath + name) : + FileSystem.Exists(Path + name); + } + + /// Get entry located under given path. + /// Name of entry to get. + /// If set to true force usage of local file system. + /// Returns entry information. + public virtual Entry GetEntry(string name, bool forceLocal = false) + { + if (FileSystem == null) + throw new InvalidOperationException("This entry has no relation with any file system."); + + // If file system is rooted, check root file system + return !forceLocal && FileSystem.RootFileSystem != null ? + FileSystem.RootFileSystem.GetEntry(FullPath + name) : + FileSystem.GetEntry(Path + name); + } + + /// Get directory located under given path. + /// Name of file to get. + /// If set to true force usage of local file system. + /// Returns directory entry information. + public virtual Directory GetDirectory(string name, bool forceLocal = false) + { + Path path = Path + name; + + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't get directory from file path '{0}'", path)); + + return GetEntry(name, forceLocal) as Directory; + } + + /// Get file located under given path. + /// Name of file to get. + /// If set to true force usage of local file system. + /// Returns file entry information. + public virtual File GetFile(string name, bool forceLocal = false) + { + Path path = Path + name; + + if (path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't get file from directory path '{0}'", path)); + + return GetEntry(name, forceLocal) as File; + } + + /// Get entry in this directory. + /// Entry name. + /// Force usage of local file + /// system, to which directory is bound. + /// Enumeration of entries in this directory. + public virtual Entry GetEntries(string name, bool forceLocal = false) + { + if (FileSystem == null) + throw new InvalidOperationException("This entry has no relation with any file system."); + + // If file system is rooted, check root file system + return !forceLocal && FileSystem.RootFileSystem != null ? + FileSystem.RootFileSystem.GetEntry(FullPath + name) : + FileSystem.GetEntry(Path + name); + } + /// Create Directory and return new entry. /// Name of directory. /// Force usage of local file /// system, to which directory is bound. /// Created entry. - public Directory Create(string name, bool forceLocal = false) + public virtual Directory Create(string name, bool forceLocal = false) { return !forceLocal && FileSystem.RootFileSystem != null ? FileSystem.RootFileSystem.DirectoryCreate(FullPath.Append(string.Format("{0}{1}", @@ -82,7 +189,7 @@ namespace VirtualFS /// subdirectories, and files in path; otherwise false. /// Force usage of local file /// system, to which directory is bound. - public void Delete(bool recursive = false, bool forceLocal = false) + public virtual void Delete(bool recursive = false, bool forceLocal = false) { if (!forceLocal && FileSystem.RootFileSystem != null) FileSystem.RootFileSystem.DirectoryDelete(FullPath, recursive); @@ -96,7 +203,7 @@ namespace VirtualFS /// system, to which directory is bound. /// Created file is empty. /// The entry information. - public File FileCreate(string name, bool forceLocal = false) + public virtual File FileCreate(string name, bool forceLocal = false) { if (!forceLocal && FileSystem.RootFileSystem != null) { @@ -113,7 +220,7 @@ namespace VirtualFS var path = Path.Append( name.Validated("name", invalidChars: Path.InvalidFileChars, invalidSeq: Path.InvalidPathSequences)); - using (var stream = FileSystem.FileCreate(path)) + using (var stream = FileSystem.FileCreate(path, true)) stream.Close(); return FileSystem.GetEntry(path) as File; diff --git a/VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs b/VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs index 0b75345..4226b96 100644 --- a/VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs +++ b/VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs @@ -55,7 +55,7 @@ namespace VirtualFS.EmbeddedResource { Assembly = assembly; - var fi = new System.IO.FileInfo(Assembly.Location); + var fi = new System.IO.FileInfo(Uri.UnescapeDataString(new UriBuilder(Assembly.CodeBase).Path)); _pathToResource = Assembly.GetManifestResourceNames() .Where(x => (string.IsNullOrEmpty(rootNamespace) || x.StartsWith(rootNamespace)) && (filter == null || filter.IsMatch(x))) @@ -68,6 +68,7 @@ namespace VirtualFS.EmbeddedResource CreationTime = fi.CreationTimeUtc, LastAccessTime = fi.LastAccessTime, LastWriteTime = fi.LastWriteTime, + Size = 0, }) .ToDictionary( k => k.Path, @@ -142,7 +143,7 @@ namespace VirtualFS.EmbeddedResource if (!ret && path.IsDirectory && _pathToResource.Keys.Any(x => x.IsParentOf(path))) { - var fi = new System.IO.FileInfo(Assembly.Location); + var fi = new System.IO.FileInfo(Uri.UnescapeDataString(new UriBuilder(Assembly.CodeBase).Path)); _pathToResource.Add(path, new Directory() { @@ -181,7 +182,7 @@ namespace VirtualFS.EmbeddedResource /// should return null if real path can't be determined. public override string EntryRealPath(Path path) { - return this.Assembly.Location; + return Uri.UnescapeDataString(new UriBuilder(Assembly.CodeBase).Path); } /// Get entries located under given path. @@ -209,7 +210,7 @@ namespace VirtualFS.EmbeddedResource { base.FileOpenRead(path); - return new VirtualStream(this, Assembly.GetManifestResourceStream(_pathToResource[path].Data.ToString())); + return new VirtualStream(this, Assembly.GetManifestResourceStream(_pathToResource[path].Data.ToString()), path); } } } \ No newline at end of file diff --git a/VirtualFS/Entry.cs b/VirtualFS/Entry.cs index 482ade2..50f6bd5 100644 --- a/VirtualFS/Entry.cs +++ b/VirtualFS/Entry.cs @@ -60,6 +60,21 @@ namespace VirtualFS /// Gets or sets additional internal data for file system entry. internal object Data { get; set; } + /// Updates information about file. + public virtual void UpdateInfo() + { + var up = IsDirectory ? FileSystem.GetDirectory(Path) : FileSystem.GetEntry(Path); + + if (up != null) + { + IsReadOnly = up.IsReadOnly; + IsDirectory = up.IsDirectory; + CreationTime = up.CreationTime; + LastAccessTime = up.LastAccessTime; + LastWriteTime = up.LastWriteTime; + } + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/VirtualFS/Extensions/Validators.cs b/VirtualFS/Extensions/Validators.cs index 16b352e..d64df88 100644 --- a/VirtualFS/Extensions/Validators.cs +++ b/VirtualFS/Extensions/Validators.cs @@ -37,8 +37,8 @@ namespace VirtualFS.Extensions /// /// The source string. /// A description of the source string to build errors and exceptions if needed. - /// True if the returned string can be null. - /// True if the returned string can be empty. + /// True if the returned string can be null. + /// True if the returned string can be empty. /// True to trim the returned string. /// True to left-trim the returned string. /// True to right-trim the returned string. @@ -53,7 +53,7 @@ namespace VirtualFS.Extensions /// If not null, an array containing the sequences that are not allowed. /// A new validated string. public static string Validated(this string source, string desc = null, - bool canbeNull = false, bool canbeEmpty = false, + bool canBeNull = false, bool canBeEmpty = false, bool trim = true, bool trimStart = false, bool trimEnd = false, int minLen = -1, int maxLen = -1, char padLeft = '\0', char padRight = '\0', char[] invalidChars = null, char[] validChars = null, string[] invalidSeq = null) @@ -64,7 +64,7 @@ namespace VirtualFS.Extensions // Validating if null sources are accepted... if (source == null) { - if (!canbeNull) throw new ArgumentNullException(desc, string.Format("{0} cannot be null.", desc)); + if (!canBeNull) throw new ArgumentNullException(desc, string.Format("{0} cannot be null.", desc)); return null; } @@ -92,7 +92,7 @@ namespace VirtualFS.Extensions // Validating emptyness and lenghts... if (source.Length == 0) { - if (!canbeEmpty) throw new ArgumentException(string.Format("{0} cannot be empty.", desc)); + if (!canBeEmpty) throw new ArgumentException(string.Format("{0} cannot be empty.", desc)); return string.Empty; } diff --git a/VirtualFS/File.cs b/VirtualFS/File.cs index 5f9e23a..34c7624 100644 --- a/VirtualFS/File.cs +++ b/VirtualFS/File.cs @@ -35,6 +35,9 @@ namespace VirtualFS /// File representation. public class File : Entry { + /// Gets file size if it can be determined. + public virtual long Size { get; internal set; } + private static UTF8Encoding _utfNoBOM = new UTF8Encoding(false, true); /// Opens an existing file for reading. @@ -85,22 +88,38 @@ namespace VirtualFS using (Stream fileStream = OpenRead(true)) { int offset = 0; - long length = fileStream.Length; - if (length > 2147483647L) - throw new IOException("Can't read file larger than 2GB."); - - int len = (int)length; - array = new byte[len]; - - while (len > 0) + if (fileStream.CanSeek) { - int read = fileStream.Read(array, offset, len); - if (read == 0) - throw new EndOfStreamException("Can't read beyond file stream."); + long length = fileStream.Length; + if (length > 2147483647L) + throw new IOException("Can't read file larger than 2GB."); - offset += read; - len -= read; + int len = (int)length; + array = new byte[len]; + + while (len > 0) + { + int read = fileStream.Read(array, offset, len); + if (read == 0) + throw new EndOfStreamException("Can't read beyond file stream."); + + offset += read; + len -= read; + } } + else + using (MemoryStream ms = new MemoryStream()) + { + byte[] buf = new byte[4096]; + while (true) + { + int len = fileStream.Read(buf, 0, buf.Length); + if (len == 0) break; + ms.Write(buf, 0, len); + } + + array = ms.ToArray(); + } } return array; @@ -147,7 +166,7 @@ namespace VirtualFS /// The bytes to write to the file. public void WriteAllBytes(byte[] bytes) { - using (Stream fileStream = FileSystem.FileCreate(Path)) + using (Stream fileStream = FileSystem.FileCreate(Path, true)) fileStream.Write(bytes, 0, bytes.Length); } @@ -156,7 +175,7 @@ namespace VirtualFS /// The string array to write to the file. public void WriteAllLines(string[] contents) { - using (Stream fileStream = FileSystem.FileCreate(Path)) + using (Stream fileStream = FileSystem.FileCreate(Path, true)) using (var writer = new StreamWriter(fileStream, _utfNoBOM)) foreach (var item in contents) writer.WriteLine(item); @@ -167,7 +186,7 @@ namespace VirtualFS /// The string to write to the file. public void WriteAllText(string contents) { - using (Stream fileStream = FileSystem.FileCreate(Path)) + using (Stream fileStream = FileSystem.FileCreate(Path, true)) using (var writer = new StreamWriter(fileStream, _utfNoBOM)) writer.Write(contents); } diff --git a/VirtualFS/FileSystemMountPriority.cs b/VirtualFS/FileSystemMountPriority.cs new file mode 100644 index 0000000..3083ace --- /dev/null +++ b/VirtualFS/FileSystemMountPriority.cs @@ -0,0 +1,48 @@ +/* + * VirtualFS - Virtual File System library. + * Copyright (c) 2016, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace VirtualFS +{ + /// Enum containing file system mount priority + public enum FileSystemMountPriority + { + /// Normal mode. + Normal, + + /// Mount as last. + Low, + + /// Mount as first. + High, + } +} diff --git a/VirtualFS/IFileSystem.cs b/VirtualFS/IFileSystem.cs index 5decf55..15a9099 100644 --- a/VirtualFS/IFileSystem.cs +++ b/VirtualFS/IFileSystem.cs @@ -43,8 +43,8 @@ namespace VirtualFS /// Gets or sets a value indicating whether this file system /// has higher priority than others mounted on the same path. - /// This value must be set corectly before mount. - bool HighPriority { get; set; } + /// This value must be set correctly before mount. + FileSystemMountPriority Priority { get; set; } /// Gets a value indicating whether this file system is read only. bool IsReadOnly { get; } @@ -54,6 +54,9 @@ namespace VirtualFS /// file is open in this file system. bool IsBusy { get; } + /// Gets or sets name of file system. + string Name { get; set; } + #region Common /// Check if given path exists. @@ -67,6 +70,16 @@ namespace VirtualFS /// Returns entry information. Entry GetEntry(Path path); + /// Get directory located under given path. + /// Path to get. + /// Returns directory entry information. + Directory GetDirectory(Path path); + + /// Get file located under given path. + /// Path to get. + /// Returns file entry information. + File GetFile(Path path); + /// Get path to physical file containing entry. /// This may not work with all file systems. Implementation /// should return null if real path can't be determined. @@ -107,6 +120,18 @@ namespace VirtualFS /// Returns entry information. IEnumerable GetEntries(Path path, Regex mask = null); + /// Get directories located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + IEnumerable GetDirectories(Path path, Regex mask = null); + + /// Get files located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + IEnumerable GetFiles(Path path, Regex mask = null); + /// Create directory and return new entry. /// Parent directory must exist. /// The directory path to create. @@ -122,6 +147,12 @@ namespace VirtualFS #region File + /// Creates empty file on file system and returns it's description. + /// The path and name of the file to create. + /// If file exists replace it. + /// File description. + File FileTouch(Path path, bool overwrite = false); + /// Opens an existing file for reading. /// The file to be opened for reading. /// A read-only on the specified path. @@ -129,15 +160,17 @@ namespace VirtualFS /// Opens an existing file for writing. /// The file to be opened for writing. + /// Append file. /// An unshared object on /// the specified path with write access. - Stream FileOpenWrite(Path path); + Stream FileOpenWrite(Path path, bool append = false); /// Creates or overwrites a file in the specified path. /// The path and name of the file to create. + /// If file exists replace it. /// A that provides /// write access to the file specified in path. - Stream FileCreate(Path path); + Stream FileCreate(Path path, bool overwrite = false); /// Deletes the specified file. An exception is not thrown /// if the specified file does not exist. diff --git a/VirtualFS/Implementation/RootFileSystem.cs b/VirtualFS/Implementation/RootFileSystem.cs index b0eba6b..abab18a 100644 --- a/VirtualFS/Implementation/RootFileSystem.cs +++ b/VirtualFS/Implementation/RootFileSystem.cs @@ -123,7 +123,7 @@ namespace VirtualFS.Implementation if (pathAndFs == null) throw new InvalidOperationException("File system containing parent directory was not found."); - return pathAndFs.FileSystem.EntryRealPath(path.RemoveParent(parent)); + return pathAndFs.FileSystem.EntryRealPath(path.RemoveParent(pathAndFs.Path)); } /// Copies an existing file or directory to a new location. @@ -215,7 +215,7 @@ namespace VirtualFS.Implementation if (pathAndFs == null) throw new InvalidOperationException("Writable file system containing parent directory was not found."); - pathAndFs.FileSystem.ReName(path.RemoveParent(parent), newName); + pathAndFs.FileSystem.ReName(path.RemoveParent(pathAndFs.Path), newName); } /// Get entries located under given path. @@ -235,6 +235,40 @@ namespace VirtualFS.Implementation .Distinct(); } + /// Get directories located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetDirectories(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.")); + + return _mounts + .Where(x => x.Key.IsParentOfOrSame(path)) + .SelectMany(v => v.Value.Where(x => x.Exists(path.RemoveParent(v.Key))), + (k, v) => v.GetDirectories(path.RemoveParent(k.Key))) + .SelectMany(e => e, (p, e) => e) + .Distinct(); + } + + /// Get files located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetFiles(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.")); + + return _mounts + .Where(x => x.Key.IsParentOfOrSame(path)) + .SelectMany(v => v.Value.Where(x => x.Exists(path.RemoveParent(v.Key))), + (k, v) => v.GetFiles(path.RemoveParent(k.Key), mask)) + .SelectMany(e => e, (p, e) => e) + .Distinct(); + } + /// Create Directory and return new entry. /// Path of directory. /// Created entry. @@ -255,7 +289,7 @@ namespace VirtualFS.Implementation if (pathAndFs == null) throw new InvalidOperationException("Writable file system containing parent directory was not found."); - return pathAndFs.FileSystem.DirectoryCreate(path.RemoveParent(parent)); + return pathAndFs.FileSystem.DirectoryCreate(path.RemoveParent(pathAndFs.Path)); } /// Deletes the specified directory and, if @@ -281,7 +315,7 @@ namespace VirtualFS.Implementation if (pathAndFs == null) throw new InvalidOperationException("Writable file system containing parent directory was not found."); - pathAndFs.FileSystem.DirectoryDelete(path.RemoveParent(parent)); + pathAndFs.FileSystem.DirectoryDelete(path.RemoveParent(pathAndFs.Path)); } /// Opens an existing file for reading. @@ -297,23 +331,24 @@ namespace VirtualFS.Implementation throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); var pathAndFs = _mounts - .Where(x => x.Key.IsParentOfOrSame(parent)) + .Where(x => x.Key.IsParentOfOrSame(path)) .SelectMany(v => v.Value, (k, v) => new { Path = k.Key, FileSystem = v }) - .FirstOrDefault(pfs => pfs.FileSystem.Exists(parent.RemoveParent(pfs.Path))); + .FirstOrDefault(pfs => pfs.FileSystem.Exists(path.RemoveParent(pfs.Path))); if (pathAndFs == null) throw new InvalidOperationException("File system containing parent directory was not found."); - return pathAndFs.FileSystem.FileOpenRead(path.RemoveParent(parent)); + return pathAndFs.FileSystem.FileOpenRead(path.RemoveParent(pathAndFs.Path)); } /// Opens an existing file for writing. /// The file to be opened for writing. + /// Append file. /// An unshared object on /// the specified path with write access. /// Writable /// file system containing parent directory was not found. - public override Stream FileOpenWrite(Path path) + public override Stream FileOpenWrite(Path path, bool append = false) { Path parent = path.Parent; @@ -328,16 +363,39 @@ namespace VirtualFS.Implementation if (pathAndFs == null) throw new InvalidOperationException("Writable file system containing parent directory was not found."); - return pathAndFs.FileSystem.FileOpenWrite(path.RemoveParent(parent)); + return pathAndFs.FileSystem.FileOpenWrite(path.RemoveParent(pathAndFs.Path)); + } + + /// Creates empty file on file system and returns it's description. + /// The path and name of the file to create. + /// If file exists replace it. + /// File description. + public override File FileTouch(Path path, bool overwrite = false) + { + Path parent = path.Parent; + + if (path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path)); + + var pathAndFs = _mounts + .Where(x => x.Key.IsParentOfOrSame(parent)) + .SelectMany(v => v.Value, (k, v) => new { Path = k.Key, FileSystem = v }) + .FirstOrDefault(pfs => !pfs.FileSystem.IsReadOnly && pfs.FileSystem.Exists(parent.RemoveParent(pfs.Path))); + + if (pathAndFs == null) + throw new InvalidOperationException("Writable file system containing parent directory was not found."); + + return pathAndFs.FileSystem.FileTouch(path.RemoveParent(pathAndFs.Path), overwrite); } /// Creates or overwrites a file in the specified path. /// The path and name of the file to create. + /// If file exists replace it. /// A that provides /// write access to the file specified in path. /// Writable /// file system containing parent directory was not found. - public override Stream FileCreate(Path path) + public override Stream FileCreate(Path path, bool overwrite = false) { Path parent = path.Parent; @@ -352,7 +410,7 @@ namespace VirtualFS.Implementation if (pathAndFs == null) throw new InvalidOperationException("Writable file system containing parent directory was not found."); - return pathAndFs.FileSystem.FileCreate(path.RemoveParent(parent)); + return pathAndFs.FileSystem.FileCreate(path.RemoveParent(pathAndFs.Path), overwrite); } /// Files the delete. @@ -374,7 +432,7 @@ namespace VirtualFS.Implementation if (pathAndFs == null) throw new InvalidOperationException("Writable file system containing parent directory was not found."); - pathAndFs.FileSystem.FileDelete(path.RemoveParent(parent)); + pathAndFs.FileSystem.FileDelete(path.RemoveParent(pathAndFs.Path)); } #endregion IFileSystem implementation @@ -403,7 +461,7 @@ namespace VirtualFS.Implementation public bool Mount(IFileSystem fs, Path path) { if (!path.IsDirectory) - throw new InvalidOperationException("Path '{0}' is not a directory. You can only mount file system under directories."); + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory. You can only mount file system under directories.", path)); lock (_globalLock) { @@ -413,7 +471,15 @@ namespace VirtualFS.Implementation if (!_mounts.ContainsKey(path)) _mounts.Add(path, new List()); - _mounts[path].Insert(_mounts[path].Count(f => f.HighPriority), fs); + if (fs.Priority == FileSystemMountPriority.High) + _mounts[path].Insert(0, fs); + else if (fs.Priority == FileSystemMountPriority.Low) + _mounts[path].Add(fs); + else + _mounts[path].Insert(_mounts[path].Count(f => f.Priority == FileSystemMountPriority.High), fs); + + if (fs is BaseFileSystem) + ((BaseFileSystem)fs).RootFileSystem = this; } return true; @@ -449,6 +515,9 @@ namespace VirtualFS.Implementation if (_mounts[mountPaths[i]].Count == 0) _mounts.Remove(mountPaths[i]); + if (fs is BaseFileSystem) + ((BaseFileSystem)fs).RootFileSystem = null; + result = true; } } diff --git a/VirtualFS/Implementation/VirtualStream.cs b/VirtualFS/Implementation/VirtualStream.cs index 2d0f9d7..80d9560 100644 --- a/VirtualFS/Implementation/VirtualStream.cs +++ b/VirtualFS/Implementation/VirtualStream.cs @@ -26,6 +26,7 @@ * THE POSSIBILITY OF SUCH DAMAGE. */ +using System; using System.IO; using VirtualFS.Base; @@ -36,19 +37,30 @@ namespace VirtualFS.Implementation public class VirtualStream : Stream { /// Gets the internal stream. + /// Do not close internal stream by yourself. public Stream InternalStream { get; private set; } + /// Gets the path of file. + public Path Path { get; private set; } + /// Gets the file system. public IFileSystem FileSystem { get; private set; } - /// Initializes a new instance of - /// the class. + /// Gets or sets some unspecified kind of data. + internal object Unspecified { get; set; } + + /// Occurs when stream is closed. + public event EventHandler OnClose; + + /// Initializes a new instance of the class. /// The file system. /// The internal stream. - internal VirtualStream(BaseFileSystem fileSystem, Stream stream) + /// The path of file. + internal VirtualStream(BaseFileSystem fileSystem, Stream stream, Path path) { fileSystem.IncreaseOpened(); InternalStream = stream; + Path = path; FileSystem = fileSystem; } @@ -61,6 +73,9 @@ namespace VirtualFS.Implementation { ((BaseFileSystem)FileSystem).DecreaseOpened(); InternalStream.Close(); + + if (OnClose != null) + OnClose(this, EventArgs.Empty); } #region Wrapped Properties diff --git a/VirtualFS/Libraries/ICSharpCode.SharpZipLib.dll b/VirtualFS/Libraries/ICSharpCode.SharpZipLib.dll deleted file mode 100644 index fe643eb..0000000 Binary files a/VirtualFS/Libraries/ICSharpCode.SharpZipLib.dll and /dev/null differ diff --git a/VirtualFS/Memory/MemoryFileSystem.cs b/VirtualFS/Memory/MemoryFileSystem.cs index ffd4f87..3e870e6 100644 --- a/VirtualFS/Memory/MemoryFileSystem.cs +++ b/VirtualFS/Memory/MemoryFileSystem.cs @@ -197,31 +197,68 @@ namespace VirtualFS.Memory { base.FileOpenRead(path); - return new VirtualStream(this, new System.IO.MemoryStream((byte[])(GetEntry(path).Data ?? new byte[] { }))); + return new VirtualStream(this, new System.IO.MemoryStream((byte[])(GetEntry(path).Data ?? new byte[] { })), path); } /// Opens an existing file for writing. /// The file to be opened for writing. + /// Append file. /// An unshared object on /// the specified path with write access. - public override System.IO.Stream FileOpenWrite(Path path) + public override System.IO.Stream FileOpenWrite(Path path, bool append = false) { base.FileOpenWrite(path); var e = GetEntry(path); - var data = (byte[])(e.Data ?? new byte[] { }); - var ms = new System.IO.MemoryStream(); - ms.Write(data, 0, data.Length); - return new MemoryWriteVirtualStream(this, (File)e, ms); + if (append) + { + var data = (byte[])(e.Data ?? new byte[] { }); + ms.Write(data, 0, data.Length); + } + + return new MemoryWriteVirtualStream(this, (File)e, path, ms); + } + + /// Creates empty file on file system and returns it's description. + /// The path and name of the file to create. + /// If file exists replace it. + /// File description. + public override File FileTouch(Path path, bool overwrite = false) + { + base.FileTouch(path, overwrite); + + var e = (File)GetEntry(path); + if (e != null) + return e; + else + { + e = new File + { + Path = path, + Data = new byte[] { }, + FileSystem = this, + IsReadOnly = IsReadOnly, + CreationTime = DateTime.Now, + LastAccessTime = DateTime.Now, + LastWriteTime = DateTime.Now, + Size = 0, + }; + + lock (_lock) + _pathToEntry[path] = e; + + return e; + } } /// Creates or overwrites a file in the specified path. /// The path and name of the file to create. + /// If file exists replace it. /// A that provides /// write access to the file specified in path. - public override System.IO.Stream FileCreate(Path path) + public override System.IO.Stream FileCreate(Path path, bool overwrite = false) { base.FileCreate(path); @@ -239,13 +276,14 @@ namespace VirtualFS.Memory CreationTime = DateTime.Now, LastAccessTime = DateTime.Now, LastWriteTime = DateTime.Now, + Size = 0, }; lock (_lock) _pathToEntry[path] = e; } - return new MemoryWriteVirtualStream(this, e, new System.IO.MemoryStream()); + return new MemoryWriteVirtualStream(this, e, path, new System.IO.MemoryStream()); } /// Deletes the specified file. An exception is not thrown diff --git a/VirtualFS/Memory/MemoryWriteVirtualStream.cs b/VirtualFS/Memory/MemoryWriteVirtualStream.cs index e33ab8f..40e94d8 100644 --- a/VirtualFS/Memory/MemoryWriteVirtualStream.cs +++ b/VirtualFS/Memory/MemoryWriteVirtualStream.cs @@ -44,9 +44,10 @@ namespace VirtualFS.Memory /// class. /// The file system. /// The file to which stream belongs. + /// The path of file. /// The internal stream. - internal MemoryWriteVirtualStream(BaseFileSystem fileSystem, File file, MemoryStream stream) - : base(fileSystem, stream) + internal MemoryWriteVirtualStream(BaseFileSystem fileSystem, File file, Path path, MemoryStream stream) + : base(fileSystem, stream, path) { File = file; } @@ -59,6 +60,7 @@ namespace VirtualFS.Memory public override void Close() { File.Data = ((MemoryStream)InternalStream).ToArray(); + File.Size = ((byte[])File.Data).LongLength; base.Close(); } } diff --git a/VirtualFS/Path.cs b/VirtualFS/Path.cs index 7eb5fd4..77fcf80 100644 --- a/VirtualFS/Path.cs +++ b/VirtualFS/Path.cs @@ -27,17 +27,15 @@ */ using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using VirtualFS.Extensions; namespace VirtualFS { - /// - /// Class representing path in file system. - /// - /// - /// Path is always rooted, so you can't make a mistake. - /// + /// Class representing path in file system. + /// Path is always rooted, so you can't make a mistake. public class Path { #region Static part @@ -76,6 +74,40 @@ namespace VirtualFS Root = new Path(); } + /// Determines whether the specified path is rooted. + /// The path to check. + /// Returns true if the specified path is rooted; otherwise, false. + public static bool IsRooted(string path) + { + return path[0] == SeparatorChar; + } + + /// Creates the rooted path. + /// The path. + /// Path that is rooted in case of passing not rooted path in form of string. + public static Path CreateRootedPath(string path) + { + return IsRooted(path) ? (Path)path : Root + path; + } + + /// Tries the parse path. + /// The path. + /// The virtual file system path. + /// Returns true if the specified path was parsed; otherwise, false. + public static bool TryParsePath(string path, out Path vfspath) + { + try + { + vfspath = Path.CreateRootedPath(path); + } + catch + { + vfspath = null; + } + + return vfspath != null; + } + #endregion Static part private string _path; @@ -89,20 +121,41 @@ namespace VirtualFS { } - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The path. /// Path is not rooted. public Path(string path) { - _path = path.Validated("Path", invalidChars: InvalidPathChars, invalidSeq: InvalidPathSequences); - _segments = _path.Split(SeparatorChar).Where(x => !string.IsNullOrEmpty(x)).ToArray(); - if (path[0] != SeparatorChar) throw new InvalidOperationException("Path is not rooted."); - IsDirectory = _path.EndsWith(SeparatorChar.ToString()); + List segments = path.Split(SeparatorChar).Where(x => !string.IsNullOrEmpty(x)).ToList(); + List finalSegments = new List(); + + foreach (string segment in segments) + { + if (segment == "." || segment == "") + continue; + else if (segment == "..") + { + if (finalSegments.Any()) + finalSegments.RemoveAt(finalSegments.Count - 1); + else + throw new InvalidOperationException("Path tries to leave root."); + + continue; + } + //else if (segment.ToArray().Distinct().FirstOrDefault() == '.') + // throw new InvalidOperationException("Path contains too much M&M's."); + + finalSegments.Add(segment); + } + + IsDirectory = path.EndsWith(SeparatorChar.ToString()); + + _path = string.Concat(SeparatorChar.ToString(), string.Join(SeparatorChar.ToString(), finalSegments), IsDirectory && finalSegments.Any() ? SeparatorChar.ToString() : string.Empty) + .Validated("Path", invalidChars: InvalidPathChars, invalidSeq: InvalidPathSequences); + _segments = finalSegments.ToArray(); } /// Gets a value indicating whether this instance is directory. @@ -166,6 +219,22 @@ namespace VirtualFS return name.Substring(extensionIndex + 1); } + /// Gets the file name without extension. + /// Return file name without extension as string. + /// The specified path is not a file. + public string GetNameWithoutExtension() + { + if (IsDirectory) + throw new ArgumentException("The specified path is not a file."); + + string name = _segments.Last(); + int extensionIndex = name.LastIndexOf(ExtensionChar); + if (extensionIndex < 0 || extensionIndex == name.Length) + return string.Empty; + + return name.Substring(0, extensionIndex); + } + /// Gets the name of entry from path. public string Name { get { return _segments.Length > 0 ? _segments.Last() : null; } } @@ -203,7 +272,7 @@ namespace VirtualFS { if (!parent.IsDirectory) throw new ArgumentException("The specified path can not be the parent of this path: it is not a directory."); - if (!_path.StartsWith(_path)) + if (!_path.StartsWith(parent)) throw new ArgumentException("The specified path is not a parent of this path."); return new Path(_path.Remove(0, parent._path.Length - 1)); @@ -211,7 +280,26 @@ namespace VirtualFS internal Path AddParent(Path parent) { - return parent.ToString().TrimEnd(SeparatorChar) + this; + return string.Concat(parent.ToString().TrimEnd(SeparatorChar), this.ToString()); + } + + /// Combine paths. + /// Array of paths to combine. + /// Instance of Path + public static Path Combine(params string[] components) + { + var list = new List(); + foreach (var component in components) + if (!string.IsNullOrEmpty(component)) + foreach (var c in component.Split(new char[] { SeparatorChar }, StringSplitOptions.RemoveEmptyEntries)) + list.Add(c); + + var last = components.Where(x => !string.IsNullOrEmpty(x)).LastOrDefault(); + if (last == null) + last = string.Empty; + + return (string.Join(SeparatorChar.ToString(), list.ToArray()) + + (last.EndsWith(SeparatorChar.ToString()) ? SeparatorChar.ToString() : string.Empty)); } #region Operators @@ -220,14 +308,14 @@ namespace VirtualFS /// The path. public static implicit operator string(Path path) { - return path._path; + return path == null ? null : path._path; } /// Convert string to path. /// The path. public static implicit operator Path(string path) { - return new Path(path); + return path == null ? null : new Path(path); } /// Determines whether the specified is equal to this instance. diff --git a/VirtualFS/Physical/FtpFileSystem.cs b/VirtualFS/Physical/FtpFileSystem.cs new file mode 100644 index 0000000..5afcef2 --- /dev/null +++ b/VirtualFS/Physical/FtpFileSystem.cs @@ -0,0 +1,378 @@ +/* + * VirtualFS - Virtual File System library. + * Copyright (c) 2013, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.FtpClient; +using System.Text.RegularExpressions; +using VirtualFS.Base; +using VirtualFS.Implementation; + +namespace VirtualFS.Physical +{ + /// Ftp file system implementation. + /// This is very, very simple implementation + /// of a FTP file system. Use this when regular FTP won't do the job. + public class FtpFileSystem : BaseFileSystem + { + private string _ftpServer; + private int _ftpServerPort; + private string _userName; + private string _password; + private string _rootPath; + + /// Gets or sets the connection type, default is + /// AutoPassive which tries + /// a connection with EPSV first and if it fails then tries + /// PASV before giving up. If you know exactly which kind of + /// connection you need you can slightly increase performance + /// by defining a speicific type of passive or active data + /// connection here. + public FtpDataConnectionType ConnectionType { get; set; } + + /// Initializes a new instance of the class. + /// The FTP server. + /// FTP User name. + /// FTP password. + /// FTP root path on server. + public FtpFileSystem(Uri ftpServer, string userName, string password, string rootPath) + : base(false) + { + ConnectionType = FtpDataConnectionType.AutoActive; + _ftpServer = ftpServer.Host; + _ftpServerPort = ftpServer.Port; + _userName = userName; + _password = password; + _rootPath = rootPath; + + if (_ftpServer.EndsWith("/")) + _ftpServer = _ftpServer.Substring(0, _ftpServer.Length - 1); + + if (!string.IsNullOrEmpty(_rootPath) && _rootPath.EndsWith("/")) + _rootPath = _rootPath.Substring(0, _rootPath.Length - 1); + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + try + { + using (FtpClient conn = GetConnection()) + { + if (path.IsDirectory) + return conn.DirectoryExists(GetFtpPath(path)); + else + return conn.FileExists(GetFtpPath(path)); + } + } + catch (WebException) + { + return false; + } + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + Entry result = null; + + if (Exists(path)) + { + if (path.IsDirectory) + result = new Directory(); + else + result = new File(); + + result.IsReadOnly = IsReadOnly; + result.Path = path; + result.FileSystem = this; + } + + return result; + } + + /// Get path to physical file containing entry. + /// Virtual file system path. + /// Real file system path or null if real + /// path doesn't exist. + /// This may not work with all file systems. Implementation + /// should return null if real path can't be determined. + public override string EntryRealPath(Path path) + { + string uri = _ftpServer; + + if (!string.IsNullOrEmpty(_rootPath)) + uri += _rootPath; + + if (path.Parent != null) + uri += (string)path.Parent + (string)path.Name ?? string.Empty; + + return uri; + } + + /// Get entries located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetEntries(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.")); + + List result = new List(); + + using (FtpClient conn = GetConnection()) + { + foreach (FtpListItem item in conn + .GetListing(GetFtpPath(path), + FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks | FtpListOption.AllFiles)) + { + switch (item.Type) + { + case FtpFileSystemObjectType.File: + result.Add(new File() + { + IsReadOnly = IsReadOnly, + Path = path + item.Name, + FileSystem = this, + CreationTime = item.Created, + LastWriteTime = item.Modified, + LastAccessTime = item.Modified, + Size = item.Size, + }); + break; + + case FtpFileSystemObjectType.Directory: + result.Add(new Directory() + { + IsReadOnly = IsReadOnly, + Path = string.Concat(path, item.Name.TrimEnd('/'), "/"), + FileSystem = this, + CreationTime = item.Created, + LastWriteTime = item.Modified, + LastAccessTime = item.Modified, + }); + break; + + case FtpFileSystemObjectType.Link: + + if (item.LinkObject != null) + { + switch (item.LinkObject.Type) + { + case FtpFileSystemObjectType.File: + result.Add(new File() + { + IsReadOnly = IsReadOnly, + Path = path + item.Name, + FileSystem = this, + CreationTime = item.LinkObject.Created, + LastWriteTime = item.LinkObject.Modified, + LastAccessTime = item.LinkObject.Modified, + Size = item.LinkObject.Size, + }); + break; + + case FtpFileSystemObjectType.Directory: + result.Add(new Directory() + { + IsReadOnly = IsReadOnly, + Path = string.Concat(path, item.Name.TrimEnd('/'), "/"), + FileSystem = this, + CreationTime = item.LinkObject.Created, + LastWriteTime = item.LinkObject.Modified, + LastAccessTime = item.LinkObject.Modified, + }); + break; + } + } + + break; + } + } + } + + return result; + } + + /// Change name of the entry under specified path. + /// The path to entry which name will be changed. + /// The new name of entry. + public override void ReName(Path path, string newName) + { + base.ReName(path, newName); + + using (FtpClient conn = GetConnection()) + conn.Rename(GetFtpPath(path), GetFtpPath(path.Parent + newName)); + } + + /// Create Directory and return new entry. + /// Path of directory. + /// Created entry. + public override Directory DirectoryCreate(Path path) + { + base.DirectoryCreate(path); + + using (FtpClient conn = GetConnection()) + conn.CreateDirectory(GetFtpPath(path), true); + + return (Directory)GetEntry(path); + } + + /// Deletes the specified directory and, if + /// indicated, any subdirectories in the directory. + /// The path of the directory to remove. + /// Set true to remove directories, + /// subdirectories, and files in path; otherwise false. + public override void DirectoryDelete(Path path, bool recursive = false) + { + base.DirectoryDelete(path, recursive); + + using (FtpClient conn = GetConnection()) + conn.DeleteDirectory(GetFtpPath(path)); + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + public override Stream FileOpenRead(Path path) + { + base.FileOpenRead(path); + + FtpClient conn = GetConnection(); + + var ret = new VirtualStream(this, conn.OpenRead(GetFtpPath(path)), path); + + ret.Unspecified = conn; + ret.OnClose += (s, e) => ((FtpClient)((VirtualStream)s).Unspecified).Dispose(); + + return ret; + } + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// Append file. + /// An unshared object on + /// the specified path with write access. + public override Stream FileOpenWrite(Path path, bool append = false) + { + base.FileOpenWrite(path, append); + + return FileOpenWritePrivate(path, append); + } + + /// Creates empty file on file system and returns it's description. + /// The path and name of the file to create. + /// If file exists replace it. + /// File description. + public override File FileTouch(Path path, bool overwrite = false) + { + base.FileTouch(path, overwrite); + using (var fs = FileOpenWritePrivate(path, false)) + fs.Close(); + + return new File() + { + IsReadOnly = IsReadOnly, + Path = path, + FileSystem = this, + CreationTime = DateTime.Now, + LastWriteTime = DateTime.Now, + LastAccessTime = DateTime.Now, + Size = 0, + }; + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// If file exists replace it. + /// A that provides + /// write access to the file specified in path. + public override Stream FileCreate(Path path, bool overwrite = false) + { + base.FileCreate(path, overwrite); + + return FileOpenWritePrivate(path, false); + } + + /// Deletes the specified file. An exception is not thrown + /// if the specified file does not exist. + /// The path of the file to be deleted. + public override void FileDelete(Path path) + { + base.FileDelete(path); + + using (FtpClient conn = GetConnection()) + conn.DeleteFile(GetFtpPath(path)); + } + + private Stream FileOpenWritePrivate(Path path, bool append) + { + FtpClient conn = GetConnection(); + + var ret = new VirtualStream(this, append ? conn.OpenAppend(GetFtpPath(path), FtpDataType.Binary) : conn.OpenWrite(GetFtpPath(path), FtpDataType.Binary), path); + + ret.Unspecified = conn; + ret.OnClose += (s, e) => ((FtpClient)((VirtualStream)s).Unspecified).Dispose(); + + return ret; + } + + private string GetFtpPath(string path) + { + string fullPath = string.Empty; // _ftpServer; + + if (!string.IsNullOrEmpty(_rootPath)) + fullPath += _rootPath; + + if (path != null) + fullPath += path; + + return fullPath; + } + + private FtpClient GetConnection() + { + FtpClient conn = new FtpClient(); + conn.DataConnectionType = ConnectionType; + conn.Host = _ftpServer; + conn.Port = _ftpServerPort; + + conn.Credentials = new NetworkCredential(_userName, _password); + + return conn; + } + } +} \ No newline at end of file diff --git a/VirtualFS/Physical/PhysicalFileSystem.cs b/VirtualFS/Physical/PhysicalFileSystem.cs index d535b01..864c343 100644 --- a/VirtualFS/Physical/PhysicalFileSystem.cs +++ b/VirtualFS/Physical/PhysicalFileSystem.cs @@ -62,7 +62,7 @@ namespace VirtualFS.Physical return new Directory { IsReadOnly = IsReadOnly, - Path = rootPath + string.Format("{0}{1}", fsi.Name, Path.SeparatorChar), + Path = rootPath == null ? Path.Root : (Path)(rootPath + string.Format("{0}{1}", fsi.Name, Path.SeparatorChar)), FileSystem = this, CreationTime = fsi.CreationTimeUtc, LastAccessTime = fsi.LastAccessTime, @@ -77,6 +77,7 @@ namespace VirtualFS.Physical CreationTime = fsi.CreationTimeUtc, LastAccessTime = fsi.LastAccessTime, LastWriteTime = fsi.LastWriteTime, + Size = (fsi as System.IO.FileInfo).Length, }; return null; @@ -108,7 +109,7 @@ namespace VirtualFS.Physical fsi = new System.IO.FileInfo(EntryRealPath(path)); if (fsi.Exists) - result = FileSystemInfoToEntry(fsi, path); + result = FileSystemInfoToEntry(fsi, path.Parent); return result; } @@ -130,6 +131,36 @@ namespace VirtualFS.Physical .Select(f => FileSystemInfoToEntry(f, path)); } + /// Get directories located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetDirectories(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.")); + + return new System.IO.DirectoryInfo(EntryRealPath(path)) + .EnumerateDirectories() + .Where(f => mask == null || mask.IsMatch(f.Name)) + .Select(f => FileSystemInfoToEntry(f, path) as Directory); + } + + /// Get files located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetFiles(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.")); + + return new System.IO.DirectoryInfo(EntryRealPath(path)) + .EnumerateFiles() + .Where(f => mask == null || mask.IsMatch(f.Name)) + .Select(f => FileSystemInfoToEntry(f, path) as File); + } + /// Get path to physical file containing entry. /// Virtual file system path. /// Real file system path or null if real @@ -218,34 +249,51 @@ namespace VirtualFS.Physical { base.FileOpenRead(path); - return new VirtualStream(this, System.IO.File.OpenRead(EntryRealPath(path))); + return new VirtualStream(this, System.IO.File.OpenRead(EntryRealPath(path)), path); } /// Opens an existing file for writing. /// The file to be opened for writing. - /// An unshared object on + /// Append file. + /// An unshared object on /// the specified path with write access. - public override Stream FileOpenWrite(Path path) + public override Stream FileOpenWrite(Path path, bool append = false) { - base.FileOpenWrite(path); + base.FileOpenWrite(path, append); string realPath = EntryRealPath(path); if (System.IO.File.Exists(realPath)) - return new VirtualStream(this, new FileStream(realPath, FileMode.Open, FileAccess.Write, FileShare.Read, 4096, FileOptions.None)); + return new VirtualStream(this, new FileStream(realPath, append ? FileMode.Append : FileMode.Open, FileAccess.Write, FileShare.Read, 4096, FileOptions.None), path); return null; } - /// Creates or overwrites a file in the specified path. + /// Creates empty file on file system and returns it's description. /// The path and name of the file to create. - /// A that provides - /// write access to the file specified in path. - public override Stream FileCreate(Path path) + /// If file exists replace it. + /// File description. + public override File FileTouch(Path path, bool overwrite = false) { - base.FileCreate(path); + base.FileTouch(path, overwrite); string realPath = EntryRealPath(path); - return new VirtualStream(this, new FileStream(realPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 4096)); + using (var fs = new FileStream(realPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 4096)) + fs.Close(); + + return FileSystemInfoToEntry(new System.IO.FileInfo(realPath), path.Parent) as File; + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// If file exists replace it. + /// A that provides + /// write access to the file specified in path. + public override Stream FileCreate(Path path, bool overwrite = false) + { + base.FileCreate(path, overwrite); + string realPath = EntryRealPath(path); + + return new VirtualStream(this, new FileStream(realPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 4096), path); } /// Deletes the specified file. An exception is thrown @@ -253,7 +301,7 @@ namespace VirtualFS.Physical /// The path of the file to be deleted. public override void FileDelete(Path path) { - base.FileCreate(path); + base.FileDelete(path); string realPath = EntryRealPath(path); if (System.IO.File.Exists(realPath)) diff --git a/VirtualFS/Physical/SFtpFileSystem.cs b/VirtualFS/Physical/SFtpFileSystem.cs new file mode 100644 index 0000000..994a654 --- /dev/null +++ b/VirtualFS/Physical/SFtpFileSystem.cs @@ -0,0 +1,500 @@ +/* + * VirtualFS - Virtual File System library. + * Copyright (c) 2021, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +using Renci.SshNet; +using Renci.SshNet.Sftp; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using VirtualFS.Base; + +namespace VirtualFS.Physical +{ + public class SFtpFileSystem : BaseFileSystem + { + private string _ftpServer; + private string _userName; + private string _password; + private byte[] _key; + private byte[] _fingerPrint; + private string _rootPath; + private Action _exHandler; + private SftpClient _client; + + private SftpClient Client + { + get + { + if (_client == null) + _client = CreateClientPrivate(); + + return _client; + } + } + + /// Initializes a new instance of the class. + /// The SFTP server. + /// SFTP User name. + /// SFTP password. + /// SFTP private key. + /// SFTP server fingerprint. + /// SFTP root path on server. + public SFtpFileSystem(string host, string userName, string password, byte[] key, byte[] fingerPrint, string rootPath, Action exHandler = null) + : base(false) + { + _exHandler = exHandler ?? (e => { }); + _ftpServer = host; + _userName = userName; + _password = password; + _key = key; + _fingerPrint = fingerPrint; + _rootPath = rootPath; + + if (_ftpServer.EndsWith("/")) + _ftpServer = _ftpServer.Substring(0, _ftpServer.Length - 1); + + if (!string.IsNullOrEmpty(_rootPath) && _rootPath.EndsWith("/")) + _rootPath = _rootPath.Substring(0, _rootPath.Length - 1); + + if (_rootPath == "/") + _rootPath = string.Empty; + } + + private SftpClient CreateClientPrivate() + { + var ams = new List(); + + if (!string.IsNullOrEmpty(_password)) + ams.Add(new PasswordAuthenticationMethod(_userName, _password)); + + if (_key != null && _key.Length > 0) + using (var ms = new MemoryStream(_key)) + ams.Add(new PrivateKeyAuthenticationMethod(_userName, new PrivateKeyFile(ms))); + + var ci = new ConnectionInfo(_ftpServer, _userName, ams.ToArray()); + + var client = new SftpClient(ci); + + if (_fingerPrint != null && _fingerPrint.Length > 0) + client.HostKeyReceived += (sender, e) => + { + e.CanTrust = true; + + if (_fingerPrint.Length == e.FingerPrint.Length) + for (var i = 0; i < _fingerPrint.Length; i++) + if (_fingerPrint[i] != e.FingerPrint[i]) + { + e.CanTrust = false; + break; + } + else + e.CanTrust = false; + }; + else + client.HostKeyReceived += (sender, e) => e.CanTrust = true; + + client.Connect(); + + return client; + } + + private Entry CreateEntry(Path path, ISftpFile ls, bool pathIsParent = true) + { + Entry e = null; + if (ls.IsDirectory) + e = new Directory + { + IsReadOnly = IsReadOnly, + Path = pathIsParent ? (path + (ls.Name + "/")) : path, + FileSystem = this, + Data = ls, + }; + else + e = new File + { + CreationTime = ls.LastWriteTime, + LastWriteTime = ls.LastWriteTime, + LastAccessTime = ls.LastAccessTime, + Size = ls.Length, + IsReadOnly = IsReadOnly, + Path = pathIsParent ? (path + ls.Name) : path, + FileSystem = this, + Data = ls, + }; + return e; + } + + private string AdaptPath(SftpClient c, Path path) + { + var r = path.ToString().Substring(1); + + if (!string.IsNullOrEmpty(_rootPath)) { + r = (string)(_rootPath + (string)path); + + if (string.IsNullOrEmpty(r)) + return null; + + r = r[0] == Path.SeparatorChar ? r.Substring(1) : r; + } + + var wd = c.WorkingDirectory; + + if (!wd.EndsWith(Path.SeparatorChar.ToString())) + wd += Path.SeparatorChar; + + return wd + r; + } + + /// Get path to physical file containing entry. + /// Virtual file system path. + /// Real file system path or null if real + /// path doesn't exist. + /// This may not work with all file systems. Implementation + /// should return null if real path can't be determined. + public override string EntryRealPath(Path path) + { + string uri = _ftpServer; + + if (!string.IsNullOrEmpty(_rootPath)) + uri += _rootPath; + + if (path.Parent != null) + uri += (string)path.Parent + (string)path.Name ?? string.Empty; + + return uri; + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + try + { + return Client.Exists(AdaptPath(Client, path)); + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + + return false; + } + } + + /// Get entries located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetEntries(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.", path)); + + List result = new List(); + + try + { + var lst = Client.ListDirectory(AdaptPath(Client, path)); + + foreach (var ls in lst) + { + if (ls.Name == "." || ls.Name == "..") + continue; + + Entry e = CreateEntry(path, ls); + + if (e != null) + result.Add(e); + } + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + + return result; + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + Entry result = null; + + try + { + if (Client.Exists(AdaptPath(Client, path))) + result = CreateEntry(path, Client.Get(AdaptPath(Client, path)), false); + } + catch (Exception) + { + } + + return result; + } + + /// Change name of the entry under specified path. + /// The path to entry which name will be changed. + /// The new name of entry. + public override void ReName(Path path, string newName) + { + base.ReName(path, newName); + + try + { + Client.RenameFile(AdaptPath(Client, path), AdaptPath(Client, path.Parent + newName)); + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + } + + + /// Create Directory and return new entry. + /// Path of directory. + /// Created entry. + public override Directory DirectoryCreate(Path path) + { + base.DirectoryCreate(path); + + try + { + Client.CreateDirectory(AdaptPath(Client, path)); + + if (Client.Exists(AdaptPath(Client, path))) + return CreateEntry(path, Client.Get(AdaptPath(Client, path)), false) as Directory; + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + + return null; + } + + /// Deletes the specified directory and, if + /// indicated, any subdirectories in the directory. + /// The path of the directory to remove. + /// Set true to remove directories, + /// subdirectories, and files in path; otherwise false. + public override void DirectoryDelete(Path path, bool recursive = false) + { + base.DirectoryDelete(path, recursive); + + try + { + if (Client.Exists(AdaptPath(Client, path))) + Client.DeleteDirectory(AdaptPath(Client, path)); + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + public override Stream FileOpenRead(Path path) + { + base.FileOpenRead(path); + + try + { + return Client.OpenRead(AdaptPath(Client, path)); + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + + return null; + } + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// Append file. + /// An unshared object on + /// the specified path with write access. + public override Stream FileOpenWrite(Path path, bool append = false) + { + base.FileOpenWrite(path, append); + + try + { + return Client.OpenWrite(AdaptPath(Client, path)); + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + + return null; + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// If file exists replace it. + /// A that provides + /// write access to the file specified in path. + public override File FileTouch(Path path, bool overwrite = false) + { + base.FileTouch(path, overwrite); + + try + { + using (var fs = Client.OpenWrite(AdaptPath(Client, path))) + fs.Close(); + + return new File + { + CreationTime = DateTime.Now, + LastWriteTime = DateTime.Now, + LastAccessTime = DateTime.Now, + Size = 0, + IsReadOnly = false, + Path = path, + FileSystem = this, + Data = null, + }; + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + + return null; + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// If file exists replace it. + /// A that provides + /// write access to the file specified in path. + public override Stream FileCreate(Path path, bool overwrite = false) + { + base.FileCreate(path, overwrite); + + try + { + return Client.OpenWrite(AdaptPath(Client, path)); + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + + return null; + } + + + /// Deletes the specified file. An exception is not thrown + /// if the specified file does not exist. + /// The path of the file to be deleted. + public override void FileDelete(Path path) + { + base.FileDelete(path); + + try + { + if (Client.Exists(AdaptPath(Client, path))) + Client.DeleteFile(AdaptPath(Client, path)); + } + catch (Exception ex) + { + _exHandler(ex); + + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + } + } +} \ No newline at end of file diff --git a/VirtualFS/Physical/SimpleFtpCachedFileSystem.cs b/VirtualFS/Physical/SimpleFtpCachedFileSystem.cs new file mode 100644 index 0000000..3911cba --- /dev/null +++ b/VirtualFS/Physical/SimpleFtpCachedFileSystem.cs @@ -0,0 +1,211 @@ +/* + * VirtualFS - Virtual File System library. + * Copyright (c) 2013, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using VirtualFS.Base; +using VirtualFS.Implementation; + +namespace VirtualFS.Physical +{ + /// Ftp file system implementation. + /// This is very, very simple implementation + /// of a FTP file system. Use this when regular FTP won't do the job. + public class SimpleFtpCachedFileSystem : SimpleFtpFileSystem + { + private Dictionary> _cache = new Dictionary>(); + + /// Initializes a new instance of the class. + /// The FTP server. + /// FTP User name. + /// FTP password. + /// FTP root path on server. + public SimpleFtpCachedFileSystem(string ftpServer, string userName, string password, string rootPath) + : base(ftpServer, userName, password, rootPath) + { + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + try + { + if (path.IsDirectory && path == "/") + return true; + + if (path.Parent == null) + return false; + + var entries = this.GetEntries(path.Parent); + + return entries.Any(x => x.Path == path); + } + catch (WebException) + { + return false; + } + } + + /// Get entries located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetEntries(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.", path)); + + List cached = null; + if (_cache.ContainsKey(path)) + cached = _cache[path]; + else + { + cached = base.GetEntries(path, null).ToList(); + _cache.Add(path, cached); + } + + if (mask == null) + return cached.Select(x => x).ToList(); + + return cached.Where(x => mask.IsMatch(x.Path.Name)).Select(x => x).ToList(); + } + + /// Change name of the entry under specified path. + /// The path to entry which name will be changed. + /// The new name of entry. + public override void ReName(Path path, string newName) + { + base.ReName(path, newName); + + if (_cache.ContainsKey(path.Parent)) + { + var rf = _cache[path.Parent].FirstOrDefault(x => x.Path == path); + + if (rf != null) + rf.Path = (path.Parent + newName); + } + } + + /// Create Directory and return new entry. + /// Path of directory. + /// Created entry. + public override Directory DirectoryCreate(Path path) + { + var dir = base.DirectoryCreate(path); + + if (_cache.ContainsKey(path.Parent)) + { + _cache[path.Parent].Add(new Directory + { + Path = path, + FileSystem = this, + IsReadOnly = IsReadOnly, + }); + } + + return dir; + } + + /// Deletes the specified directory and, if + /// indicated, any subdirectories in the directory. + /// The path of the directory to remove. + /// Set true to remove directories, + /// subdirectories, and files in path; otherwise false. + public override void DirectoryDelete(Path path, bool recursive = false) + { + base.DirectoryDelete(path, recursive); + + if (_cache.ContainsKey(path.Parent)) + { + var de = _cache[path.Parent].FirstOrDefault(x => x.Path == path); + + if (de != null) + _cache[path.Parent].Remove(de); + } + } + + /// Creates empty file on file system and returns it's description. + /// The path and name of the file to create. + /// If file exists replace it. + /// File description. + public override File FileTouch(Path path, bool overwrite = false) + { + var r = base.FileTouch(path, overwrite); + + if (_cache.ContainsKey(path.Parent)) + _cache[path.Parent].Add(r); + + return r; + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// If file exists replace it. + /// A that provides + /// write access to the file specified in path. + public override Stream FileCreate(Path path, bool overwrite = false) + { + var str = base.FileCreate(path, overwrite); + + if (_cache.ContainsKey(path.Parent)) + { + _cache[path.Parent].Add(new File + { + Path = path, + FileSystem = this, + IsReadOnly = IsReadOnly, + }); + } + + return str; + } + + /// Deletes the specified file. An exception is not thrown + /// if the specified file does not exist. + /// The path of the file to be deleted. + public override void FileDelete(Path path) + { + base.FileDelete(path); + + if (_cache.ContainsKey(path.Parent)) + { + var de = _cache[path.Parent].FirstOrDefault(x => x.Path == path); + + if (de != null) + _cache[path.Parent].Remove(de); + } + } + } +} \ No newline at end of file diff --git a/VirtualFS/Physical/SimpleFtpFileSystem.cs b/VirtualFS/Physical/SimpleFtpFileSystem.cs new file mode 100644 index 0000000..a2f52d2 --- /dev/null +++ b/VirtualFS/Physical/SimpleFtpFileSystem.cs @@ -0,0 +1,376 @@ +/* + * VirtualFS - Virtual File System library. + * Copyright (c) 2013, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text.RegularExpressions; +using VirtualFS.Base; +using VirtualFS.Implementation; + +namespace VirtualFS.Physical +{ + /// Ftp file system implementation. + /// This is very, very simple implementation + /// of a FTP file system. Use this when regular FTP won't do the job. + public class SimpleFtpFileSystem : BaseFileSystem + { + private string _ftpServer; + private string _userName; + private string _password; + private string _rootPath; + + /// Initializes a new instance of the class. + /// The FTP server. + /// FTP User name. + /// FTP password. + /// FTP root path on server. + public SimpleFtpFileSystem(string ftpServer, string userName, string password, string rootPath) + : base(false) + { + _ftpServer = ftpServer; + _userName = userName; + _password = password; + _rootPath = rootPath; + + if (_ftpServer.EndsWith("/")) + _ftpServer = _ftpServer.Substring(0, _ftpServer.Length - 1); + + if (!string.IsNullOrEmpty(_rootPath) && _rootPath.EndsWith("/")) + _rootPath = _rootPath.Substring(0, _rootPath.Length - 1); + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + try + { + if (path.IsDirectory) + { + FtpDoStuff(WebRequestMethods.Ftp.ListDirectory, path, null); + return true; + } + else + { + bool res = false; + FtpDoStuff(WebRequestMethods.Ftp.ListDirectory, path, null, r => + { + using (StreamReader sr = new StreamReader(r.GetResponseStream())) + { + string str = sr.ReadLine(); + while (str != null) + { + if (string.IsNullOrEmpty(str) || str == "." || str == "..") + { + str = sr.ReadLine(); + continue; + } + + if (str == path.Name) + { + res = true; + sr.ReadToEnd(); + return; + } + } + } + }); + + return res; + } + } + catch (WebException) + { + return false; + } + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + Entry result = null; + + if (Exists(path)) + { + if (path.IsDirectory) + result = new Directory(); + else + result = new File(); + + result.IsReadOnly = IsReadOnly; + result.Path = path; + result.FileSystem = this; + } + + return result; + } + + /// Get path to physical file containing entry. + /// Virtual file system path. + /// Real file system path or null if real + /// path doesn't exist. + /// This may not work with all file systems. Implementation + /// should return null if real path can't be determined. + public override string EntryRealPath(Path path) + { + string uri = _ftpServer; + + if (!string.IsNullOrEmpty(_rootPath)) + uri += _rootPath; + + if (path.Parent != null) + uri += (string)path.Parent + (string)path.Name ?? string.Empty; + + return uri; + } + + /// Get entries located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + public override IEnumerable GetEntries(Path path, Regex mask = null) + { + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.", path)); + + List result = new List(); + + FtpDoStuff(WebRequestMethods.Ftp.ListDirectory, path, null, r => + { + using (StreamReader sr = new StreamReader(r.GetResponseStream())) + { + string str = sr.ReadLine(); + while (str != null) + { + if (string.IsNullOrEmpty(str) || str == "." || str == "..") + { + str = sr.ReadLine(); + continue; + } + + if (mask == null || mask.IsMatch(str)) + { + // Here lies a monster. Do not try to understand... just accept. + if (str.Contains(".")) + result.Add(new File() + { + IsReadOnly = IsReadOnly, + Path = path + str, + FileSystem = this, + }); + else + result.Add(new Directory() + { + IsReadOnly = IsReadOnly, + Path = string.Concat(path, str, "/"), + FileSystem = this, + }); + } + + str = sr.ReadLine(); + } + } + }); + + return result; + } + + /// Change name of the entry under specified path. + /// The path to entry which name will be changed. + /// The new name of entry. + public override void ReName(Path path, string newName) + { + base.ReName(path, newName); + + System.Net.FtpWebRequest ftp = (System.Net.FtpWebRequest)System.Net.FtpWebRequest.Create(EntryRealPath(path)); + ftp.Credentials = new System.Net.NetworkCredential(_userName, _password); + ftp.Method = WebRequestMethods.Ftp.Rename; + ftp.RenameTo = newName; + + using (var r = ftp.GetResponse()) + r.Close(); + } + + /// Create Directory and return new entry. + /// Path of directory. + /// Created entry. + public override Directory DirectoryCreate(Path path) + { + base.DirectoryCreate(path); + + if (FtpDoStuff(WebRequestMethods.Ftp.MakeDirectory, path) == FtpStatusCode.PathnameCreated) + return (Directory)GetEntry(path); + + return null; + } + + /// Deletes the specified directory and, if + /// indicated, any subdirectories in the directory. + /// The path of the directory to remove. + /// Set true to remove directories, + /// subdirectories, and files in path; otherwise false. + public override void DirectoryDelete(Path path, bool recursive = false) + { + base.DirectoryDelete(path, recursive); + + FtpDoStuff(WebRequestMethods.Ftp.RemoveDirectory, path); + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + public override Stream FileOpenRead(Path path) + { + base.FileOpenRead(path); + + System.Net.FtpWebRequest ftp = (System.Net.FtpWebRequest)System.Net.FtpWebRequest.Create(EntryRealPath(path)); + ftp.Credentials = new System.Net.NetworkCredential(_userName, _password); + ftp.Method = System.Net.WebRequestMethods.Ftp.DownloadFile; + ftp.KeepAlive = true; + + var resp = ftp.GetResponse(); + var ret = new VirtualStream(this, resp.GetResponseStream(), path); + + ret.Unspecified = resp; + ret.OnClose += (s, e) => + { + using (var r = (FtpWebResponse)((VirtualStream)s).Unspecified) + r.Close(); + }; + + return ret; + } + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// Append file. + /// An unshared object on + /// the specified path with write access. + public override Stream FileOpenWrite(Path path, bool append = false) + { + base.FileOpenWrite(path, append); + + return FileOpenWritePrivate(path, append); + } + + /// Creates empty file on file system and returns it's description. + /// The path and name of the file to create. + /// If file exists replace it. + /// File description. + public override File FileTouch(Path path, bool overwrite = false) + { + base.FileTouch(path, overwrite); + using (var fs = FileOpenWritePrivate(path, false)) + fs.Close(); + + return new File() + { + IsReadOnly = IsReadOnly, + Path = path, + FileSystem = this, + CreationTime = DateTime.Now, + LastWriteTime = DateTime.Now, + LastAccessTime = DateTime.Now, + Size = 0, + }; + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// If file exists replace it. + /// A that provides + /// write access to the file specified in path. + public override Stream FileCreate(Path path, bool overwrite = false) + { + base.FileCreate(path, overwrite); + + return FileOpenWritePrivate(path, false); + } + + /// Deletes the specified file. An exception is not thrown + /// if the specified file does not exist. + /// The path of the file to be deleted. + public override void FileDelete(Path path) + { + base.FileDelete(path); + + FtpDoStuff(WebRequestMethods.Ftp.DeleteFile, path.Parent, path.Name); + } + + private Stream FileOpenWritePrivate(Path path, bool append) + { + System.Net.FtpWebRequest ftp = (System.Net.FtpWebRequest)System.Net.FtpWebRequest.Create(EntryRealPath(path)); + ftp.Credentials = new System.Net.NetworkCredential(_userName, _password); + ftp.Method = append ? WebRequestMethods.Ftp.AppendFile : WebRequestMethods.Ftp.UploadFile; + ftp.KeepAlive = true; + ftp.UseBinary = true; + + var ret = new VirtualStream(this, ftp.GetRequestStream(), path); + + ret.Unspecified = ftp; + ret.OnClose += (s, e) => + { + var req = (FtpWebRequest)((VirtualStream)s).Unspecified; + using (var resp = req.GetResponse()) + resp.Close(); + }; + + return ret; + } + + internal FtpStatusCode FtpDoStuff(string method, string path, string filename = null, Action act = null) + { + string uri = _ftpServer; + + if (!string.IsNullOrEmpty(_rootPath)) + uri += _rootPath; + + if (path != null) + uri += path + filename ?? string.Empty; + + System.Net.FtpWebRequest ftp = (System.Net.FtpWebRequest)System.Net.FtpWebRequest.Create(uri); + ftp.Credentials = new System.Net.NetworkCredential(_userName, _password); + ftp.Method = method; + ftp.KeepAlive = true; + + using (var resp = (FtpWebResponse)ftp.GetResponse()) + { + if (act != null) + act(resp); + + return resp.StatusCode; + } + } + } +} \ No newline at end of file diff --git a/VirtualFS/VirtualFS.csproj b/VirtualFS/VirtualFS.csproj index 66b682f..03558f9 100644 --- a/VirtualFS/VirtualFS.csproj +++ b/VirtualFS/VirtualFS.csproj @@ -21,6 +21,8 @@ + +