File system manipulation helper

Posted on

Problem

What do you think about this file system manipulation helper? There is an utility class Folder which I can use to define directory structure of my app:

static class Folders
{
    public static Folder Bin =>
        new Folder(Assembly.GetExecutingAssembly());

    public static Folder App => Bin.Up();
    public static Folder Docs => App.Down("Docs");
    public static Folder Temp => App.Down("Temp");
}

Then I can do some manipulations in an easy way:

    static void Main(string[] args)
    {
        Folders.Temp.Create();
        Folders.Bin.Run("scan.exe.", "-no-ui");
        Folders.Temp.CopyTo(Folders.Docs, f => f.EndsWith(".pdf"));
        Folders.Temp.Empty();
    }

Here is the library class Folder used above:

public class Folder
{
    readonly string _path;

    public Folder(Assembly assembly) 
        : this(Path.GetDirectoryName(assembly.Location))
    {
    }

    public Folder(string path)
    {
        _path = path;
    }

    public override string ToString() => _path;

    public static implicit operator string(Folder folder) =>
        folder.ToString();

    public IEnumerable<Folder> Folders =>
        Directory.GetDirectories(this, "*", SearchOption.TopDirectoryOnly)
            .Select(p => new Folder(p));

    public IEnumerable<Folder> AllFolders =>
        Directory.GetDirectories(this, "*", SearchOption.AllDirectories)
            .Select(p => new Folder(p));

    public IEnumerable<string> Files =>
        Directory.GetFiles(this, "*.*", SearchOption.TopDirectoryOnly);

    public IEnumerable<string> AllFiles =>
        Directory.GetFiles(this, "*.*", SearchOption.AllDirectories);

    public Folder Up() =>
        new Folder(
            new DirectoryInfo(this)
                .Parent.FullName);

    public Folder Down(string folderName) =>
        new Folder(
            Path.Combine(
                this,
                folderName));

    public void Create() =>
        Directory.CreateDirectory(this);

    public void Empty()
    {
        var directoryInfo = new DirectoryInfo(this);
        if (!directoryInfo.Exists)
            return;

        foreach (var file in AllFiles)
            File.Delete(file);

        foreach (var folder in Folders)
            Directory.Delete(folder, true);
    }

    public void CopyTo(Folder destination) =>
        CopyTo(destination, file => true);

    public void CopyTo(Folder destination, Func<string, bool> filter)
    {
        //Create directories
        foreach (string directoryPath in AllFolders)
            Directory.CreateDirectory(directoryPath.Replace(this, destination));

        //Copy all the files & replaces any files with the same name
        foreach (string filePath in AllFiles.Where(filter))
            File.Copy(filePath, filePath.Replace(this, destination), true);
    }

    public void Run(string exe, string args = "")
    {
        string defaultCurrentDirectory = Environment.CurrentDirectory;
        Environment.CurrentDirectory = this;
        try
        {
            var process = new Process();
            process.StartInfo.FileName = exe;
            process.StartInfo.Arguments = args;
            process.Start();
            process.WaitForExit();
            if (process.ExitCode != 0)
                throw new InvalidOperationException(
                    $"{exe} process failed with exit code {process.ExitCode}.");
        }
        finally
        {
            Environment.CurrentDirectory = defaultCurrentDirectory;
        }
    }
}

Solution

I’m always a fan of using interfaces wherever possible so that unit tests can mock out my component easily. So let’s create a couple of interfaces:

public interface IFolder
{
    IEnumerable<IFolder> Folders { get; }

    IEnumerable<IFolder> AllFolders { get; }

    IEnumerable<string> Files { get; }

    IEnumerable<string> AllFiles { get; }

    IFolder Up();

    IFolder Down(string folderName);

    void Create();

    void Empty();

    void CopyTo(IFolder destination);

    void CopyTo(IFolder destination, Func<string, bool> filter);

    void Run(string exe, string args = "");
}

and

internal interface IFolders
{
    IFolder Bin { get; }

    IFolder App { get; }

    IFolder Docs { get; }

    IFolder Temp { get; }
}

I also like to re-use constants. And I added another implicit operator to go with your new constructor:

public class Folder : IFolder
{
    private const string DirectoryWildcard = "*";

    private const string FileWildcard = "*.*";

    private readonly string _Path;

    public Folder(Assembly assembly)
        : this(Path.GetDirectoryName(assembly.Location))
    {
    }

    public Folder(string path)
    {
        this._Path = path;
    }

    public override string ToString() => this._Path;

    public static implicit operator string(Folder folder) => folder.ToString();

    public static implicit operator Folder(string path) => new Folder(path);

    public static implicit operator Folder(Assembly assembly) => new Folder(assembly);

    public IEnumerable<IFolder> Folders => Directory
        .GetDirectories(this, DirectoryWildcard, SearchOption.TopDirectoryOnly)
        .Select(path => new Folder(path));

    public IEnumerable<IFolder> AllFolders => Directory
        .GetDirectories(this, DirectoryWildcard, SearchOption.AllDirectories)
        .Select(path => new Folder(path));

    public IEnumerable<string> Files => Directory.GetFiles(this, FileWildcard, SearchOption.TopDirectoryOnly);

    public IEnumerable<string> AllFiles => Directory.GetFiles(this, FileWildcard, SearchOption.AllDirectories);

    public IFolder Up() => new Folder(new DirectoryInfo(this).Parent.FullName);

    public IFolder Down(string folderName) => new Folder(Path.Combine(this, folderName));

    public void Create() => Directory.CreateDirectory(this);

    public void Empty()
    {
        var directoryInfo = new DirectoryInfo(this);

        if (!directoryInfo.Exists)
        {
            return;
        }

        foreach (var file in AllFiles)
        {
            File.Delete(file);
        }

        foreach (var folder in Folders)
        {
            Directory.Delete(folder.ToString(), true);
        }
    }

    public void CopyTo(IFolder destination) => this.CopyTo(destination, file => true);

    public void CopyTo(IFolder destination, Func<string, bool> filter)
    {
        // Create directories
        foreach (var folder in this.AllFolders)
        {
            Directory.CreateDirectory(folder.ToString().Replace(this, destination.ToString()));
        }

        // Copy all the files & replaces any files with the same name
        foreach (var filePath in this.AllFiles.Where(filter))
        {
            File.Copy(filePath, filePath.Replace(this, destination.ToString()), true);
        }
    }

    public void Run(string exe, string args = "")
    {
        var defaultCurrentDirectory = Environment.CurrentDirectory;

        Environment.CurrentDirectory = this;
        try
        {
            var process = new Process { StartInfo = { FileName = exe, Arguments = args } };

            process.Start();
            process.WaitForExit();
            if (process.ExitCode != 0)
            {
                throw new InvalidOperationException(
                    $"{exe} process failed with exit code {process.ExitCode}.");
            }
        }
        finally
        {
            Environment.CurrentDirectory = defaultCurrentDirectory;
        }
    }
}

And finally, the Folders.cs implementation:

internal class Folders : IFolders
{
    private readonly Assembly _Assembly;

    public Folders(Assembly assembly = null)
    {
        this._Assembly = assembly ?? Assembly.GetExecutingAssembly();
    }

    public static IFolders Default => new Folders();

    public IFolder Bin => new Folder(this._Assembly);

    public IFolder App => this.Bin.Up();

    public IFolder Docs => this.App.Down("Docs");

    public IFolder Temp => this.App.Down("Temp");
}

Now that it’s not static, you’ll either have to create a new one with a particular assembly, or Folders.Default will have the current assembly as per original design.

Leave a Reply

Your email address will not be published. Required fields are marked *