From 5f748e55a2d51e148c486bd02ff76dd09c8c6e22 Mon Sep 17 00:00:00 2001 From: "grzegorz.russek" Date: Wed, 15 May 2024 09:38:41 +0200 Subject: [PATCH] Initial commit --- .gitignore | 51 ++ VirtualFS.Tests/PathTests.cs | 47 ++ .../Physical/PhysicalFileSystemTests.cs | 14 + VirtualFS.Tests/RootFileSystemTest.cs | 44 ++ VirtualFS.Tests/VirtualFS.Tests.csproj | 27 + VirtualFS.sln | 34 ++ VirtualFS/Base/BaseFileSystem.cs | 391 ++++++++++++++ VirtualFS/Compressed/ZipReadFileSystem.cs | 228 +++++++++ VirtualFS/Directory.cs | 123 +++++ .../EmbeddedResourceFileSystem.cs | 215 ++++++++ VirtualFS/Entry.cs | 99 ++++ VirtualFS/Extensions/Validators.cs | 128 +++++ VirtualFS/File.cs | 207 ++++++++ VirtualFS/IFileSystem.cs | 149 ++++++ VirtualFS/IRootFileSystem.cs | 69 +++ VirtualFS/Implementation/RootFileSystem.cs | 482 ++++++++++++++++++ VirtualFS/Implementation/VirtualStream.cs | 251 +++++++++ .../Libraries/ICSharpCode.SharpZipLib.dll | Bin 0 -> 200704 bytes VirtualFS/Memory/MemoryFileSystem.cs | 265 ++++++++++ VirtualFS/Memory/MemoryWriteVirtualStream.cs | 65 +++ VirtualFS/Path.cs | 307 +++++++++++ VirtualFS/Physical/PhysicalFileSystem.cs | 263 ++++++++++ VirtualFS/VirtualFS.csproj | 29 ++ 23 files changed, 3488 insertions(+) create mode 100644 .gitignore create mode 100644 VirtualFS.Tests/PathTests.cs create mode 100644 VirtualFS.Tests/Physical/PhysicalFileSystemTests.cs create mode 100644 VirtualFS.Tests/RootFileSystemTest.cs create mode 100644 VirtualFS.Tests/VirtualFS.Tests.csproj create mode 100644 VirtualFS.sln create mode 100644 VirtualFS/Base/BaseFileSystem.cs create mode 100644 VirtualFS/Compressed/ZipReadFileSystem.cs create mode 100644 VirtualFS/Directory.cs create mode 100644 VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs create mode 100644 VirtualFS/Entry.cs create mode 100644 VirtualFS/Extensions/Validators.cs create mode 100644 VirtualFS/File.cs create mode 100644 VirtualFS/IFileSystem.cs create mode 100644 VirtualFS/IRootFileSystem.cs create mode 100644 VirtualFS/Implementation/RootFileSystem.cs create mode 100644 VirtualFS/Implementation/VirtualStream.cs create mode 100644 VirtualFS/Libraries/ICSharpCode.SharpZipLib.dll create mode 100644 VirtualFS/Memory/MemoryFileSystem.cs create mode 100644 VirtualFS/Memory/MemoryWriteVirtualStream.cs create mode 100644 VirtualFS/Path.cs create mode 100644 VirtualFS/Physical/PhysicalFileSystem.cs create mode 100644 VirtualFS/VirtualFS.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecc8fa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# ---> core +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn +*/[Bb]in/ +*/[Oo]bj/ +*/[Oo]ut/ + +# Visual Studio 2015 +.vs/ + +# Logs +[Ll]og-*.txt +[Ll]ogs/ + +# Other +appsettings.Development.json +configurationCache.bin +configurationCache.bin.bak +*.cache \ No newline at end of file diff --git a/VirtualFS.Tests/PathTests.cs b/VirtualFS.Tests/PathTests.cs new file mode 100644 index 0000000..f822fd6 --- /dev/null +++ b/VirtualFS.Tests/PathTests.cs @@ -0,0 +1,47 @@ +namespace VirtualFS.Tests +{ + [TestClass] + public class PathTests + { + [TestMethod] + public virtual void TestPathCast() + { + Path p = "/test/path"; + + Assert.IsNotNull(p); + Assert.AreEqual("/test/path", (string)p); + } + + [TestMethod] + public virtual void TestPathDirectoryNotDirectory() + { + Assert.IsFalse(((Path)"/test/path").IsDirectory == ((Path)"/test/path/").IsDirectory); + } + + [TestMethod] + public virtual void TestPathAddPath() + { + Assert.AreEqual((Path)"/test/path/SomeFile.txt", ((Path)"/test/path/") + "SomeFile.txt"); + } + + [TestMethod] + public virtual void TestPathExt() + { + Assert.AreEqual("txt", ((Path)"/test/path/SomeFile.txt").GetExtension()); + } + + [TestMethod] + public virtual void TestPathParent() + { + Assert.IsNull(new Path().Parent); + Assert.AreEqual(new Path(), ((Path)"/test/").Parent); + Assert.AreEqual((Path)"/test/", ((Path)"/test/path/").Parent); + } + + [TestMethod] + public virtual void TestInvalidPath() + { + Assert.ThrowsException(() => new Path("test")); + } + } +} diff --git a/VirtualFS.Tests/Physical/PhysicalFileSystemTests.cs b/VirtualFS.Tests/Physical/PhysicalFileSystemTests.cs new file mode 100644 index 0000000..2d473ac --- /dev/null +++ b/VirtualFS.Tests/Physical/PhysicalFileSystemTests.cs @@ -0,0 +1,14 @@ +using VirtualFS.Physical; + +namespace VirtualFS.Tests.Physical +{ + [TestClass] + public class PhysicalFileSystemTests + { + [TestMethod] + public void TestEnumerate() + { + Assert.IsTrue(new PhysicalFileSystem(new System.IO.DirectoryInfo("C:\\")).GetEntries("/").Count() > 0); + } + } +} \ No newline at end of file diff --git a/VirtualFS.Tests/RootFileSystemTest.cs b/VirtualFS.Tests/RootFileSystemTest.cs new file mode 100644 index 0000000..d1baaac --- /dev/null +++ b/VirtualFS.Tests/RootFileSystemTest.cs @@ -0,0 +1,44 @@ +using VirtualFS.Implementation; +using VirtualFS.Physical; + +namespace VirtualFS.Tests +{ + [TestClass] + public class RootFileSystemTest + { + private RootFileSystem _root; + private string _realRootPath = "C:\\Temp\\"; + + [TestInitialize] + public virtual void SetUp() + { + if (!System.IO.Directory.Exists(_realRootPath)) + System.IO.Directory.CreateDirectory(_realRootPath); + + _root = new RootFileSystem(); + _root.Mount(new PhysicalFileSystem(new System.IO.DirectoryInfo(_realRootPath)), (Path)"/"); + } + + [TestMethod] + public virtual void TestEnumerate() + { + var root = new RootFileSystem(); + root.Mount(new PhysicalFileSystem(new System.IO.DirectoryInfo("C:\\")), (Path)"/"); + root.Mount(new PhysicalFileSystem(new System.IO.DirectoryInfo("C:\\")), (Path)"/"); + + Assert.AreEqual(new PhysicalFileSystem(new System.IO.DirectoryInfo("C:\\")).GetEntries("/").Count(), root.GetEntries("/").Count()); + } + + [TestMethod] + public virtual void TestDirectoryCreateAndDelete() + { + var dir = _root.Root.Create("Test"); + + Assert.IsTrue(System.IO.Directory.Exists(_realRootPath + "Test\\")); + + dir.Delete(); + + Assert.IsFalse(System.IO.Directory.Exists(_realRootPath + "Test\\")); + } + } +} \ No newline at end of file diff --git a/VirtualFS.Tests/VirtualFS.Tests.csproj b/VirtualFS.Tests/VirtualFS.Tests.csproj new file mode 100644 index 0000000..151af00 --- /dev/null +++ b/VirtualFS.Tests/VirtualFS.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/VirtualFS.sln b/VirtualFS.sln new file mode 100644 index 0000000..de4a1dc --- /dev/null +++ b/VirtualFS.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualFS", "VirtualFS\VirtualFS.csproj", "{1FB8AADC-9568-41A3-AD8E-6181E028A80B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualFS.Tests", "VirtualFS.Tests\VirtualFS.Tests.csproj", "{D5013B4E-8A1B-4DBB-8FB5-E09935F4F764}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1FB8AADC-9568-41A3-AD8E-6181E028A80B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FB8AADC-9568-41A3-AD8E-6181E028A80B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FB8AADC-9568-41A3-AD8E-6181E028A80B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FB8AADC-9568-41A3-AD8E-6181E028A80B}.Release|Any CPU.Build.0 = Release|Any CPU + {D5013B4E-8A1B-4DBB-8FB5-E09935F4F764}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5013B4E-8A1B-4DBB-8FB5-E09935F4F764}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5013B4E-8A1B-4DBB-8FB5-E09935F4F764}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5013B4E-8A1B-4DBB-8FB5-E09935F4F764}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {22781EB3-2148-4CA4-845A-B55265A7B5C2} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + StartupItem = Tester\Tester.csproj + EndGlobalSection +EndGlobal diff --git a/VirtualFS/Base/BaseFileSystem.cs b/VirtualFS/Base/BaseFileSystem.cs new file mode 100644 index 0000000..f560821 --- /dev/null +++ b/VirtualFS/Base/BaseFileSystem.cs @@ -0,0 +1,391 @@ +/* + * 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.Text.RegularExpressions; + +namespace VirtualFS.Base +{ + /// Basic abstract implementation of file system. + public abstract class BaseFileSystem : IFileSystem + { + private object _lock = new object(); + + private volatile int _openCount = 0; + + /// + /// Initializes a new instance of the class. + /// + /// If set to true file system and + /// all it's files/directories will be treated as read only. + public BaseFileSystem(bool readOnly) + { + IsReadOnly = readOnly; + HighPriority = false; + } + + /// Gets reference to root file system. + public virtual IRootFileSystem RootFileSystem { get; internal set; } + + /// 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; } + + /// + /// Gets a value indicating whether this file system is read only. + /// + public virtual bool IsReadOnly { get; private set; } + + /// Gets a value indicating whether this file system is busy. + /// Implementations of file systems should set this flag if any + /// file is open in this file system or wrap stream into + /// . + public virtual bool IsBusy + { + get + { + bool res = false; + + lock (_lock) + res = _openCount > 0; + + return res; + } + } + + /// + /// Check if given path exists. + /// + /// Path to check. + /// + /// Returns true if entry does + /// exist, otherwise false. + /// + public abstract bool Exists(Path path); + + /// + /// Get entry located under given path. + /// + /// Path to get. + /// + /// Returns entry information. + /// + public abstract Entry GetEntry(Path path); + + /// 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 abstract string EntryRealPath(Path path); + + /// Copies an existing file or directory to a new location. + /// Source path of file or directory. + /// Destination path. This path must ba a directory. + /// If true the destination files and directories + /// can be overwritten; otherwise, false. + /// This method uses simple yet universal approach. + /// it's much slower than native one so implementing file system consider + /// better approach for internal copy. + public virtual void Copy(Path source, Path destination, bool overwrite = false) + { + InternalCopy(source, destination, overwrite); + } + + /// Moves an existing file or directory to a new location. + /// Source path of file or directory. + /// Destination path. This path must ba a directory. + /// If true the destination files and directories + /// can be overwritten; otherwise, false. Files that aren't moved + /// will remain on file system. + /// If true the source directories will be left intact; + /// otherwise, false. + /// This method uses simple yet universal approach. + /// it's much slower than native one so implementing file system consider + /// better approach for internal move. + public virtual void Move(Path source, Path destination, bool overwrite = false, bool leaveStructure = false) + { + InternalCopy(source, destination, overwrite, (s, d) => + { + if (!s.IsDirectory) + FileDelete(s); + else if (s.IsDirectory && !leaveStructure) + try + { + DirectoryDelete(s, false); + } + catch (IOException) + { + // We swallow IOException, cause it's suggests that directory contains files + } + }); + } + + private void InternalCopy(Path source, Path destination, bool overwrite, Action copySuccess = null) + { + if (source == destination) + return; + + if (!destination.IsDirectory) + throw new InvalidOperationException(string.Format("Can't copy to non directory path ('{0}').", destination)); + + if (!source.IsDirectory) + InternalCopySingleFile(source, destination, overwrite, copySuccess); + else + { + Queue> cpy = new Queue>(); + Stack> del = new Stack>(); + + if (!Exists(destination)) + DirectoryCreate(destination); + + cpy.Enqueue(new Tuple(GetEntry(source) as Directory, GetEntry(destination) as Directory)); + + while (cpy.Count > 0) + { + var dir = cpy.Dequeue(); + + if (dir != null) + { + if (copySuccess != null) + del.Push(dir); + + foreach (var e in dir.Item1.GetEntries()) + { + if (e is Directory) + { + Directory np = null; + Path newDir = dir.Item2.Path + e.Path.Name; + + if (!Exists(newDir)) + np = DirectoryCreate(newDir); + + cpy.Enqueue(new Tuple(e as Directory, np ?? GetEntry(newDir) as Directory)); + } + else + InternalCopySingleFile(e.Path, dir.Item2.Path, overwrite, copySuccess); + } + } + } + + if (copySuccess != null) + { + Tuple item = null; + while ((item = del.Pop()) != null) + copySuccess(item.Item1.Path, item.Item2.Path); + } + } + } + + private void InternalCopySingleFile(Path source, Path destination, bool overwrite, Action copySuccess) + { + var dst = destination.Append(source.Name); + + if (!Exists(source)) + throw new InvalidOperationException("Source file doesn't exist."); + + if (!overwrite && Exists(dst)) + throw new InvalidOperationException("Destination file already exist."); + + Directory dir = (Directory)GetEntry(destination); + + if (dir.IsReadOnly || dir.FileSystem.IsReadOnly) + throw new InvalidOperationException("Can't copy on read only file system."); + + dir + .FileCreate(source.Name) + .WriteAllBytes(((File)GetEntry(source)).ReadAllBytes()); + + if (copySuccess != null) + copySuccess(source, destination.Append(source.Name)); + } + + /// Change name of the entry under specified path. + /// The path to entry which name will be changed. + /// The new name of entry. + public virtual void ReName(Path path, string newName) + { + if (path.Name == newName) + return; + + if (IsReadOnly) + throw new InvalidOperationException(string.Format("Can't rename file from '{0}' to '{1}' on read only file system.", path, newName)); + + if (!Exists(path)) + throw new InvalidOperationException(string.Format("Can't rename non existing entry '{0}' to '{1}'.", path, newName)); + } + + /// + /// Get entries located under given path. + /// + /// Path to get. + /// Mask to filter out unwanted entries. + /// + /// Returns entry information. + /// + public abstract IEnumerable GetEntries(Path path, Regex mask = null); + + /// + /// Create Directory and return new entry. + /// + /// Path of directory. + /// + /// Created entry. + /// + public virtual Directory DirectoryCreate(Path path) + { + if (IsReadOnly) + throw new InvalidOperationException(string.Format("Can't create directory ('{0}') on read only file system.", path)); + + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't create directory from file path ('{0}').", path)); + + if (path.Parent == null) + throw new InvalidOperationException("Can't create root directory of a file system."); + + if (!Exists(path.Parent)) + throw new InvalidOperationException("Can't create entire directory path."); + + if (Exists(path)) + throw new InvalidOperationException("Can't create directory that already exist."); + + 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 virtual void DirectoryDelete(Path path, bool recursive = false) + { + if (IsReadOnly) + throw new InvalidOperationException(string.Format("Can't delete directory ('{0}') on read only file system.", path)); + + if (path == Path.Root) + throw new InvalidOperationException(string.Format("Can't delete root directory.", path)); + + if (!path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't delete directory from file path ('{0}').", path)); + + if (!Exists(path)) + throw new InvalidOperationException("Can't delete directory that doesn't exist."); + + if (!recursive && GetEntries(path).Any()) + 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. + /// 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 (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."); + + 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 virtual void FileDelete(Path path) + { + if (path.IsDirectory) + throw new InvalidOperationException(string.Format("Can't delete directory path ('{0}') as file.", path)); + + if (IsReadOnly) + throw new InvalidOperationException(string.Format("Can't delete file ('{0}') from read only file system.", path)); + + if (!Exists(path)) + throw new InvalidOperationException("Can't delete not existing file."); + } + + internal void IncreaseOpened() + { + lock (_lock) + _openCount++; + } + + internal void DecreaseOpened() + { + lock (_lock) + _openCount--; + } + + /// Performs application-defined tasks associated with + /// freeing, releasing, or resetting unmanaged resources. + public virtual void Dispose() + { + } + } +} \ No newline at end of file diff --git a/VirtualFS/Compressed/ZipReadFileSystem.cs b/VirtualFS/Compressed/ZipReadFileSystem.cs new file mode 100644 index 0000000..7fc7b08 --- /dev/null +++ b/VirtualFS/Compressed/ZipReadFileSystem.cs @@ -0,0 +1,228 @@ +/* + * 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.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using ICSharpCode.SharpZipLib.Zip; +using VirtualFS.Base; +using VirtualFS.Implementation; + +namespace VirtualFS.Compressed +{ + /// Zip file based read only file system implementation. + public class ZipReadFileSystem : BaseFileSystem + { + /// Gets the Zip file. + public ZipFile File { get; private set; } + + private Dictionary _pathToEntry; + + /// Gets the assembly on which file system operates. + public Assembly Assembly { get; private set; } + + /// Initializes a new instance of the class. + /// The stream with zip file. + /// The root path. + /// The file filter. + public ZipReadFileSystem(System.IO.Stream stream, Path root = null, Regex filter = null) + : base(true) + { + File = new ZipFile(stream); + + foreach (ZipEntry zipEntry in File) + { + if (filter == null || filter.IsMatch(zipEntry.Name)) + { + if (zipEntry.IsDirectory) + { + var path = ZipEntryToPath(zipEntry, root, true); + + if (path != null) + _pathToEntry.Add(path, new Directory + { + Data = zipEntry, + Path = path, + FileSystem = this, + IsReadOnly = true, + CreationTime = zipEntry.DateTime, + LastAccessTime = zipEntry.DateTime, + LastWriteTime = zipEntry.DateTime, + }); + } + else if (zipEntry.IsFile) + { + var path = ZipEntryToPath(zipEntry, root); + + if (path != null) + _pathToEntry.Add(path, new File + { + Data = zipEntry, + Path = path, + FileSystem = this, + IsReadOnly = true, + CreationTime = zipEntry.DateTime, + LastAccessTime = zipEntry.DateTime, + LastWriteTime = zipEntry.DateTime, + }); + } + } + } + + // Ugly but it works (fix this - performance) + _pathToEntry + .Keys + .Where(p => !p.IsDirectory) + .ToList() + .ForEach(p => + { + var x = p.Parent; + while (x != null) + { + Exists(x); + x = x.Parent; + } + }); + } + + private static Path ZipEntryToPath(ZipEntry zipEntry, Path root, bool dir = false) + { + var ret = zipEntry.Name.Replace('\\', Path.SeparatorChar); + if (ret[0] != Path.SeparatorChar) + ret = Path.SeparatorChar + ret; + + if (dir && !ret.EndsWith(Path.SeparatorChar.ToString())) + ret += Path.SeparatorChar; + + if (root != null) + { + if (root.IsParentOf(ret)) + ret = ((Path)ret).RemoveParent(root); + else + return null; + } + + return ret; + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + bool ret = path == Path.Root || + (_pathToEntry.ContainsKey(path) && + ((_pathToEntry[path] is Directory) == path.IsDirectory)); + + if (!ret && _pathToEntry.Keys.Any(x => x.IsParentOf(path))) + { + _pathToEntry.Add(path, new Directory() + { + Data = null, + IsReadOnly = IsReadOnly, + Path = path, + FileSystem = this, + CreationTime = DateTime.Now, + LastAccessTime = DateTime.Now, + LastWriteTime = DateTime.Now, + }); + + ret = true; + } + + return ret; + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + if (!Exists(path)) + return null; + + return _pathToEntry[path]; + } + + /// 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) + { + return null; + } + + /// 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.")); + + if (Exists(path)) + return _pathToEntry + .Where(k => k.Key.Parent == path) + .Where(k => mask == null || mask.IsMatch(k.Key.Name)) + .Select(v => v.Value); + + return null; + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + public override System.IO.Stream FileOpenRead(Path path) + { + base.FileOpenRead(path); + + return new VirtualStream(this, File.GetInputStream((ZipEntry)_pathToEntry[path].Data)); + } + + /// Performs application-defined tasks associated with + /// freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + if (File != null) + { + // Makes close also shut the underlying stream + File.IsStreamOwner = true; + + // Ensure we release resources + File.Close(); + } + } + } +} \ No newline at end of file diff --git a/VirtualFS/Directory.cs b/VirtualFS/Directory.cs new file mode 100644 index 0000000..f59ef4a --- /dev/null +++ b/VirtualFS/Directory.cs @@ -0,0 +1,123 @@ +/* + * 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.Text.RegularExpressions; +using VirtualFS.Extensions; + +namespace VirtualFS +{ + /// Class representing directory entry. + public class Directory : Entry + { + /// + /// Initializes a new instance of the class. + /// + internal Directory() + { + IsDirectory = true; + } + + /// Enumerate entries 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 IEnumerable GetEntries(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.GetEntries(FullPath, mask) : + FileSystem.GetEntries(Path, mask); + } + + /// 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) + { + return !forceLocal && FileSystem.RootFileSystem != null ? + FileSystem.RootFileSystem.DirectoryCreate(FullPath.Append(string.Format("{0}{1}", + name.Validated("name", invalidChars: Path.InvalidFileChars, invalidSeq: Path.InvalidPathSequences), + Path.SeparatorChar))) : + FileSystem.DirectoryCreate(Path.Append(string.Format("{0}{1}", + name.Validated("name", invalidChars: Path.InvalidFileChars, invalidSeq: Path.InvalidPathSequences), + Path.SeparatorChar))); + } + + /// Deletes the specified directory and, if + /// indicated, any subdirectories in the directory. + /// Set true to remove directories, + /// 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) + { + if (!forceLocal && FileSystem.RootFileSystem != null) + FileSystem.RootFileSystem.DirectoryDelete(FullPath, recursive); + else + FileSystem.DirectoryDelete(Path, recursive); + } + + /// Creates or overwrites a file in the specified path. + /// The of the file to create. + /// Force usage of local file + /// system, to which directory is bound. + /// Created file is empty. + /// The entry information. + public File FileCreate(string name, bool forceLocal = false) + { + if (!forceLocal && FileSystem.RootFileSystem != null) + { + var path = FullPath.Append( + name.Validated("name", invalidChars: Path.InvalidFileChars, invalidSeq: Path.InvalidPathSequences)); + + using (var stream = FileSystem.RootFileSystem.FileCreate(path)) + stream.Close(); + + return FileSystem.RootFileSystem.GetEntry(path) as File; + } + else + { + var path = Path.Append( + name.Validated("name", invalidChars: Path.InvalidFileChars, invalidSeq: Path.InvalidPathSequences)); + + using (var stream = FileSystem.FileCreate(path)) + stream.Close(); + + return FileSystem.GetEntry(path) as File; + } + } + } +} \ No newline at end of file diff --git a/VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs b/VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs new file mode 100644 index 0000000..0b75345 --- /dev/null +++ b/VirtualFS/EmbeddedResource/EmbeddedResourceFileSystem.cs @@ -0,0 +1,215 @@ +/* + * 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.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using VirtualFS.Base; +using VirtualFS.Implementation; + +namespace VirtualFS.EmbeddedResource +{ + /// Embedded resource file system implementation. + public class EmbeddedResourceFileSystem : BaseFileSystem + { + private static object _lock = new object(); + + private Dictionary _pathToResource; + + /// Gets the assembly on which file system operates. + public Assembly Assembly { get; private set; } + + /// Initializes a new instance of the class. + /// The assembly. + /// The root namespace. + /// The filter. + public EmbeddedResourceFileSystem(Assembly assembly, string rootNamespace = null, Regex filter = null) + : base(true) + { + Assembly = assembly; + + var fi = new System.IO.FileInfo(Assembly.Location); + + _pathToResource = Assembly.GetManifestResourceNames() + .Where(x => (string.IsNullOrEmpty(rootNamespace) || x.StartsWith(rootNamespace)) && (filter == null || filter.IsMatch(x))) + .Select(x => new File() + { + Data = x, + IsReadOnly = IsReadOnly, + Path = ResourceToPath(rootNamespace, x), + FileSystem = this, + CreationTime = fi.CreationTimeUtc, + LastAccessTime = fi.LastAccessTime, + LastWriteTime = fi.LastWriteTime, + }) + .ToDictionary( + k => k.Path, + v => (Entry)v); + + _pathToResource + .Select(p => p.Key) + .Where(p => ((string)p).LastIndexOf(Path.SeparatorChar) > 0) + .Select(p => (Path)((string)p).Substring(0, ((string)p).LastIndexOf(Path.SeparatorChar) + 1)) + .Distinct() + .ToList() + .ForEach(p => _pathToResource.Add(p, new Directory() + { + Data = null, + IsReadOnly = IsReadOnly, + Path = p, + FileSystem = this, + CreationTime = fi.CreationTimeUtc, + LastAccessTime = fi.LastAccessTime, + LastWriteTime = fi.LastWriteTime, + })); + + // Ugly but it works (fix this - performance) + _pathToResource + .Keys + .Where(p => !p.IsDirectory) + .ToList() + .ForEach(p => + { + var x = p.Parent; + while (x != null) + { + Exists(x); + x = x.Parent; + } + }); + } + + private static Path ResourceToPath(string rootNamespace, string resName) + { + string ret = resName; + + int extDot = 0; + if (!string.IsNullOrEmpty(rootNamespace)) + ret = ret.Substring(rootNamespace.Length).TrimStart('.'); + + extDot = ret.LastIndexOf('.'); + ret = ret.Replace('.', Path.SeparatorChar); + if (extDot >= 0) + { + ret = ret + .Remove(extDot, 1) + .Insert(extDot, "."); + } + + return (Path)ret.Insert(0, Path.SeparatorChar.ToString()); + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + bool ret = false; + + lock (_lock) + { + ret = path == Path.Root || + (_pathToResource.ContainsKey(path) && + ((_pathToResource[path] is Directory) == path.IsDirectory)); + + if (!ret && path.IsDirectory && _pathToResource.Keys.Any(x => x.IsParentOf(path))) + { + var fi = new System.IO.FileInfo(Assembly.Location); + + _pathToResource.Add(path, new Directory() + { + Data = null, + IsReadOnly = IsReadOnly, + Path = path, + FileSystem = this, + CreationTime = fi.CreationTimeUtc, + LastAccessTime = fi.LastAccessTime, + LastWriteTime = fi.LastWriteTime, + }); + + ret = true; + } + } + + return ret; + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + if (!Exists(path)) + return null; + + return _pathToResource[path]; + } + + /// 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) + { + return this.Assembly.Location; + } + + /// 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.")); + + if (Exists(path)) + return _pathToResource + .Where(k => k.Key.Parent == path) + .Where(k => mask == null || mask.IsMatch(k.Key.Name)) + .Select(v => v.Value); + + return null; + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + public override System.IO.Stream FileOpenRead(Path path) + { + base.FileOpenRead(path); + + return new VirtualStream(this, Assembly.GetManifestResourceStream(_pathToResource[path].Data.ToString())); + } + } +} \ No newline at end of file diff --git a/VirtualFS/Entry.cs b/VirtualFS/Entry.cs new file mode 100644 index 0000000..482ade2 --- /dev/null +++ b/VirtualFS/Entry.cs @@ -0,0 +1,99 @@ +/* + * 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; + +namespace VirtualFS +{ + /// Basic file system entry. + public class Entry + { + /// Gets file system on which entry is located. + public virtual IFileSystem FileSystem { get; internal set; } + + /// Gets a value indicating whether this entry is read only. + public virtual bool IsReadOnly { get; internal set; } + + /// Gets a value indicating whether this is a directory entry. + public virtual bool IsDirectory { get; internal set; } + + /// Gets path to entry on local file system. + public virtual Path Path { get; internal set; } + + /// Gets full path to entry including mount point. + public Path FullPath { get { return (FileSystem == null || FileSystem.RootFileSystem == null) ? null : Path.AddParent(FileSystem.RootFileSystem.GetMountPath(FileSystem)); } } + + /// Gets or sets the creation time, in coordinated universal time (UTC), of the current file or directory. + public virtual DateTime CreationTime { get; set; } + + /// Gets or sets the time, in coordinated universal time (UTC), that the current file or directory was last accessed. + public virtual DateTime LastAccessTime { get; set; } + + /// Gets or sets the time, in coordinated universal time (UTC), when the current file or directory was last written to. + public DateTime LastWriteTime { get; set; } + + /// Gets or sets additional internal data for file system entry. + internal object Data { get; set; } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// Returns true if the specified + /// is equal to this instance; otherwise, false. + public override bool Equals(object obj) + { + if (obj is Entry) + return Equals((Entry)obj); + + return false; + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// Returns true if the specified is equal + /// to this instance; otherwise, false. + public bool Equals(Entry other) + { + return other.Path.Equals(Path); + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() + { + return Path.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/VirtualFS/Extensions/Validators.cs b/VirtualFS/Extensions/Validators.cs new file mode 100644 index 0000000..16b352e --- /dev/null +++ b/VirtualFS/Extensions/Validators.cs @@ -0,0 +1,128 @@ +/* + * 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; + +namespace VirtualFS.Extensions +{ + internal static class Validators + { + /// + /// Returns a new validated string using the rules given. + /// + /// 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 to trim the returned string. + /// True to left-trim the returned string. + /// True to right-trim the returned string. + /// If >= 0, the min valid length for the returned string. + /// If >= 0, the max valid length for the returned string. + /// If not '\0', the character to use to left-pad the returned string if needed. + /// If not '\0', the character to use to right-pad the returned string if needed. + /// If not null, an array containing invalid chars that must not appear in the returned + /// string. + /// If not null, an array containing the only characters that are considered valid for the + /// returned string. + /// 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 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) + { + // Assuring a valid descriptor... + if (string.IsNullOrWhiteSpace(desc)) desc = "Source"; + + // Validating if null sources are accepted... + if (source == null) + { + if (!canbeNull) throw new ArgumentNullException(desc, string.Format("{0} cannot be null.", desc)); + return null; + } + + // Trimming if needed... + if (trim && !(trimStart || trimEnd)) source = source.Trim(); + else + { + if (trimStart) source = source.TrimStart(' '); + if (trimEnd) source = source.TrimEnd(' '); + } + + // Adjusting lenght... + if (minLen > 0) + { + if (padLeft != '\0') source = source.PadLeft(minLen, padLeft); + if (padRight != '\0') source = source.PadRight(minLen, padRight); + } + + if (maxLen > 0) + { + if (padLeft != '\0') source = source.PadLeft(maxLen, padLeft); + if (padRight != '\0') source = source.PadRight(maxLen, padRight); + } + + // Validating emptyness and lenghts... + if (source.Length == 0) + { + if (!canbeEmpty) throw new ArgumentException(string.Format("{0} cannot be empty.", desc)); + return string.Empty; + } + + if (minLen >= 0 && source.Length < minLen) throw new ArgumentException(string.Format("Lenght of {0} '{1}' is lower than '{2}'.", desc, source, minLen)); + if (maxLen >= 0 && source.Length > maxLen) throw new ArgumentException(string.Format("Lenght of {0} '{1}' is bigger than '{2}'.", desc, source, maxLen)); + + // Checking invalid chars... + if (invalidChars != null) + { + int n = source.IndexOfAny(invalidChars); + if (n >= 0) throw new ArgumentException(string.Format("Invalid character '{0}' found in {1} '{2}'.", source[n], desc, source)); + } + + // Checking valid chars... + if (validChars != null) + { + int n = validChars.ToString().IndexOfAny(source.ToCharArray()); + if (n >= 0) + throw new ArgumentException(string.Format("Invalid character '{0}' found in {1} '{2}'.", validChars.ToString()[n], desc, source)); + } + + // Checking invalid sequences + if (invalidSeq != null) + { + foreach (var seq in invalidSeq) + if (source.IndexOf(seq) >= 0) + throw new ArgumentException(string.Format("Invalid sequence '{0}' found in {1} '{2}'.", seq, desc, source)); + } + + return source; + } + } +} \ No newline at end of file diff --git a/VirtualFS/File.cs b/VirtualFS/File.cs new file mode 100644 index 0000000..5f9e23a --- /dev/null +++ b/VirtualFS/File.cs @@ -0,0 +1,207 @@ +/* + * 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.Collections.Generic; +using System.IO; +using System.Text; + +namespace VirtualFS +{ + /// File representation. + public class File : Entry + { + private static UTF8Encoding _utfNoBOM = new UTF8Encoding(false, true); + + /// Opens an existing file for reading. + /// Force usage of local file + /// system, to which directory is bound. + /// A read-only on the specified path. + public Stream OpenRead(bool forceLocal = false) + { + if (!forceLocal && FileSystem.RootFileSystem != null) + return FileSystem.RootFileSystem.FileOpenRead(FullPath); + else + return FileSystem.FileOpenRead(Path); + } + + /// Opens an existing file for writing. + /// Force usage of local file + /// system, to which directory is bound. + /// An unshared object on + /// the specified path with write access. + public Stream OpenWrite(bool forceLocal = false) + { + if (!forceLocal && FileSystem.RootFileSystem != null) + return FileSystem.RootFileSystem.FileOpenWrite(FullPath); + else + return FileSystem.FileOpenWrite(Path); + } + + /// Deletes the specified file. An exception is thrown + /// if the specified file does not exist. + /// Force usage of local file + /// system, to which directory is bound. + public void Delete(bool forceLocal = false) + { + if (!forceLocal && FileSystem.RootFileSystem != null) + FileSystem.RootFileSystem.FileDelete(FullPath); + else + FileSystem.FileDelete(Path); + } + + #region Utility + + /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. + /// A byte array containing the contents of the file. + public byte[] ReadAllBytes() + { + byte[] array; + + 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) + { + int read = fileStream.Read(array, offset, len); + if (read == 0) + throw new EndOfStreamException("Can't read beyond file stream."); + + offset += read; + len -= read; + } + } + + return array; + } + + /// Opens a file, reads all lines of the file with the specified encoding, and then closes the file. + /// The encoding applied to the contents of the file. + /// A string array containing all lines of the file. + public string[] ReadAllLines(Encoding encoding = null) + { + if (encoding == null) + encoding = Encoding.UTF8; + + List list = new List(); + + using (Stream fileStream = OpenRead(true)) + using (StreamReader streamReader = new StreamReader(fileStream, encoding)) + { + string item; + while ((item = streamReader.ReadLine()) != null) + { + list.Add(item); + } + } + + return list.ToArray(); + } + + /// Opens a file, reads all lines of the file with the specified encoding, and then closes the file. + /// The encoding applied to the contents of the file. + /// A string containing all lines of the file. + public string ReadAllText(Encoding encoding = null) + { + if (encoding == null) + encoding = Encoding.UTF8; + + using (Stream fileStream = OpenRead(true)) + using (StreamReader streamReader = new StreamReader(fileStream, encoding, true, 1024)) + return streamReader.ReadToEnd(); + } + + /// Writes the specified byte array to the file replacing it's current content, + /// and then closes the file. + /// The bytes to write to the file. + public void WriteAllBytes(byte[] bytes) + { + using (Stream fileStream = FileSystem.FileCreate(Path)) + fileStream.Write(bytes, 0, bytes.Length); + } + + /// Write the specified string array to the file replacing it's current + /// content, and then closes the file. + /// The string array to write to the file. + public void WriteAllLines(string[] contents) + { + using (Stream fileStream = FileSystem.FileCreate(Path)) + using (var writer = new StreamWriter(fileStream, _utfNoBOM)) + foreach (var item in contents) + writer.WriteLine(item); + } + + /// Writes the specified string to the file replacing it's current content, and + /// then closes the file. + /// The string to write to the file. + public void WriteAllText(string contents) + { + using (Stream fileStream = FileSystem.FileCreate(Path)) + using (var writer = new StreamWriter(fileStream, _utfNoBOM)) + writer.Write(contents); + } + + /// Opens a file, appends the specified byte array to + /// the file, and then closes the file. + /// The bytes to write to the file. + public void AppendAllBytes(byte[] bytes) + { + using (Stream fileStream = OpenWrite(true)) + fileStream.Write(bytes, 0, bytes.Length); + } + + /// Opens a file, appends the specified string array to + /// the file, and then closes the file. + /// The string array to write to the file. + public void AppendAllLines(string[] contents) + { + using (Stream fileStream = OpenWrite(true)) + using (var writer = new StreamWriter(fileStream, _utfNoBOM)) + foreach (var item in contents) + writer.WriteLine(item); + } + + /// Opens a file, appends the specified string to the file, and + /// then closes the file. + /// The string to write to the file. + public void AppendAllText(string contents) + { + using (Stream fileStream = OpenWrite(true)) + using (var writer = new StreamWriter(fileStream, _utfNoBOM)) + writer.Write(contents); + } + + #endregion Utility + } +} \ No newline at end of file diff --git a/VirtualFS/IFileSystem.cs b/VirtualFS/IFileSystem.cs new file mode 100644 index 0000000..5decf55 --- /dev/null +++ b/VirtualFS/IFileSystem.cs @@ -0,0 +1,149 @@ +/* + * 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.Text.RegularExpressions; + +namespace VirtualFS +{ + /// Describes basic operations on file system. + /// All operations involving paths are considered + /// to include only local file system. + public interface IFileSystem : IDisposable + { + /// Gets reference to root file system. + IRootFileSystem RootFileSystem { get; } + + /// 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; } + + /// Gets a value indicating whether this file system is read only. + bool IsReadOnly { get; } + + /// Gets a value indicating whether this file system is busy. + /// Implementations of file systems should set this flag if any + /// file is open in this file system. + bool IsBusy { get; } + + #region Common + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + bool Exists(Path path); + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + Entry GetEntry(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. + /// Virtual file system path. + /// Real file system path or null if real + /// path doesn't exist. + string EntryRealPath(Path path); + + /// Copies an existing file or directory to a new location. + /// Source path of file or directory. + /// Destination path. This path must ba a directory. + /// If true the destination files and directories + /// can be overwritten; otherwise, false. + void Copy(Path source, Path destination, bool overwrite = false); + + /// Moves an existing file or directory to a new location. + /// Source path of file or directory. + /// Destination path. This path must ba a directory. + /// If true the destination files and directories + /// can be overwritten; otherwise, false. Files that aren't moved + /// will remain on file system. + /// If true the source directories will be left intact; + /// otherwise, false. + void Move(Path source, Path destination, bool overwrite = false, bool leaveStructure = false); + + /// Change name of the entry under specified path. + /// The path to entry which name will be changed. + /// The new name of entry. + void ReName(Path path, string newName); + + #endregion Common + + #region Directory + + /// Get entries located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + IEnumerable GetEntries(Path path, Regex mask = null); + + /// Create directory and return new entry. + /// Parent directory must exist. + /// The directory path to create. + /// Created entry. + Directory DirectoryCreate(Path 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. + void DirectoryDelete(Path path, bool recursive = false); + + #endregion Directory + + #region File + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + Stream FileOpenRead(Path path); + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// An unshared object on + /// the specified path with write access. + Stream FileOpenWrite(Path path); + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// A that provides + /// write access to the file specified in path. + Stream FileCreate(Path path); + + /// Deletes the specified file. An exception is not thrown + /// if the specified file does not exist. + /// The path of the file to be deleted. + void FileDelete(Path path); + + #endregion File + } +} \ No newline at end of file diff --git a/VirtualFS/IRootFileSystem.cs b/VirtualFS/IRootFileSystem.cs new file mode 100644 index 0000000..3d94e83 --- /dev/null +++ b/VirtualFS/IRootFileSystem.cs @@ -0,0 +1,69 @@ +/* + * 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.Collections.Generic; + +namespace VirtualFS +{ + /// Interface describing root file system. + public interface IRootFileSystem : IFileSystem + { + /// Gets root of the file system. + Directory Root { get; } + + /// Mount file system in given path. + /// Mounting another file system can overlap, you + /// can mount several file systems in a given directory. + /// Mount path can't be a file. You can mount file system + /// in file system in file system and so on... When opening + /// a file which might exist on any of those file systems + /// last mounted is taken to consideration first. + /// File system to mount. + /// Path on which mount new file system. + /// Returns true if file system was mounted + /// correctly, otherwise false. + bool Mount(IFileSystem fs, Path path); + + /// Un mounts file system from current file system. + /// All file systems mounted previously in path owned by + /// this file system will remain mounted and accessible. + /// File system to un mount. + /// Returns true if file system was un mounted + /// correctly, otherwise false. + bool Umount(IFileSystem fs); + + /// Enumerate mounted file systems. + /// Enumeration of mounted file systems. + IEnumerable GetMounted(); + + /// Get mount path of this file system. + /// File system to check. + /// Path to file system on root file system. + Path GetMountPath(IFileSystem fileSystem); + } +} \ No newline at end of file diff --git a/VirtualFS/Implementation/RootFileSystem.cs b/VirtualFS/Implementation/RootFileSystem.cs new file mode 100644 index 0000000..b0eba6b --- /dev/null +++ b/VirtualFS/Implementation/RootFileSystem.cs @@ -0,0 +1,482 @@ +/* + * 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.Text.RegularExpressions; +using VirtualFS.Base; + +namespace VirtualFS.Implementation +{ + /// + /// Root file system is a file system containing other file systems. + /// + /// + /// Root file system by it self is read only and uses '/' directory. + /// You can of course mount other file systems to root path. + /// + public class RootFileSystem : BaseFileSystem, IRootFileSystem + { + private static object _globalLock = new object(); + + private Dictionary> _mounts; + + /// Initializes a new instance of the class. + public RootFileSystem() + : base(true) + { + _mounts = new Dictionary>(); + + Root = new Directory + { + FileSystem = this, + IsReadOnly = true, + Path = new Path(), + }; + } + + #region IFileSystem implementation + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + // Find all filesystems containing this path + // If any of the file systems under this path + // contains file represented as shrter path, return true. + return _mounts + .Where(x => x.Key.IsParentOfOrSame(path)) + .Any(x => x.Value.Any(f => f.Exists(path.RemoveParent(x.Key)))); + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + // Find all filesystems containing this path + foreach (var kvp in _mounts + .Where(x => x.Key.IsParentOfOrSame(path))) + { + // Make path shorter (remove mount point) + var shortPath = path.RemoveParent(kvp.Key); + + // If find first file system that contains + // this file. + var fs = kvp.Value.FirstOrDefault(f => f.Exists(shortPath)); + + // If file system found open file + if (fs != null) + return fs.GetEntry(shortPath); + } + + // Sorry file not found + throw new System.IO.FileNotFoundException(string.Format("File '{0}' was not found under any of the mounted file systems.", path), path); + } + + /// Get path to physical file containing entry. + /// Virtual file system path. + /// Real file system path or null if real + /// path doesn't exist. + /// File system + /// containing parent directory was not found. + /// 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) + { + Path parent = path.Parent; + + 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.Exists(parent.RemoveParent(pfs.Path))); + + if (pathAndFs == null) + throw new InvalidOperationException("File system containing parent directory was not found."); + + return pathAndFs.FileSystem.EntryRealPath(path.RemoveParent(parent)); + } + + /// Copies an existing file or directory to a new location. + /// Source path of file or directory. + /// Destination path. This path must ba a directory. + /// If true the destination files and directories + /// can be overwritten; otherwise, false. + /// This method uses simple yet universal approach. + /// it's much slower than native one so implementing file system consider + /// better approach for internal copy. + public override void Copy(Path source, Path destination, bool overwrite = false) + { + Path srcParent = source.Parent; + Path dstParent = destination.Parent; + + var pathAndFsSrc = _mounts + .Where(x => x.Key.IsParentOfOrSame(srcParent)) + .SelectMany(v => v.Value, (k, v) => new { Path = k.Key, FileSystem = v }) + .FirstOrDefault(pfs => pfs.FileSystem.Exists(srcParent.RemoveParent(pfs.Path))); + + var pathAndFsDst = _mounts + .Where(x => x.Key.IsParentOfOrSame(dstParent)) + .SelectMany(v => v.Value, (k, v) => new { Path = k.Key, FileSystem = v }) + .FirstOrDefault(pfs => !pfs.FileSystem.IsReadOnly && pfs.FileSystem.Exists(dstParent.RemoveParent(pfs.Path))); + + if (pathAndFsSrc != null && pathAndFsDst != null && + pathAndFsSrc.Path == pathAndFsDst.Path && + pathAndFsSrc.FileSystem == pathAndFsDst.FileSystem) + { + pathAndFsSrc.FileSystem.Copy(source, destination, overwrite); + return; + } + + base.Copy(source, destination, overwrite); + } + + /// Moves an existing file or directory to a new location. + /// Source path of file or directory. + /// Destination path. This path must ba a directory. + /// If true the destination files and directories + /// can be overwritten; otherwise, false. Files that aren't moved + /// will remain on file system. + /// If true the source directories will be left intact; + /// otherwise, false. + /// This method uses simple yet universal approach. + /// it's much slower than native one so implementing file system consider + /// better approach for internal move. + public override void Move(Path source, Path destination, bool overwrite = false, bool leaveStructure = false) + { + Path srcParent = source.Parent; + Path dstParent = destination.Parent; + + var pathAndFsSrc = _mounts + .Where(x => x.Key.IsParentOfOrSame(srcParent)) + .SelectMany(v => v.Value, (k, v) => new { Path = k.Key, FileSystem = v }) + .FirstOrDefault(pfs => !pfs.FileSystem.IsReadOnly && pfs.FileSystem.Exists(srcParent.RemoveParent(pfs.Path))); + + var pathAndFsDst = _mounts + .Where(x => x.Key.IsParentOfOrSame(dstParent)) + .SelectMany(v => v.Value, (k, v) => new { Path = k.Key, FileSystem = v }) + .FirstOrDefault(pfs => !pfs.FileSystem.IsReadOnly && pfs.FileSystem.Exists(dstParent.RemoveParent(pfs.Path))); + + if (pathAndFsSrc != null && pathAndFsDst != null && + pathAndFsSrc.Path == pathAndFsDst.Path && + pathAndFsSrc.FileSystem == pathAndFsDst.FileSystem) + { + pathAndFsSrc.FileSystem.Move(source, destination, overwrite); + return; + } + + base.Move(source, destination, overwrite, leaveStructure); + } + + /// 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) + { + if (path.Name == newName) + return; + + Path parent = path.Parent; + + 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."); + + pathAndFs.FileSystem.ReName(path.RemoveParent(parent), newName); + } + + /// 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.")); + + return _mounts + .Where(x => x.Key.IsParentOfOrSame(path)) + .SelectMany(v => v.Value.Where(x => x.Exists(path.RemoveParent(v.Key))), + (k, v) => v.GetEntries(path.RemoveParent(k.Key))) + .SelectMany(e => e, (p, e) => e) + .Distinct(); + } + + /// Create Directory and return new entry. + /// Path of directory. + /// Created entry. + /// Can't create root directory. + /// or Writable file system containing parent directory was not found. + public override Directory DirectoryCreate(Path path) + { + Path parent = path.Parent; + + if (parent == null) + throw new InvalidOperationException("Can't create root directory."); + + 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.DirectoryCreate(path.RemoveParent(parent)); + } + + /// 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. + /// + /// Writable file system containing parent directory was not found. + /// + public override void DirectoryDelete(Path path, bool recursive = false) + { + Path parent = path.Parent; + + if (parent == null) + throw new InvalidOperationException(string.Format("Can't delete root directory.", 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."); + + pathAndFs.FileSystem.DirectoryDelete(path.RemoveParent(parent)); + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + /// File + /// system containing parent directory was not found. + public override Stream FileOpenRead(Path path) + { + 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.Exists(parent.RemoveParent(pfs.Path))); + + if (pathAndFs == null) + throw new InvalidOperationException("File system containing parent directory was not found."); + + return pathAndFs.FileSystem.FileOpenRead(path.RemoveParent(parent)); + } + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// 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) + { + 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.FileOpenWrite(path.RemoveParent(parent)); + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// 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) + { + 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.FileCreate(path.RemoveParent(parent)); + } + + /// Files the delete. + /// The path. + /// Writable + /// file system containing parent directory was not found. + public override void FileDelete(Path path) + { + 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."); + + pathAndFs.FileSystem.FileDelete(path.RemoveParent(parent)); + } + + #endregion IFileSystem implementation + + #region IRootFileSystem implementation + + /// Gets root of the file system. + public Directory Root { get; private set; } + + /// Mount file system in given path. + /// File system to mount. This should be + /// implementation of . + /// Path on which mount new file system. + /// Returns true if file system was mounted + /// correctly, otherwise false. + /// + /// Path '{0}' is not a directory. You can only mount + /// file system under directories. or File system is + /// already mounted. + /// Mounting another file system can overlap, you + /// can mount several file systems in a given directory. + /// Mount path can't be a file. You can mount file system + /// in file system in file system and so on... When opening + /// a file which might exist on any of those file systems + /// last mounted is taken to consideration first. + 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."); + + lock (_globalLock) + { + if (GetMounted().Contains(fs)) + throw new InvalidOperationException("File system is already mounted."); + + if (!_mounts.ContainsKey(path)) + _mounts.Add(path, new List()); + + _mounts[path].Insert(_mounts[path].Count(f => f.HighPriority), fs); + } + + return true; + } + + /// Un mounts file system from current file system. + /// File system to un mount. + /// Returns true if file system was un mounted + /// correctly, otherwise false. + /// All file systems mounted previously in path owned by + /// this file system will remain mounted and accessible. + public bool Umount(IFileSystem fs) + { + bool result = false; + + // If file system is busy fail. + if (!fs.IsBusy) + { + // Find all mount paths + var mountPaths = _mounts + .Where(x => x.Value.Contains(fs)) + .Select(x => x.Key) + .ToArray(); + + int num = mountPaths.Length; + + // If file system is not mounted fail. + if (num > 0) + for (int i = 0; i < num; i++) + { + _mounts[mountPaths[i]].Remove(fs); + + if (_mounts[mountPaths[i]].Count == 0) + _mounts.Remove(mountPaths[i]); + + result = true; + } + } + + return result; + } + + /// Enumerate mounted file systems. + /// Enumeration of mounted file systems. + public IEnumerable GetMounted() + { + return _mounts + .Values + .SelectMany(l => l); + } + + /// Get mount path of this file system. + /// File system to check. + /// Path to file system on root file system. + public Path GetMountPath(IFileSystem fileSystem) + { + var pfs = _mounts + .SelectMany(k => k.Value, (k, v) => new { Path = k.Key, FileSystem = v }) + .SingleOrDefault(f => f.FileSystem == fileSystem); + + return pfs == null ? null : pfs.Path; + } + + #endregion IRootFileSystem implementation + } +} \ No newline at end of file diff --git a/VirtualFS/Implementation/VirtualStream.cs b/VirtualFS/Implementation/VirtualStream.cs new file mode 100644 index 0000000..2d0f9d7 --- /dev/null +++ b/VirtualFS/Implementation/VirtualStream.cs @@ -0,0 +1,251 @@ +/* + * 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.IO; +using VirtualFS.Base; + +namespace VirtualFS.Implementation +{ + /// Provides a generic view of a sequence + /// of bytes in virtual file system. + public class VirtualStream : Stream + { + /// Gets the internal stream. + public Stream InternalStream { get; private set; } + + /// Gets the file system. + public IFileSystem FileSystem { get; private set; } + + /// Initializes a new instance of + /// the class. + /// The file system. + /// The internal stream. + internal VirtualStream(BaseFileSystem fileSystem, Stream stream) + { + fileSystem.IncreaseOpened(); + InternalStream = stream; + FileSystem = fileSystem; + } + + /// Closes the current stream and releases any resources + /// (such as sockets and file handles) associated with the current + /// stream. + /// This wrapped method will decrease number of opened + /// files in file system. + public override void Close() + { + ((BaseFileSystem)FileSystem).DecreaseOpened(); + InternalStream.Close(); + } + + #region Wrapped Properties + + /// + /// When overridden in a derived class, gets a value indicating whether the current stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { get { return InternalStream.CanRead; } } + + /// + /// When overridden in a derived class, gets a value indicating whether the current stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { get { return InternalStream.CanSeek; } } + + /// + /// Gets a value that determines whether the current stream can time out. + /// + /// A value that determines whether the current stream can time out. + public override bool CanTimeout { get { return InternalStream.CanTimeout; } } + + /// + /// When overridden in a derived class, gets a value indicating whether the current stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { get { return InternalStream.CanWrite; } } + + /// + /// When overridden in a derived class, gets the length in bytes of the stream. + /// + /// A long value representing the length of the stream in bytes. + public override long Length { get { return InternalStream.Length; } } + + /// + /// When overridden in a derived class, gets or sets the position within the current stream. + /// + /// The current position within the stream. + public override long Position { get { return InternalStream.Position; } set { InternalStream.Position = value; } } + + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to read before timing out. + public override int ReadTimeout { get { return InternalStream.ReadTimeout; } set { InternalStream.ReadTimeout = value; } } + + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to write before timing out. + public override int WriteTimeout { get { return InternalStream.WriteTimeout; } set { InternalStream.WriteTimeout = value; } } + + #endregion Wrapped Properties + + #region Wrapped Methods + + /// + /// Begins an asynchronous read operation. + /// + /// The buffer to read the data into. + /// The byte offset in at which to begin writing data read from the stream. + /// The maximum number of bytes to read. + /// An optional asynchronous callback, to be called when the read is complete. + /// A user-provided object that distinguishes this particular asynchronous read request from other requests. + /// + /// An that represents the asynchronous read, which could still be pending. + /// + public override System.IAsyncResult BeginRead(byte[] buffer, int offset, int count, System.AsyncCallback callback, object state) + { + return InternalStream.BeginRead(buffer, offset, count, callback, state); + } + + /// + /// Begins an asynchronous write operation. + /// + /// The buffer to write data from. + /// The byte offset in from which to begin writing. + /// The maximum number of bytes to write. + /// An optional asynchronous callback, to be called when the write is complete. + /// A user-provided object that distinguishes this particular asynchronous write request from other requests. + /// + /// An IAsyncResult that represents the asynchronous write, which could still be pending. + /// + public override System.IAsyncResult BeginWrite(byte[] buffer, int offset, int count, System.AsyncCallback callback, object state) + { + return InternalStream.BeginWrite(buffer, offset, count, callback, state); + } + + /// + /// Waits for the pending asynchronous read to complete. + /// + /// The reference to the pending asynchronous request to finish. + /// + /// The number of bytes read from the stream, between zero (0) and the number of bytes you requested. Streams return zero (0) only at the end of the stream, otherwise, they should block until at least one byte is available. + /// + public override int EndRead(System.IAsyncResult asyncResult) + { + return InternalStream.EndRead(asyncResult); + } + + /// + /// Ends an asynchronous write operation. + /// + /// A reference to the outstanding asynchronous I/O request. + public override void EndWrite(System.IAsyncResult asyncResult) + { + InternalStream.EndWrite(asyncResult); + } + + /// + /// When overridden in a derived class, clears all buffers for this + /// stream and causes any buffered data to be written to the underlying device. + /// + public override void Flush() + { + InternalStream.Flush(); + } + + /// + /// When overridden in a derived class, reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between and ( + - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + public override int Read(byte[] buffer, int offset, int count) + { + return InternalStream.Read(buffer, offset, count); + } + + /// + /// Reads a byte from the stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream. + /// + /// + /// The unsigned byte cast to an Int32, or -1 if at the end of the stream. + /// + public override int ReadByte() + { + return InternalStream.ReadByte(); + } + + /// + /// When overridden in a derived class, sets the position within the current stream. + /// + /// A byte offset relative to the parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + public override long Seek(long offset, SeekOrigin origin) + { + return InternalStream.Seek(offset, origin); + } + + /// + /// When overridden in a derived class, sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + public override void SetLength(long value) + { + InternalStream.SetLength(value); + } + + /// + /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies bytes from to the current stream. + /// The zero-based byte offset in at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + public override void Write(byte[] buffer, int offset, int count) + { + InternalStream.Write(buffer, offset, count); + } + + /// + /// Writes a byte to the current position in the stream and advances the position within the stream by one byte. + /// + /// The byte to write to the stream. + public override void WriteByte(byte value) + { + base.WriteByte(value); + } + + #endregion Wrapped Methods + } +} \ No newline at end of file diff --git a/VirtualFS/Libraries/ICSharpCode.SharpZipLib.dll b/VirtualFS/Libraries/ICSharpCode.SharpZipLib.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe643ebc638dfc94f1149528d0e9a91ce52dc353 GIT binary patch literal 200704 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

M z%nmJmlih&i&lMN)m6iP3QhC|r*5AHSE;@^Y@;_?HiKn2VDwL^oA-dG1Tj_w*0;@BJ z5ol9wQF_7@h_ir-Pi2!@BII}W07lu3w-zclp_`vnKYK9p$jKSPwUqwF1UT#90{Tx^DrldGy%Z*GIZK;^WvqLv%= zCTw-cN$MT3fojG#1YNzhgt1vQo-`c^E`o#s4b{wK2tammVbGaThpgI%10C82bY(%X zkf^}<@}&?G0wq_HWN^bpET#(5MJ%x}-y3IWFHE&ANQD5$3>paGUf4!avnh^ShzaZ8 ztcZ#q^_Wybcrc_Ir^LKz4I^&KdaUt7p8Xbk#! ze*LD4sUKjKL-SWG6lDzvL^oR}u!Ey2xhvz8 z)Eab~O&BcO^?MAJ0A+;k2W`+S<_`Tr2v=yM30G=|62`qDIfO8fB=607ceUENZCVYO z0w5SrnAlp<&<(Qvc?umtU$fpl%Tg7SBFQ4og8gDcw#Af7XFFTKnFljQ z#oIvy;?+2`4+x8z;Y)`~e;y~mx1_Wk823H2tAl|CK~-o{ugAKBTUa9&oHX8NWZA-H zIbj(roI@Bp_gJSL~WZ2^|rVu zb*bq#YsGE5!kR{Dr6F=+AJVa?)KEu?=3yYYNF0vQXc=3}7t#cl5-ia+Bxs9DSae03 zAc9c&xoJhW>f3^-c2?G*BkdKimweB;d$biz(i(!=n^Z_F?4^>vv zP;s$j=(3U;RK^&9Vp%E_*CegDP%dbL8?NOg7g)&OWSN!4N^xajt+>9*Am7;)hE@t| z*ZD5Jz3PIw<#ndauSmv1L1K%A#UfL4#kF!_iJ>J(X})jScQz*tF0E};7B*II*m{uB z*>YJ5uI9^GsfZFQ#kD2ZA8Rl!+9VrBe05``T)0^bTV!6zRW4zh0Nxl|6s0%ny zva$_wj{=NbJYp>zC6O%*NM*5AXJ2`FhDadGNaPUNZ;{EmWt_K%*@ccf0wdVfiRvT_#r_4PX^%Kk=)EiXtq)C~{lEO+{ zX9S#xkT53ggt3)Yb0?l6z$+c>MN;W=pv#O-rmBS5pAVe%bKY=Z2} z9(At{4FrUwEDQy^WV%LOw9D4$DixE(=Eqoae2vX`6YFX}?sK9j*W)KXKEFXok zPBCYBnvFTO7LBqBReM*OTmqcd;aM+^g(wwb(5j5z;Yf%Q49NH~Ze`K-K*tUchmNB* zAiTd1=P~qj#su@R+EQ2?ArcDJ9ty{DYb`7R-`;Y)%iWZKnvd`^2^F`uJAf7KF^hQJ z@brllRoaJ?RvJ8nJdO`3B3y*9D6i2C_WYX$zH>hgQPi|S<;|^am8|Sb(GMnCu(#Pb z)ZthcF|%dRA`b0H<0Y->CAxy^o8ZHV2iu5^0UfI<`Wr#DO`3`1=Ukf5tT$M(Fz!@| zDCV-EhoN`0$6kfREj=@wzqrJyO<`EEEA0cTstYy=~xb?C4G3kL9SqTk_A$3y+UUVdjar+>&mh}MMYV3@Hji4ihYaw$VD(+ z*!ZwG3r$^jxF8(pWgI>xW9aE@K3^(BCa>OrK}~hQNJ8mGKEHtNuf;6HXe=AM;L*}3 zvVyI&1)N>1V4rQFkSjB|xUx}NF03s{7&f?#&=}XvdRx}U<~K@j+n}rIqONi*4zpjI z$*mMiVMLZd4s$Y8Zb2}?b_v6@U=-Tw*CA%e&Nie24um00Jb)PGvNXht(QLhglO?LU z72DQdA+8S7W%FzjEQU)5z9t9B!8%s+BPN`g6$LPg*fk?Jo z$W_YgdFUw%Z?D-9*Ck)B2w>fMtmaqY61Ihk8NOC0PbFoN^Woc-oSiGKH&Sw{-gM|WIB{$%E!I&%a8&LJ={j@8Oh2SK@ z=wcRy!@QN^ThfF*r@?wl#o~f~PHYlU3sXk}tIXejgUPk6R{_?XoSES2T9EX z4_wp|y|=auu%<>{2{lOet=ySG%SbRnIy-oHFU$es0cY$~c15YjP8l}5_qgXJ6s$q8 zJjn>jR6R*rqSM6|CVa)F(Q4hU(lUUz$<77dq{`MVc5>-zsx+$i4-Ix(-Fcp!RtsES zj`nQ%?dHXUrl5G}G*vtXnTYWSVXOi2&|P~JvMMAMf)EHG>%UUK4tjQ_vbM2mh#?XH z+QC+k;ClW>3SA*U^|Roj)orj1uN;oxiN6H8PaA3!(~woW0StjwEOh)Qt@xY;Iy#Xax;R0KO9J*w^8kpt^{|zgyJR7UT&R z-@6?!8CjpbdJ|^8&Zy}EEQaG54Rzv+JJC+FwyS0(4xVkc8mj!|vBfPc3}^#jX3@!pFP>=T`(&@&HpQ5fm_5_!7t{&eo|+dmJW&B(~W=I!LbGC2PlY8XK*?o- zZm`n=1Db-VFLB>G`duj;tmsLR#;ox}7T_FE^gXf36SBAR1v*yeVBh0bx0%v;{n|X3uDB3J}VrHZ!PVThpn=tS@1M3dT*6CtE&h5@% zwkB{7O;bA#`qk7BWJop-=s-ZO7&5+jIU-|^NW}cA6?0#YhF45Sj$k|JW2+fkE_!xf zj@4uSjg}MJQ{zIcz_{`%ga) z=v=bVhs^LS04tiuvYl6$crxWvZtq?KUzU0B9Sy2TqDU zZ?n(YtGx*?8te30t>Qe!q4R#jX2PEF{M#Sa-{3N=5qNpWAh^y>?wEK5NfpruN^`$iQX(vYPr^rVrk^rq=XLpMBU*AvIs z;j7|a1d6DZ+}pxxr-qXP*}eKs)zI^KbFV`Jx@T^#<`B%nkgP+yESBcLS-0S^Yk{`k zv7gQ4_Oa0@ZnO?g;;iBv|4rB|S?q8(*Xx}vb8{Q+vn-8oAV{!!2W%`TmSE{%r*02z zUY(i2^Oj5(cC7Fjet!2o(kDU;`L4#091Tm?NOOCx5~Pz<0R|+Tpf}z|6{Xzf=+;uNA=}r(yYadzvL<$| znU3QS_q?ppgiz824aq!{kOL$aGdq7jbt}*w!wz6gl-O?H&>u0 zOCK6;y4!(oKjlz_2$GfjMkg90Il7E`7!HJdU3!DGs}Y+00tSoOl4>=Muv?plGu?Ky zqVpZ#zM24zYWvxUUNI!37EcbFad`?UJ;e|@U<9|=V2_u92Mc_Xl(MMC92q(pmravZ z<+)?P-NFz}>Oly*tiqP9v(7i6*c-;dy+9Ih_Q;hQc41`S7N09>BKF#Ngh?V?Wp_^l z-0J&!ZNeoDk~Wyc<13;Z3Tq74jl2^Pu9XeS@`yh`zjKn0Y(pmp3vt?Xmun$KV-+O$ z3WuLzK(D0w3+2zwI+hz(^l(#_%*0->6n-8dsNjoQB&dhysp%ya6%an)>Rne&zunMY z!B<2tFK#P~6{9lHu~XIZqzpH&KpMVb4G0a($CSJHbOAPqCJQB3OjR%ap=YtW16HS* z!~?xoL>$=ofVtbr*Ra9L!zLkhG7o1}BwjHPDt2|l>9{J|r~o%~QHRW`pF44PDCA5% zTuC{SH08Kagk3~fg{bDRe-ueYiLb)3wF8C%B$fsw!ZE{(Az{igYPb?1{}I=vHAu*r zdehh%OoZNIJxS81EFb-I-fGr{eH!S?UgpxWBUDIYc=)0xvLuvB94FN1_#ja}HX2xJ ziZ{NKaPn^A;iP*f*E5UW(Q;P!Bww~ZfTSc`CgD(*MFa8h(|7C7HqhNJjK?Rn_A3R* z{Q4F)&Vl?P3gO$NNYnVlHx}n9GS>}zwaN=t`%b{)e!y#9b^yr)?Z{HlRT+lrVmN(k`7k-Vi4EVM@1_V{LN? zu52{J3$Ccl0>!;_b)dIXCEegG<+xsTUCC2^YHCzu`mhL_P-;oho}_xmewo%0?H!EH z@_jZ7_XF6HJwq3B04Q+-Jw%m^ocUxm;OSZ`f|5SXJ7VnTcg!;Lt=gdk26RD$;s#FS zlch)$G+}e(19fVjGBVi)S`Qjkn7Z7Qan*G->@iInAq=6!2s0ya;5yJnK)|jUu?e!u ziWh+i1=`ku{qmybywBmS6Z&Z$3Rp+neO~#cE{J{Z)yODq>yDp*GCZ?k#0qH$@h<4; z5(bK|;r2`V<)ECNxmlN+Btom-RdE+7$z>9dc5=B@}mr}D*DQNJy+_)3}|6+Y2%D@e;q;^x4G;_ zhT)py2&>J*L3MC<+s=X70Ryt3;bXt0Kf`MOI7m~Cz`}J0?_&-3)(onq+i(NtT;^#H zU&n#at{|rX4%3b`U2;SFhxA7q2bk<}c38y?AY5 zcJbx;#hID+M5c4Z?o05RpIZfo{?O!>Y?Y9lc@~}m?zOp@tJlouKBw-`nR_qI%*#AsmxutdiHr51QPVrgFadXQk`@8H^cE}VR(=3=evY_>i!~^W@XZnL zS_J#T6)6KQu;8gaxn{N1q6^sE;g_=5$?b+L$yefiLOAw&V)Hl-)0r!T zsT(DvV{sdouj~ZAJD2BEmT)-}J~O}p2_BaPzr?p0HBtDfT;Q@G{6GWHXZ9tWhcvr& z`~>v=tii>&4O~uvI36|p)n_Tv`5EoISOADpd_`+8SJ2B!^$^(T7fIvSp(V)2xbXFE zD0-ee(b<`~t36Y_y8Q~K8lV2v?JCmt-Q6hmYLvb^PVgI0ExoX=1FZuR4K}3SR{|jL zz$DaYXl>#NGI8bSjPwZSOWkcXE|FBGeYTBjs zVCSGw1%k>FfQ9g@pB*Sr@+^uG)WMNan(&0EQ~C5Xv{Q4QD{A4V-&Y}8y}`8 zj!2*uJ#HjNM{x{W+M-6qk8W^W#HP62PN>+sG9Tc(gn_L@XQHdTf#UF?fNX&88bAq% zOerC6fbvOI;7H8{9|%zc3CO6x;G~B9oRtngI04skn2n1G?=-sK29k zGS&bnAhO)#23&|;C#*J+h)s~YdeX~sfsUxrYdAZM&H+^H*XW^L$YxyBC*N}el+Ybc z!P2=Ne4MG(fRFIk0TNZL3%ab=LjRN}!7W^I9USTrLU$$@hL0W#R@(~{gfrIG^PmxH zA?d!3T#Q)X@@?v+OE~;<&t47K-r0nk%HV~oJ}^CdSq9p&t%CD~f&sUBx_f?12Rn4? z-MAZGMFEW)j&!rWy)}33m1_%N#&^`jFzfYi;3JEmfvaS}eDSSEeSbG-i*_9?2AtD2 zdH&nq;z;yh3Z@R9IVo*3G<^FmRp7S27K+&KGF(%PGMwmT8w!>+xmnRlFno~5Zp(FC zrhVG#&h)JA?2B_)-A%bGK<^ENWdRk4V|Ticv-IItw717XaPzt=lb6%O{H9#KGY17L z)6lClt;Z#@3bJCHt$f0#Wh0pG7|kJof?G6ke^Eg$pyrq1SOtKa^vjs%RHb0Q3Ka=1 zT;3*VPe8F~*q9hT1dja{p52Cw++I5n*(wrwDwN%Z^A%#i@v3YoS17P$%fHC@Y&m)1 zQ#l+!7xGmz0_7l@X#oHQKrv0$FnriFzND78-aB+LNjau_?kwZCV-F`Kd`SICVVYoo zz1u!uOHR2PcVBO7;{9GHd_(E-?K#=i}Xipqn-b zOgJG5cy38JP-;Md2v-3cWtKxZjw$1Q8(*~P1fcoozRCj3a1LS_Fx4zvO9rmYG*|*H z$OUXp;C9iwxCl`{9S7eN?hAd=aQ2qHDE-+;w*|9^P7&SlgsKOU0XM~Qpl$o{Vh0}~ zD$Aleub0B{fG^E{>J_*sAK!Y_{8vB9%uXS^tG$GGb$e@o=-;~7%YIqzN)OR8tzVVc zy7;2m=An12Jue2N9S1cIeEDQ<|8lSLHTMd+{mB)eLB-V-R+z&%=rWEs^XL+6J^emT zUl!&Xu@bxi(W!C*dOttBfMVdjbpVIKi6h8g*A`XrXjt2Tk-Ce|gwfe)W9=L4)oso$ zJ^`l9oz}`+|I*b4EJK3kEE4fcC&?>?b(bg@G2M7!Hj~@BN0QO}Y{?QYPbx z5@-pU@bT~rG0lkR5#rtx#IoHZVojK$>}~575SS@0o1^y~BvokQmTZW95S4Z%qrhfK zoE3odt$IK8@SoK3Mx?UG!V=Quo4U|Wu~4!F#;{HqV4!9xWq~<*8o(y-AOq95wthD~nkv$cb(zQm3oI@l zEIOQ}@6ry@2U&VogdvVTpqjhb9ruSd^ono2p6r@Kn^bKan?R887#{tuynNky zIF$MO<;U&}zRbhlwXG+pW!)MwRLQj4K2$-EO4c6y7_pSl|) zFMY!D0|$1mBjQmvavLfEP=G7d?t$pJ3C6eJ8KF^%!CQ|Rs33B7or;V*vg$U2f$8VHCP793agOwPU&56!3@F%^i4!4JF%D1R|I#@AMW9?Cc;W z+$6N%i4eHZnuS2z0PPo@#t!0I4OzXts*mPKoHiW^>`K0GdU$0R|EZ7>MTZNV8`w7zL)m zlDVw{5+t#yq(OCqV=MW$^ol*inFmUB$2UnBu&72fyO8Z}2-h_~o8` zl!6T#DUm^13zDdgv^qjfw6KM`aGW64MQjsoWl$nO%q%^)y%I_W_{E@1*g^-r?n^(w zQsmW)dGq1g>6#Uk$LGwbw|jbvg$K9z7U05paz1W$Ki}AVOY^{^2ex=_6A;+Oe*s2d zo|)$z&wxh;kf@?|6(Q!d0deGio-1_+sST8>ArH4%BWFus%9Xm0(&jBc-we{++H$4* z`m^1pw9!KET-g?|VjsPLz+_F?ezbBiqCbduO+cc9<$~ zdgb9pvW3}f#UU8MuA*T4FO2sQ>^ix{hr)b(0_rI8^p-vb?myIg*DY)x3S%E?zOKbn zsp%XsZ(DE;{NoHUkXVB7m0~34vf!rXWTXsvk78{bI6)o&tiR%+=Su0?#<7@Ojdkj1 z)qH*sKWn139h4#0BlenC9Y-KFMyZdqMU1?MbcS)aJwh>xevY|YC_~=X>B;5DEX}@_ z3uix=%Qnyg=`w>}c4Y<`p>}cxGdpM=r`9;fv6iJxz`L-Daj)!n~(5Vu9QNtg}PjUJsBrP`V6@ru1v&JVZDqTt}r2q zxs6lOiHw?#%N}i!yjND^vD*mY7nNjARs7`Nb%|%B9WSx8mcuQNSLO@z1Gu^yH|G{9 z`$0_NvfbG@Gqhzd?F@8pukYUsHzLehDhrLD?zE`#B|*BAOOptKm29{$`TZu zN>nU5MMx1tg($@genujk565}VOvqT{Epsjev?@*<2~)_vYM5Q(6!Gqt*F4AeR{>9= zx@&9Jyo9hW-xa8Im$2kqJdY}b6A6-ah!{^8IJ~DkVkr7iXu`OGbbcE7IpT} zW`EiuAgk!(5!*aNAy0zhDEo!5nPMH>P=QjGwR|$lSW9SAl%44flp~WROHwvO)gg}` z{$4a@gRMtf8QfdO+yLx2OOKNd{|2ampMNNE{?^0x&DK7KNH0EYyIvLb)HGl=4?Rxb z-a{!0Z$Iof=P`d37`}&7NBry>YLI3~tE6c16Sr~{NQcI}uz(dMugO3;=33rG=&lGX zrm3hVw=tKADx~6$9ETx<-vGZ_!kfw?CoGGF4vHSUzhwI1lOh*-j5h0JunnN0OoP(o)Qn9*@ zT#m0TY81z(WeM9&p*8Y(ZD}y9GvlPI>V#D@&0_ zW0s>dulrFr(}DCtAxTw8RRvukuCPiJUFD4VtT9i27O74$sA9qyA_o^`upRP!N)=UJ z9VW0$Z~G1|vGHASJo6;;z`Wr3d7RRT`)cAUcAKgA+F|yyIT^d+Vmz0#QWpYiE4O~# zBz|Q9^YQDC-5`&7G*ZVoLs%PZ$bQVyKI&6&3RaCU%!;kHuxcS>3uf31K z&)<6ZmPt{hfrsM19|Hoe-BTgD_weKN9KF9~o>?LPR83G7&jIWAvTq|Q72-jDF_X0I|HA)Oue#*epMUg=u}p*4A(vb$JaafDefMrbjZ0< z4v@PJDxq2X$;SF$2x&&*J&Zc+cTSx?Rd_n3A|0Hzzo|sK%^v8uz|o z9yAV2$#Z|so?0CtN{IB?K?rol?G{D`6@{G0hinet!=Zs@~q|Hxi%`8SjX=j}k zuaioNxg0yO*TFDq-!xE1v58!i+B18>0dQlsezE#oQ6~X)@C3ztag<qBYls}FADV`%94(_8ag z?Nt5w0b}y5Jd}3-0yjiX@_Uiy99^1E6GPPJz!hLU(in9~^J3hpCVL~*+{VhN-8)dK zy>_~kqSR46)ZwuDNhvMhnOU8ezI$v{9ckNUtu2kfH|XfgtYwWOXe~> zkeAIH(13LeD}X~2^D#&Nl~*)~{%nxW2{H$&Ny@TjgtaJ1c~Wq!DazEMAjQw03E1i) z%C$|Mj;k3*`@1O3CGbW$jf^H7z-ofW3A;}rDaKi?Q@6yp_(KZo-;8UHQOQo{VEsnEQ$nUs( zYRRgPf)H^VBh=~G4neZ?@-eOg*#^p8*+f4r ztSOm*j;eP}m8tmp=9L1d%E93c)6AC*lwOebBi zfBu;lD5o5MrwoZgDJna-DIH}<#4Ok136|ECIAc?*{{eMgj`q^ajpFpbX~#oN#8i6W zw`_R@{l`t&#p0~Fro)dBk$H_6;%#~EVRQ4GYDDjjZ^>eX-`qEOEv?)0 zC5)81I5|vw-*!g)6|>M~t|3?VZ|OyRNiff}{H(ZU#6tQ2i07&@C{R|nTxK{!=7=4=Ln*u7eaWfp#KQ$pXi+zdVD|&@e!Km;=G6Oc3MsbYeO&Z6?%GKxrs3} zu%=vFovXuLN>55S##Nz=9v(79$_7$`J7A(^oqa~tl>TXjn9e7IUH_^`32GEvP2!@} zs^9)a+Uw^xk(S3D-kFy@#G?=)9ud>%MI?UHuNB2LQ&kO!Gwj9xsOF`yjave{f z=JhxqCUxkT;}*8c74BuZ)^rx;qT?reKqoGZi5WaZ%a3>oWi!P&G32w^E zyiKp^hpj%QoWNn4`GUs?^8bEm&UK?^3LW7U;-YKGQ2=>}=bKD*CwT&o#jmqwKQBH> z$BknFG^tnfmQthMWBm3j{(@S;Jv{hgBav;v%+|IfBgL%Y`8G}+$7ceQ9BfC>k9J}&`jEqsPsZ?dBZnnYsol>*E(3{qoWDOOzKDGs zs`c}>O6%#tT5?~1v*fd0+wPxxUyZ$AY%P0X>-2WF0_tI;1mb#_$}*r#nX48&wUdr$ zPF&GmJkb>n;5VecMZLmlYU&YF2GYbnmGh@3-u(IDt)C?H!RO@yup(-0IBBH%P~3WR z7(`pqN$9AB7}dTgBZ=v13lOhOaN6kAS|prK!$s9NqJ8%D@?yVM*~@-NA6(hUPC#yO ztvHsRF8|CkP7`uEd7N#UKkwym@2FJ=#SctkRD?1|?4aFGm7Z5U7-}rbEkMew|Lj2m z{r{I?;n+&QT1S=DG45Fi-8q<;W|}gVsHBGuNZMrSdX$+?SyjMGNDWWt_c$kbkj4?7 zA~$>D1`wZ@VHz$%X(CvoJ;8Bfl_Bp?h$B0d3oaR38DyAnv)LPPiDw5hz*W?DP6G3U zLx4lCV!xQTOx(F|y>rVWHKECT2M_b<8X!is$UA!+s+yN`G3ZmNS3|@19#y3|YjGsK z5FH7XPi0_Hp=*uq>tr#k)5cqonPGR7HNN`M{vE52%;atsEwL?iDY=n(L!zH>`sc1p zTMgLgmKDti?-XWBPSrIB|1SFtd+q$Y?3F4JzD>fhWM|w**{mup16KDkO{d;t@WdT& ztkUcu_7O+S(gH$EA)b+5odQOzRWa$QSLu+9npB|-TQSXS4B8ANRqYGzyKNTIB56~1 z%7{7aL)Cayt<*$TCTU*gY~nZ&7Xfax#CAuiFaTeVLd~t;Wtpd&xfeDAL}OvlY*>@? zAvAkAQuO#r^jK5kbw}Aw2sxD{KC39lU61r*UOkB)^a`YYx-!@Rb4ThoVtkgoi7Vm8 zLY}L$BK3z`P}qt)5pQ0PI|c|%k5z--+{D9W zSD;nW2vsCK4(eShUMY;4P)bwL^)O%id0KTJZ4ooPfi|G^&%2T(!5+f1pT-Tg#Bc?g zK-?gV`azZ*@wav5QqSklSTo<`V%BM4#x@NDlZ4hmGoQOC%AIphe%vIyZdnOy@0W0WT zIco1Q7s{kKC*l?(k6D{DdeB;2g{viuk!!`)d8CzFnS@@izN%EIpUF^f8kBF&MaOC` zEBEZh71hR6Yo?CS#{3RBvyByu!f&rQ`Z$d*IV~YhLs5OtTu{6`rGz+xJq8Ad5r#nZo)5n~C>A#L9-4Y*l z4SV^q$@X&D=vIFG3w|9#@7W~$20D812S~qwo>-DNt(-7!*TuK?ou+3lavafP5mDN$ zBahG{R@BS|9JMJS?lxwb9_VM|orz-NdxJ&gu zR%BmzwdZ-BBAx!~Wt48U=+Q0RM%#ve;^aAlB9MEEmHccDklw?83MW!({4fGqz`tMr z-M{)Df8)ZBbHD!gKYQyR{;U7ZnA9KK|LV=Bum12qOqt`H@T_F*%0F zWV$di#G=D|jPjAhV-)%6bEq-Il0yhgFff5C6BARb$;@zKdNh?{+9)C^Mo@?e5*TJq z3T-APO)@c^lK+NNDU5z-cr5)nj0f-O(UGxf3~US&92*}R8y$Y$4B>AAgU^_ek3RZ; zJ{oz9Lz>{@M`(^iPM=GS;+0B`VH9W*k)c$I0Gb}2Mts7IvPbqdW`;-c4?PXzZwyVN zbOJ*k`F}q`iyz@1(#J>5(D?W$#y>eUHi<%j%y43IVge~=hsGw5a+W<#0vIM;z;FvA z&m+LJ^G|U|=hI)B$c&`l#5=ulUV!(ereC$`SCOtM*KEqQFy#%K@`lt!KaiOeXc z#2{ud^)+@e^|j$KM4v}AX$aWqQN=SZ=eSYSn;v}(OGqS~9!;-IRnJd8Zc?#0mJ1^o zL%*ZbgyJYVWe`h|85Pt(cr;_C(*e3hI5U*KzyhPsOB$Y|BadT=Coouabo5(O^^q~W zFo6sB+e>9e5|gMmH1b!T!Lp@L6m78R@%eGBb9|n-H!*Q0Gn#&rT?3YAmO6)a)5o_aAD6og_%j$Y9$)9vkTa1{v< zj*mAGm4P4qgY?mdlam}kTEjYqqwkFY*6E|~$$FxQMt`WUA14j+{sEBM4Cywn40Up}f53oAtu}~Z+ zF$O4^K(8NSaneUW(2jn9@nKnxJ{$u|Opi_qW*q%Mdj0`c3 z6LX0xiK~eh6E7uRPV6M^C9;WyL_SeSTu-bdRugN9QsU;>=;sHM(Fl0Q(SQD1XpGab z^Ebm2TuJc2^Z^4P>{R+d5NaI3NgEnLqLv#;5lc{EBqcQg0hHkr_-=siBxOK^tMN}| z&L0w#f5eA`{weYZo20x5JQn>>UdNgY~%@cHN{7o50rbH)Iu;2VIV6o`_wp+&;mfiHJ%3J)t(;bWTsOiXSD9o zPtm~9AG4JVAD=@hpvO-MDv5B87{&?*DBN(W$`+=o;3HM?k*c=)JQ?NJnZ+k6U(@t! zGN(5fnL2(WMV2%2n3BktI-W@%Uqyk(Gh{qI4#<*3&I#gz zj2Gi$zfQD1z9zuMsD{&TP6{i=ghACK|z zI3Ghhcx3pvz()dmX%I+wpn;VUV#bI8ryDUDJg%_ii+nuA2i1m=Pw{c%`tS%I$rK(l zGk7G1@E9B8BZnzV!8NJk!SvW4*QCZxdr~Nx$F5lev7}0#2ZV-<&){ z(a(VJ`>EsKX8FG98ysevdW}i13XicL*z9OWVd7*)Ut+#AWnt= z#a~Mw|JFDZE@Vy}e+MB{{V;v3;^f$Jr4LlXA1|OGEDvC>!}{)|2&LoiG6^F@T?vXp zmn4wK`8b0CrO<&E{(xfsEUEBVf)nIj$o@bZK-%LI6X#t1(Bl)2bHFJOF30x16od$u z(Bo8}lK30P-x-kjL~4RMTY~dJ1$Ko{%Ao2C2mtQ@h>6j$X+!|QqhBQ$KjhhjodFB=?{F!9k1#!dUO>UZ)P4v69pHD7q@i0jgc=CR!v7xE*#&+Ind*gF_gb!LwF#* zUdqmbRZtY0|M73EedlMtbMV)G_@h6a{*B+A9sbFM$;-d-A3e5M-}*bxe)K2*_kaC6 z-}|+{{!2gphoAVr{_uk*|N8a6_v)u^{=q-L_?4Z1^dCI^8^M>){r2Cz`mg?z>#bX_ z4qckK|DC@%^T)~m^zwhXJNA>G)&9@V-1|TNcH(dS)o)&TdgtjsTK(O>J8|*+pS*tm zf9n3F|L*4f;>gXvwY~Gnm7T)xk3d^|iVx^sQ{RLhiJ>2Tf9mKT4MEjUCQ>6wsOw-( z!}ym3{$tj6zjL*^0)L8?JJ^DVR)Au3a zk|VSNM$aaJZtfpPa+VD6{91CJ?GN||ON4gH7_-LYUuq2h;UBCA3>9xS932{BNhy&^ zJ&}Bjnfh;fD#O6j$xrYl#*d#5lSY$lFPQ`<#>^&@XE-G318-Xm5?pGsy!{98iR7L- zo&pqMvm}Sd$H}I@`8cx9CX>VI@=*E>zHi|T{+nJ-U*+qDz82D7`c(4V^duNhx{xlw zP~anl|Al0xjxV4cvWQ`@RR{$_a|pTA7E|ZPABQe7{TcHaXl99FGOPoL2_WVRQ^yxm zh#q|(VU$0MWt|>QzfH|-cmhqNkEheGr9aQLnM}b@Ndl>%Sc1WW#UqkFd^TY|v(X7M zdHgateypX&%dBxFF0L2O;LhGIulqM+$zhb7U~Nb@t1Q}wgSaSL`}94yJn z^XJcxQf`91fUPi5D18HfKx8sGnm%4lAH({Ek@0ah!Bu_g3@~JRlrx+jeJ1%7QArFd)B)b0bodWFVu%9G9>V01U{EA&k*X}-5?`BI8o*ghVbVnGWbaqTq=$G%+03~W{QR6 zx8D3}NShxLO@>Lk(AGxD4Bw^1J359P3K{txKqWf)Cz9t-0IMf_^H4@U39SU`2B7|{ zw0K4Lz!WAY{}c;D??wjHJ*3kdNznksI-vNk19wJP5QvmW7l8NZP@pk7xr~ohT3wMv$;LYHm|sn*`0ksvieJD{MeL(`=IK$g8sZT7%X) zo*ARN1ph>)Op+}e{QyeZ$OM=ZT7+#yikJ}81x5U38R>|=3xm0pgICARK$xnea+&^+n|8FWu1`tiM4RIzQ6Qb^U5mnE#(JxN8 zp^c`VOg<5B%eD$eGL$Y0pA`lxad239gT-=U+F=K4G4XWaDab{*pk`{X(xV z=tx3t)7PIe3%noxXDvmQ@XLcu{2s!9B5w4q2rp+JP|Sg~7ZCR1dEaHo z7oyM@(BMqMe1TJZhZnb7aPancH`%_1FDA_M0+VRN3;3nGE`GU`Q^$`a`e9s7m_OYM z?eMnv0mBiN{2rJJ`2Bwa5s*IqeR=-(5xLQfv=5pdIs>X3hw73X!xL{87^hD_sSKhW zjAyF)SaKX{uW%cgP%&o>H-b&EEtf&+{L`2f=P)^SwkY^dn#eh=J`{p?iZ7d zkn&#^84FTY%6?zK^ZRhGrYHuFe;@Qb0mCGH4iAx@=ckjfD1H-`9!T|gH~mG*JMhor zpTHnU7yeZ8i6L0U_%{xNK^QN{R)(x^Uy_6?G}8X#~;vgNvY}u zwvEPwEu1IsIlje&Z@~wGu}DzV4zQ-LbB<)}6G=EuF$#1DU1h?B*~xjb=1KA^IBb6p zlb8e~&q4D#2m9_PFvu}K+C&rJw%9WeT@}yC+Yq~e`^ zji%Y>IdcBVO8jKvmy*w*y)$RdOh82%nm987VF76(5J`=rNf>OH)h9937obgX4)`CQ zWXw!7ET%%!VlR2epeQrtmyG&^rZG@tIhf-Q&<#Y`Fq9B^;SH9Goe(S?EKKCkZv_pE zek|J3me-$4Qu;ubIVL7z@&MG3bpI_nCIJ{~vlA1OFjmH>ZXyo~5BWHtjzywMh$&F> zg{(r^N$Rz!VQq@Vv4kj_MDgkK{12dF6(Aw{dNPR~+)6WfBaLM6(`%VaYx#0!u?>BPzvq#e;|x*>dd=_;!Y*OTL!Wx9 zUEN=6HN)T4z^~Y}?sgK_KBDQY|JE4JxLExCMp&+r!w*U~@Pl{wxzD{0w96p4RBPbk z2>ko#^Ct6#g#P__Bj9CI;Ap#m z_DVQaz&YS1Wps%k7q* zJkP=7zJxOtJW)!-#O3C=l1I|*p8`AnF&#}iD(e5$K?iju!#@EbQ!1xB*=M^Rg!0oR8S^@1oYp)21pHeb76PN6(*NZVxd?B3P z5d6vlYCL7cGqQR*^0azUJYyS={yl=gBM3Z#z#|Aeg1{pPJc7U@2t0zoBM9_B;3q@S z>W%rjGq`)QA4MNQ;1L8KLEsSt9zoy{1Rg=)5dIn memory file system implementation. + public class MemoryFileSystem : BaseFileSystem + { + private static object _lock = new object(); + private Dictionary _pathToEntry; + + /// Initializes a new instance of the class. + public MemoryFileSystem() + : base(false) + { + _pathToEntry = new Dictionary + { + { + Path.Root, new Directory + { + Path = Path.Root, + Data = null, + FileSystem = this, + IsReadOnly = IsReadOnly, + CreationTime = DateTime.Now, + LastAccessTime = DateTime.Now, + LastWriteTime = DateTime.Now, + } + }, + }; + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + bool ret = false; + + lock (_lock) + ret = path == Path.Root || + (_pathToEntry.ContainsKey(path) && + ((_pathToEntry[path] is Directory) == path.IsDirectory)); + + return ret; + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + if (!Exists(path)) + return null; + + Entry e = null; + + lock (_lock) + e = _pathToEntry[path]; + + return e; + } + + /// 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) + { + return null; + } + + /// 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); + + lock (_lock) + { + var e = _pathToEntry[path]; + + Path newPath = path.Parent + newName; + e.Path = newPath; + + _pathToEntry[newPath] = e; + _pathToEntry.Remove(path); + } + } + + /// Create Directory and return new entry. + /// Path of directory. + /// Created entry. + public override Directory DirectoryCreate(Path path) + { + base.DirectoryCreate(path); + + var dir = new Directory + { + Path = path, + Data = null, + FileSystem = this, + IsReadOnly = IsReadOnly, + CreationTime = DateTime.Now, + LastAccessTime = DateTime.Now, + LastWriteTime = DateTime.Now, + }; + + _pathToEntry[path] = dir; + + 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); + + lock (_lock) + _pathToEntry.Keys + .Where(x => x.IsParentOfOrSame(path)) + .ToList() + .ForEach(x => + { + _pathToEntry[x].Data = null; + _pathToEntry.Remove(x); + }); + } + + /// 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.")); + + IEnumerable list = null; + + if (Exists(path)) + { + lock (_lock) + list = _pathToEntry + .Where(k => k.Key.Parent == path) + .Where(k => mask == null || mask.IsMatch(k.Key.Name)) + .Select(v => v.Value); + } + + return list; + } + + /// Opens an existing file for reading. + /// The file to be opened for reading. + /// A read-only on the specified path. + public override System.IO.Stream FileOpenRead(Path path) + { + base.FileOpenRead(path); + + return new VirtualStream(this, new System.IO.MemoryStream((byte[])(GetEntry(path).Data ?? new byte[] { }))); + } + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// An unshared object on + /// the specified path with write access. + public override System.IO.Stream FileOpenWrite(Path path) + { + 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); + } + + /// Creates or overwrites a file in the specified path. + /// The path and name of the file to create. + /// A that provides + /// write access to the file specified in path. + public override System.IO.Stream FileCreate(Path path) + { + base.FileCreate(path); + + var e = (File)GetEntry(path); + if (e != null) + e.Data = new byte[] { }; + else + { + e = new File + { + Path = path, + Data = new byte[] { }, + FileSystem = this, + IsReadOnly = IsReadOnly, + CreationTime = DateTime.Now, + LastAccessTime = DateTime.Now, + LastWriteTime = DateTime.Now, + }; + + lock (_lock) + _pathToEntry[path] = e; + } + + return new MemoryWriteVirtualStream(this, e, new System.IO.MemoryStream()); + } + + /// 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); + + lock (_lock) + { + _pathToEntry[path].Data = null; + _pathToEntry.Remove(path); + } + } + } +} \ No newline at end of file diff --git a/VirtualFS/Memory/MemoryWriteVirtualStream.cs b/VirtualFS/Memory/MemoryWriteVirtualStream.cs new file mode 100644 index 0000000..e33ab8f --- /dev/null +++ b/VirtualFS/Memory/MemoryWriteVirtualStream.cs @@ -0,0 +1,65 @@ +/* + * 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.IO; +using VirtualFS.Base; +using VirtualFS.Implementation; + +namespace VirtualFS.Memory +{ + /// Provides a view of a sequence + /// of bytes in virtual file system that is + /// related to in memory file entry. + public class MemoryWriteVirtualStream : VirtualStream + { + /// Gets the entry associated with this stream. + public File File { get; private set; } + + /// Initializes a new instance of the + /// class. + /// The file system. + /// The file to which stream belongs. + /// The internal stream. + internal MemoryWriteVirtualStream(BaseFileSystem fileSystem, File file, MemoryStream stream) + : base(fileSystem, stream) + { + File = file; + } + + /// Closes the current stream and releases any resources + /// (such as sockets and file handles) associated with the current + /// stream. + /// This wrapped method will decrease number of opened + /// files in file system. + public override void Close() + { + File.Data = ((MemoryStream)InternalStream).ToArray(); + base.Close(); + } + } +} \ No newline at end of file diff --git a/VirtualFS/Path.cs b/VirtualFS/Path.cs new file mode 100644 index 0000000..7eb5fd4 --- /dev/null +++ b/VirtualFS/Path.cs @@ -0,0 +1,307 @@ +/* + * 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.Linq; +using VirtualFS.Extensions; + +namespace VirtualFS +{ + /// + /// Class representing path in file system. + /// + /// + /// Path is always rooted, so you can't make a mistake. + /// + public class Path + { + #region Static part + + /// Gets char used to split path elements. + public static char SeparatorChar { get; private set; } + + /// Gets char used to file name from it's extension. + public static char ExtensionChar { get; private set; } + + /// Gets an array with some invalid characters that + /// cannot be used with path names. + public static char[] InvalidPathChars { get; private set; } + + /// Gets an array with some invalid sequences that + /// cannot be used with path names. + public static string[] InvalidPathSequences { get; private set; } + + /// Gets an array with some invalid characters + /// that cannot be used file names. + public static char[] InvalidFileChars { get; private set; } + + /// Gets root path. + public static Path Root { get; private set; } + + static Path() + { + string invalidPathChars = "\"<>|\0:*?\\"; + + SeparatorChar = '/'; + ExtensionChar = '.'; + InvalidPathSequences = new string[] { SeparatorChar.ToString() + SeparatorChar }; + InvalidPathChars = invalidPathChars.ToCharArray(); + InvalidFileChars = (SeparatorChar + invalidPathChars).ToCharArray(); + + Root = new Path(); + } + + #endregion Static part + + private string _path; + private string[] _segments; + + /// + /// Initializes a new instance of the class. + /// + public Path() + : this(SeparatorChar.ToString()) + { + } + + /// + /// 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()); + } + + /// Gets a value indicating whether this instance is directory. + public virtual bool IsDirectory { get; private set; } + + /// Appends the specified path. + /// The path. + /// New instance path combined from parent and new name. + /// + /// The specified path is not a directory. or Added path is rooted. + public Path Append(string path) + { + if (!IsDirectory) + throw new InvalidOperationException("The specified path is not a directory."); + + if (path[0] == SeparatorChar) + throw new InvalidOperationException("Added path is rooted."); + + return new Path(_path + path.Validated("Path", invalidChars: InvalidPathChars, invalidSeq: InvalidPathSequences)); + } + + /// Determines whether specified path is same as current + /// path or this path is parent of given path. + /// The path. + /// Returns true if specified path is same as current + /// path or this path is parent of given path otherwise, false. + public bool IsParentOfOrSame(Path path) + { + return this == path || IsParentOf(path); + } + + /// Determines whether this path is parent of given path. + /// The path. + /// Returns true if this path is parent of given path; otherwise, false. + public bool IsParentOf(Path path) + { + return IsDirectory && _path.Length != path._path.Length && path._path.StartsWith(_path); + } + + /// Determines whether this path is child of given path. + /// The path. + /// Returns true if this path is child of given path, false. + public bool IsChildOf(Path path) + { + return path.IsParentOf(this); + } + + /// Gets the file extension. + /// Return extension string. + /// The specified path is not a file. + public string GetExtension() + { + 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(extensionIndex + 1); + } + + /// Gets the name of entry from path. + public string Name { get { return _segments.Length > 0 ? _segments.Last() : null; } } + + /// Gets the parent path. + public Path Parent { get { return GetParent(); } } + + private Path GetParent() + { + switch (_segments.Length) + { + case 0: + + // If root, then there is no parent + return null; + + case 1: + + // If only one segment then parent is root + return new Path(); + + default: + + // Join segments with separator and soround this by separator. + return string.Format("{0}{1}{0}", SeparatorChar, string.Join(SeparatorChar.ToString(), _segments, 0, _segments.Length - 1)); + } + } + + /// Removes the parent path. + /// The parent. + /// Path without first segments that are parent of path. + /// + /// The specified path can not be the parent of this path: it is not a directory. + /// or The specified path is not a parent of this path. + internal Path RemoveParent(Path parent) + { + 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)) + throw new ArgumentException("The specified path is not a parent of this path."); + + return new Path(_path.Remove(0, parent._path.Length - 1)); + } + + internal Path AddParent(Path parent) + { + return parent.ToString().TrimEnd(SeparatorChar) + this; + } + + #region Operators + + /// Strings the specified path. + /// The path. + public static implicit operator string(Path path) + { + return path._path; + } + + /// Convert string to path. + /// The path. + public static implicit operator Path(string path) + { + return new Path(path); + } + + /// Determines whether the specified is equal to this instance. + /// The to compare with this instance. + /// Return true if the specified is equal to this instance; otherwise, false. + public override bool Equals(object obj) + { + if (obj is Path) + return Equals((Path)obj); + else if (obj is String) + return _path.Equals(obj); + + return false; + } + + /// Determines whether the specified is equal to this instance. + /// The to compare with this instance. + /// Return true if the specified is equal to this instance; otherwise, false. + public bool Equals(Path other) + { + if (other == null || other._path == null) + return false; + + return other._path.Equals(_path); + } + + /// Returns a hash code for this instance. + /// A hash code for this instance, suitable for use in + /// hashing algorithms and data structures like a hash table. + public override int GetHashCode() + { + return _path.GetHashCode(); + } + + /// Equals operator. + /// The first path. + /// The second path. + /// Return true if values are equal, otherwise false. + public static bool operator ==(Path p1, Path p2) + { + if (System.Object.ReferenceEquals(p1, p2)) + return true; + + if (((object)p1 == null) || ((object)p2 == null)) + return false; + + return p1.Equals(p2); + } + + /// Not equal operator. + /// The first path. + /// The second path. + /// Return true if values are not equal, otherwise false. + public static bool operator !=(Path p1, Path p2) + { + return !(p1 == p2); + } + + /// Append operator. + /// The path. + /// The string to append. + /// New instance path combined from parent and new name. + public static Path operator +(Path p1, string p2) + { + return p1.Append(p2); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + return _path; + } + + #endregion Operators + } +} \ No newline at end of file diff --git a/VirtualFS/Physical/PhysicalFileSystem.cs b/VirtualFS/Physical/PhysicalFileSystem.cs new file mode 100644 index 0000000..d535b01 --- /dev/null +++ b/VirtualFS/Physical/PhysicalFileSystem.cs @@ -0,0 +1,263 @@ +/* + * 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.Text.RegularExpressions; +using VirtualFS.Base; +using VirtualFS.Implementation; + +namespace VirtualFS.Physical +{ + /// Physical file system implementation. + public class PhysicalFileSystem : BaseFileSystem + { + private string _root; + + /// Initializes a new instance of the + /// class. + /// The directory. + public PhysicalFileSystem(System.IO.DirectoryInfo dir) + : base(false) + { + _root = dir.FullName; + + if (!dir.Exists) + dir.Create(); + + if (_root.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString())) + _root = _root.TrimEnd(System.IO.Path.DirectorySeparatorChar); + } + + private Entry FileSystemInfoToEntry(System.IO.FileSystemInfo fsi, string rootPath) + { + if (fsi is System.IO.DirectoryInfo) + return new Directory + { + IsReadOnly = IsReadOnly, + Path = rootPath + string.Format("{0}{1}", fsi.Name, Path.SeparatorChar), + FileSystem = this, + CreationTime = fsi.CreationTimeUtc, + LastAccessTime = fsi.LastAccessTime, + LastWriteTime = fsi.LastWriteTime, + }; + else if (fsi is System.IO.FileInfo) + return new File + { + IsReadOnly = IsReadOnly && ((System.IO.FileInfo)fsi).IsReadOnly, + Path = rootPath + fsi.Name, + FileSystem = this, + CreationTime = fsi.CreationTimeUtc, + LastAccessTime = fsi.LastAccessTime, + LastWriteTime = fsi.LastWriteTime, + }; + + return null; + } + + /// Check if given path exists. + /// Path to check. + /// Returns true if entry does + /// exist, otherwise false. + public override bool Exists(Path path) + { + if (path.IsDirectory) + return System.IO.Directory.Exists(EntryRealPath(path)); + else + return System.IO.File.Exists(EntryRealPath(path)); + } + + /// Get entry located under given path. + /// Path to get. + /// Returns entry information. + public override Entry GetEntry(Path path) + { + Entry result = null; + System.IO.FileSystemInfo fsi = null; + + if (path.IsDirectory) + fsi = new System.IO.DirectoryInfo(EntryRealPath(path)); + else + fsi = new System.IO.FileInfo(EntryRealPath(path)); + + if (fsi.Exists) + result = FileSystemInfoToEntry(fsi, path); + + return result; + } + + /// Get entries located under given path. + /// Path to get. + /// Mask to filter out unwanted entries. + /// Returns entry information. + /// Path is + /// not a directory, thus it has no entries. + 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.")); + + return new System.IO.DirectoryInfo(EntryRealPath(path)) + .EnumerateFileSystemInfos() + .Where(f => mask == null || mask.IsMatch(f.Name)) + .Select(f => FileSystemInfoToEntry(f, path)); + } + + /// 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) + { + return _root + ((string)path).Replace(Path.SeparatorChar, System.IO.Path.DirectorySeparatorChar); + } + + /// Moves an existing file or directory to a new location. + /// Source path of file or directory. + /// Destination path. This path must ba a directory. + /// If true the destination files and directories + /// can be overwritten; otherwise, false. Files that aren't moved + /// will remain on file system. + /// If true the source directories will be left intact; + /// otherwise, false. + /// Source file doesn't exist. + /// This method uses simple yet universal approach. + /// it's much slower than native one so implementing file system consider + /// better approach for internal move. + public override void Move(Path source, Path destination, bool overwrite = false, bool leaveStructure = false) + { + if (source == destination) + return; + + if (IsReadOnly) + throw new InvalidOperationException(string.Format("Can't move file(s) '{0}' to '{1}' on read only file system.", source, destination)); + + if (source.IsDirectory && destination.IsDirectory && !leaveStructure && !Exists(destination) && Exists(destination.Parent)) + System.IO.Directory.Move(EntryRealPath(source), EntryRealPath(destination)); + else if (!source.IsDirectory && destination.IsDirectory && Exists(destination.Parent)) + { + if (!Exists(source)) + throw new InvalidOperationException("Source file doesn't exist."); + + System.IO.File.Move(EntryRealPath(source), EntryRealPath(destination) + source.Name); + } + else + base.Move(source, destination, overwrite, leaveStructure); + } + + /// 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 (path.IsDirectory) + System.IO.Directory.Move(EntryRealPath(path), EntryRealPath(path.Parent) + newName); + else + System.IO.File.Move(EntryRealPath(path), EntryRealPath(path.Parent) + newName); + } + + /// Create Directory and return new entry. + /// Path of directory. + /// Created entry. + public override Directory DirectoryCreate(Path path) + { + base.DirectoryCreate(path); + + return (Directory)FileSystemInfoToEntry(System.IO.Directory.CreateDirectory(EntryRealPath(path)), path.Parent); + } + + /// 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); + string realPath = EntryRealPath(path); + + if (System.IO.Directory.Exists(realPath)) + System.IO.Directory.Delete(realPath, recursive); + } + + /// 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); + + return new VirtualStream(this, System.IO.File.OpenRead(EntryRealPath(path))); + } + + /// Opens an existing file for writing. + /// The file to be opened for writing. + /// An unshared object on + /// the specified path with write access. + public override Stream FileOpenWrite(Path path) + { + base.FileOpenWrite(path); + 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 null; + } + + /// Creates or overwrites a file in the specified path. + /// 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) + { + base.FileCreate(path); + string realPath = EntryRealPath(path); + + return new VirtualStream(this, new FileStream(realPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 4096)); + } + + /// Deletes the specified file. An exception is thrown + /// if the specified file does not exist. + /// The path of the file to be deleted. + public override void FileDelete(Path path) + { + base.FileCreate(path); + string realPath = EntryRealPath(path); + + if (System.IO.File.Exists(realPath)) + System.IO.File.Delete(realPath); + } + } +} \ No newline at end of file diff --git a/VirtualFS/VirtualFS.csproj b/VirtualFS/VirtualFS.csproj new file mode 100644 index 0000000..66b682f --- /dev/null +++ b/VirtualFS/VirtualFS.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0;net472;net6.0;net7.0;net8.0 + Virtual File System library. + Copyright © RUSSEK Software 2012-2024 + RUSSEK Software + Grzegorz Russek + 1.6 + https://git.dr4cul4.pl/RUSSEK-Software/VirtualFS + https://dr4cul4.pl + VirtualFS + MIT + + + + true + snupkg + + + + + + + + + + +