Files
VirtualFS/VirtualFS/Base/BaseFileSystem.cs

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()
{
}
}
}