474 lines
20 KiB
C#
474 lines
20 KiB
C#
/*
|
|
* VirtualFS - Virtual File System library.
|
|
* Copyright (c) 2013-2026, 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
|
|
{
|
|
/// <summary>Basic abstract implementation of file system.</summary>
|
|
public abstract class BaseFileSystem : IFileSystem
|
|
{
|
|
private object _lock = new object();
|
|
|
|
private volatile int _openCount = 0;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="BaseFileSystem"/> class.
|
|
/// </summary>
|
|
/// <param name="readOnly">If set to <c>true</c> file system and
|
|
/// all it's files/directories will be treated as read only.</param>
|
|
public BaseFileSystem(bool readOnly)
|
|
{
|
|
IsReadOnly = readOnly;
|
|
Priority = FileSystemMountPriority.Normal;
|
|
}
|
|
|
|
/// <summary>Gets reference to root file system.</summary>
|
|
public virtual IRootFileSystem RootFileSystem { get; internal set; }
|
|
|
|
/// <summary>Gets or sets a value indicating whether this file system
|
|
/// has higher priority than others mounted on the same path.</summary>
|
|
/// <remarks>This value must be set correctly before mount.</remarks>
|
|
public virtual FileSystemMountPriority Priority { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this file system is read only.
|
|
/// </summary>
|
|
public virtual bool IsReadOnly { get; private set; }
|
|
|
|
/// <summary>Gets a value indicating whether this file system is busy.</summary>
|
|
/// <remarks>Implementations of file systems should set this flag if any
|
|
/// file is open in this file system or wrap stream into
|
|
/// <see cref="Implementation.VirtualStream"/>.</remarks>
|
|
public virtual bool IsBusy
|
|
{
|
|
get
|
|
{
|
|
bool res = false;
|
|
|
|
lock (_lock)
|
|
res = _openCount > 0;
|
|
|
|
return res;
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets name of file system.</summary>
|
|
public virtual string Name { get; set; }
|
|
|
|
/// <summary>Check if given path exists.</summary>
|
|
/// <param name="path">Path to check.</param>
|
|
/// <returns>Returns <c>true</c> if entry does
|
|
/// exist, otherwise <c>false</c>.</returns>
|
|
public abstract bool Exists(Path path);
|
|
|
|
/// <summary>Get entry located under given path.</summary>
|
|
/// <param name="path">Path to get.</param>
|
|
/// <returns>Returns entry information.</returns>
|
|
public abstract Entry GetEntry(Path path);
|
|
|
|
/// <summary>Get directory located under given path.</summary>
|
|
/// <param name="path">The path.</param>
|
|
/// <returns>Returns directory entry information.</returns>
|
|
public Directory GetDirectory(Path path)
|
|
{
|
|
if (!path.IsDirectory)
|
|
throw new InvalidOperationException(string.Format("Can't get directory from file path '{0}'", path));
|
|
|
|
return GetEntry(path) as Directory;
|
|
}
|
|
|
|
/// <summary>Get file located under given path.</summary>
|
|
/// <param name="path">Path to get.</param>
|
|
/// <returns>Returns file entry information.</returns>
|
|
public File GetFile(Path path)
|
|
{
|
|
if (path.IsDirectory)
|
|
throw new InvalidOperationException(string.Format("Can't get file from directory path '{0}'", path));
|
|
|
|
return GetEntry(path) as File;
|
|
}
|
|
|
|
/// <summary>Get path to physical file containing entry.</summary>
|
|
/// <param name="path">Virtual file system path.</param>
|
|
/// <returns>Real file system path or <c>null</c> if real
|
|
/// path doesn't exist.</returns>
|
|
/// <remarks>This may not work with all file systems. Implementation
|
|
/// should return null if real path can't be determined.</remarks>
|
|
public abstract string EntryRealPath(Path path);
|
|
|
|
/// <summary>Copies an existing file or directory to a new location.</summary>
|
|
/// <param name="source">Source path of file or directory.</param>
|
|
/// <param name="destination">Destination path. This path must ba a directory.</param>
|
|
/// <param name="overwrite">If <c>true</c> the destination files and directories
|
|
/// can be overwritten; otherwise, <c>false</c>.</param>
|
|
/// <remarks>This method uses simple yet universal approach.
|
|
/// it's much slower than native one so implementing file system consider
|
|
/// better approach for internal copy.</remarks>
|
|
public virtual void Copy(Path source, Path destination, bool overwrite = false)
|
|
{
|
|
InternalCopy(source, destination, overwrite);
|
|
}
|
|
|
|
/// <summary>Moves an existing file or directory to a new location.</summary>
|
|
/// <param name="source">Source path of file or directory.</param>
|
|
/// <param name="destination">Destination path. This path must ba a directory.</param>
|
|
/// <param name="overwrite">If <c>true</c> the destination files and directories
|
|
/// can be overwritten; otherwise, <c>false</c>. Files that aren't moved
|
|
/// will remain on file system.</param>
|
|
/// <param name="leaveStructure">If <c>true</c> the source directories will be left intact;
|
|
/// otherwise, <c>false</c>.</param>
|
|
/// <remarks>This method uses simple yet universal approach.
|
|
/// it's much slower than native one so implementing file system consider
|
|
/// better approach for internal move.</remarks>
|
|
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<Path, Path> copySuccess = null)
|
|
{
|
|
if (source == destination)
|
|
return;
|
|
|
|
if (source.IsDirectory && !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<Tuple<Directory, Directory>> cpy = new Queue<Tuple<Directory, Directory>>();
|
|
Stack<Tuple<Directory, Directory>> del = new Stack<Tuple<Directory, Directory>>();
|
|
|
|
if (!Exists(destination))
|
|
DirectoryCreate(destination);
|
|
|
|
cpy.Enqueue(new Tuple<Directory, Directory>(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<Directory, Directory>(e as Directory, np ?? GetEntry(newDir) as Directory));
|
|
}
|
|
else
|
|
InternalCopySingleFile(e.Path, dir.Item2.Path, overwrite, copySuccess);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (copySuccess != null)
|
|
{
|
|
Tuple<Directory, Directory> item = null;
|
|
while ((item = del.Pop()) != null)
|
|
copySuccess(item.Item1.Path, item.Item2.Path);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InternalCopySingleFile(Path source, Path destination, bool overwrite, Action<Path, Path> copySuccess)
|
|
{
|
|
var dst = destination.IsDirectory ? destination.Append(source.Name) : destination;
|
|
|
|
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(dst.Parent);
|
|
|
|
if (dir.IsReadOnly || dir.FileSystem.IsReadOnly)
|
|
throw new InvalidOperationException("Can't copy on read only file system.");
|
|
|
|
dir
|
|
.FileCreate(dst.Name)
|
|
.WriteAllBytes(((File)GetEntry(source)).ReadAllBytes());
|
|
|
|
if (copySuccess != null)
|
|
copySuccess(source, dst);
|
|
}
|
|
|
|
/// <summary>Change name of the entry under specified path.</summary>
|
|
/// <param name="path">The path to entry which name will be changed.</param>
|
|
/// <param name="newName">The new name of entry.</param>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get entries located under given path.
|
|
/// </summary>
|
|
/// <param name="path">Path to get.</param>
|
|
/// <param name="mask">Mask to filter out unwanted entries.</param>
|
|
/// <returns>
|
|
/// Returns entry information.
|
|
/// </returns>
|
|
public abstract IEnumerable<Entry> GetEntries(Path path, Regex mask = null);
|
|
|
|
/// <summary>
|
|
/// Get directories located under given path.
|
|
/// </summary>
|
|
/// <param name="path">Path to get.</param>
|
|
/// <param name="mask">Mask to filter out unwanted entries.</param>
|
|
/// <returns>
|
|
/// Returns entry information.
|
|
/// </returns>
|
|
public virtual IEnumerable<Directory> GetDirectories(Path path, Regex mask = null)
|
|
{
|
|
return GetEntries(path, mask)
|
|
.Where(e => e is Directory)
|
|
.Cast<Directory>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get files located under given path.
|
|
/// </summary>
|
|
/// <param name="path">Path to get.</param>
|
|
/// <param name="mask">Mask to filter out unwanted entries.</param>
|
|
/// <returns>
|
|
/// Returns entry information.
|
|
/// </returns>
|
|
public virtual IEnumerable<File> GetFiles(Path path, Regex mask = null)
|
|
{
|
|
return GetEntries(path, mask)
|
|
.Where(e => e is File)
|
|
.Cast<File>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create Directory and return new entry.
|
|
/// </summary>
|
|
/// <param name="path">Path of directory.</param>
|
|
/// <returns>
|
|
/// Created entry.
|
|
/// </returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Deletes the specified directory and, if
|
|
/// indicated, any subdirectories in the directory.</summary>
|
|
/// <param name="path">The path of the directory to remove.</param>
|
|
/// <param name="recursive">Set <c>true</c> to remove directories,
|
|
/// subdirectories, and files in path; otherwise <c>false</c>.</param>
|
|
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.");
|
|
}
|
|
|
|
/// <summary>Creates empty file on file system and returns it's description.</summary>
|
|
/// <param name="path">The path and name of the file to create.</param>
|
|
/// <param name="overwrite">If file exists replace it.</param>
|
|
/// <returns>File description.</returns>
|
|
public virtual File FileTouch(Path path, bool overwrite = false)
|
|
{
|
|
if (path.IsDirectory)
|
|
throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path));
|
|
|
|
if (IsReadOnly)
|
|
throw new InvalidOperationException(string.Format("Can't create file ('{0}') on read only file system.", path));
|
|
|
|
if (!Exists(path.Parent))
|
|
throw new InvalidOperationException("Can't create file in un existent directory path.");
|
|
|
|
if (Exists(path))
|
|
{
|
|
if (overwrite)
|
|
FileDelete(path);
|
|
else
|
|
throw new InvalidOperationException("Can't create file that already exist.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Opens an existing file for reading.</summary>
|
|
/// <param name="path">The file to be opened for reading.</param>
|
|
/// <returns>A read-only <see cref="Stream"/> on the specified path.</returns>
|
|
public virtual Stream FileOpenRead(Path path)
|
|
{
|
|
if (path.IsDirectory)
|
|
throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path));
|
|
|
|
if (!Exists(path))
|
|
throw new InvalidOperationException(String.Format("Can't open not existing file '{0}'. ", path));
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Opens an existing file for writing.</summary>
|
|
/// <param name="path">The file to be opened for writing.</param>
|
|
/// <param name="append">Append file.</param>
|
|
/// <returns>An unshared <see cref="Stream"/> object on
|
|
/// the specified path with write access.</returns>
|
|
public virtual Stream FileOpenWrite(Path path, bool append = false)
|
|
{
|
|
if (path.IsDirectory)
|
|
throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path));
|
|
|
|
if (IsReadOnly)
|
|
throw new InvalidOperationException(string.Format("Can't write file ('{0}') on read only file system.", path));
|
|
|
|
if (!Exists(path))
|
|
throw new InvalidOperationException(String.Format("Can't open not existing file '{0}'. ", path));
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Creates or overwrites a file in the specified path.</summary>
|
|
/// <param name="path">The path and name of the file to create.</param>
|
|
/// <param name="overwrite">If file exists replace it.</param>
|
|
/// <returns>A <see cref="Stream"/> that provides
|
|
/// write access to the file specified in path.</returns>
|
|
public virtual Stream FileCreate(Path path, bool overwrite = false)
|
|
{
|
|
if (path.IsDirectory)
|
|
throw new InvalidOperationException(string.Format("Can't open directory path ('{0}') as file.", path));
|
|
|
|
if (IsReadOnly)
|
|
throw new InvalidOperationException(string.Format("Can't create file ('{0}') on read only file system.", path));
|
|
|
|
if (!Exists(path.Parent))
|
|
throw new InvalidOperationException("Can't create file in un existent directory path.");
|
|
|
|
if (Exists(path))
|
|
{
|
|
if (overwrite)
|
|
FileDelete(path);
|
|
else
|
|
throw new InvalidOperationException("Can't create file that already exist.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Deletes the specified file. An exception is not thrown
|
|
/// if the specified file does not exist.</summary>
|
|
/// <param name="path">The path of the file to be deleted.</param>
|
|
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--;
|
|
}
|
|
|
|
/// <summary>Performs application-defined tasks associated with
|
|
/// freeing, releasing, or resetting unmanaged resources.</summary>
|
|
public virtual void Dispose()
|
|
{
|
|
}
|
|
}
|
|
} |