123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
/*******************************************************************************

        copyright:      Copyright (c) 2007 Kris Bell. All rights reserved

        license:        BSD style: $(LICENSE)

        version:        Oct 2007: Initial version

        author:         Kris

*******************************************************************************/

module tango.io.vfs.FileFolder;

private import tango.io.device.File;

private import Path = tango.io.Path;

private import tango.core.Exception;

public import tango.io.vfs.model.Vfs;

private import tango.io.model.IConduit;

private import tango.time.Time : Time;

/*******************************************************************************

        Represents a physical folder in a file system. Use one of these
        to address specific paths (sub-trees) within the file system.

*******************************************************************************/

class FileFolder : VfsFolder
{
        private const(char)[]   path;
        private VfsStats        stats;

        /***********************************************************************

                Create a file folder with the given path.

                Option 'create' will create the path when set true, 
                or reference an existing path otherwise

        ***********************************************************************/

        this (const(char)[] path, bool create=false)
        {
                this.path = open (Path.standard(path.dup), create);
        }

        /***********************************************************************

                create a FileFolder as a Group member

        ***********************************************************************/

        private this (const(char)[] path, const(char)[] name)
        {
                this.path = Path.join (path, name);
        }

        /***********************************************************************

                explicitly create() or open() a named folder

        ***********************************************************************/

        private this (FileFolder parent, const(char)[] name, bool create=false)
        {
                assert (parent);
                this.path = open (Path.join(parent.path, name), create);
        }

        /***********************************************************************

                Return a short name

        ***********************************************************************/

        @property final const(char)[] name ()
        {
                return Path.parse(path).name;
        }

        /***********************************************************************

                Return a long name

        ***********************************************************************/

        override final string toString ()
        {
                return path.idup;
        }

        /***********************************************************************

                A folder is being added or removed from the hierarchy. Use 
                this to test for validity (or whatever) and throw exceptions 
                as necessary

                Here we test for folder overlap, and bail-out when found.

        ***********************************************************************/

        final void verify (VfsFolder folder, bool mounting)
        {
                if (mounting && cast(FileFolder) folder)
                   {
                   auto src = Path.FS.padded (this.toString());
                   auto dst = Path.FS.padded (folder.toString());

                   auto len = src.length;
                   if (len > dst.length)
                       len = dst.length;

                   if (src[0..len] == dst[0..len])
                       error ("folders '"~dst.idup~"' and '"~src.idup~"' overlap");
                   }
        }

        /***********************************************************************

                Return a contained file representation 

        ***********************************************************************/

        @property final VfsFile file (const(char)[] name)
        {
                return new FileHost (Path.join (path, name));
        }

        /***********************************************************************

                Return a contained folder representation 

        ***********************************************************************/

        @property final VfsFolderEntry folder (const(char)[] path)
        {
                return new FolderHost (this, path);
        }

        /***********************************************************************

                Remove the folder subtree. Use with care!

        ***********************************************************************/

        final VfsFolder clear ()
        {
                Path.remove (Path.collate(path, "*", true));
                return this;
        }

        /***********************************************************************

                Is folder writable?

        ***********************************************************************/

        @property final bool writable ()
        {
                return Path.isWritable (path);
        }

        /***********************************************************************

                Returns content information about this folder

        ***********************************************************************/

        @property final VfsFolders self ()
        {
                return new FolderGroup (this, false);
        }

        /***********************************************************************

                Returns a subtree of folders matching the given name

        ***********************************************************************/

        @property final VfsFolders tree ()
        {
                return new FolderGroup (this, true);
        }

        /***********************************************************************

                Iterate over the set of immediate child folders. This is 
                useful for reflecting the hierarchy

        ***********************************************************************/

        final int opApply (scope int delegate(ref VfsFolder) dg)
        {
                int result;

                foreach (folder; folders(true))
                        {
                        VfsFolder x = folder;
                        if ((result = dg(x)) != 0)
                             break;
                        }
                return result;
        }

        /***********************************************************************

                Close and/or synchronize changes made to this folder. Each
                driver should take advantage of this as appropriate, perhaps
                combining multiple files together, or possibly copying to a 
                remote location

        ***********************************************************************/

        VfsFolder close (bool commit = true)
        {
                return this;
        }

        /***********************************************************************

                Sweep owned folders 

        ***********************************************************************/

        @property private FileFolder[] folders (bool collect)
        {
                FileFolder[] folders;

                stats = stats.init;
                foreach (info; Path.children (path))
                         if (info.folder)
                            {
                            if (collect)
                                folders ~= new FileFolder (info.path, info.name);
                            ++stats.folders;
                            }
                         else
                            {
                            stats.bytes += info.bytes;
                           ++stats.files;
                            }

                return folders;
        }

        /***********************************************************************

                Sweep owned files

        ***********************************************************************/

        private char[][] files (ref VfsStats stats, VfsFilter filter = null)
        {
                char[][] files;

                foreach (info; Path.children (path))
                         if (info.folder is false)
                             if (filter is null || filter(&info))
                                {
                                files ~= Path.join (info.path, info.name);
                                stats.bytes += info.bytes; 
                                ++stats.files;
                                }

                return files;         
        }

        /***********************************************************************

                Throw an exception

        ***********************************************************************/

        private const(char)[] error (string msg)
        {
                throw new VfsException (msg);
        }

        /***********************************************************************

                Create or open the given path, and detect path errors

        ***********************************************************************/

        private const(char)[] open (const(char)[] path, bool create)
        {
                if (Path.exists (path))
                   {
                   if (! Path.isFolder (path))
                       error ("FileFolder.open :: path exists but not as a folder: "~path.idup);
                   }
                else
                   if (create)
                       Path.createPath (path);
                   else
                      error ("FileFolder.open :: path does not exist: "~path.idup);
                return path;
        }
}


/*******************************************************************************

        Represents a group of files (need this declared here to avoid
        a bunch of bizarre compiler warnings)

*******************************************************************************/

class FileGroup : VfsFiles
{
        private const(char)[][] group;          // set of filtered filenames
        private const(char)[][] hosts;          // set of containing folders
        private VfsStats        stats;          // stats for contained files

        /***********************************************************************

        ***********************************************************************/

        this (FolderGroup host, VfsFilter filter)
        {
                foreach (folder; host.members)
                        {
                        auto files = folder.files (stats, filter);
                        if (files.length)
                           {
                           group ~= files;
                           //hosts ~= folder.toString();
                           }
                        }
        }

        /***********************************************************************

                Iterate over the set of contained VfsFile instances

        ***********************************************************************/

        final int opApply (scope int delegate(ref VfsFile) dg)
        {
                int  result;
                auto host = new FileHost;

                foreach (file; group)
                        {
                        VfsFile x = host;
                        host.path = Path.parse(file);
                        if ((result = dg(x)) != 0)
                             break;
                        }
                return result;
        }

        /***********************************************************************

                Return the total number of entries 

        ***********************************************************************/

        @property final size_t files ()
        {
                return group.length;
        }

        /***********************************************************************

                Return the total size of all files 

        ***********************************************************************/

        @property final ulong bytes ()
        {
                return stats.bytes;
        }
}


/*******************************************************************************

        A set of folders representing a selection. This is where file 
        selection is made, and pattern-matched folder subsets can be
        extracted. You need one of these to expose statistics (such as
        file or folder count) of a selected folder group 

*******************************************************************************/

private class FolderGroup : VfsFolders
{
        private FileFolder[] members;           // folders in group

        /***********************************************************************

                Create a subset group

        ***********************************************************************/

        private this () {}

        /***********************************************************************

                Create a folder group including the provided folder and
                (optionally) all child folders

        ***********************************************************************/

        private this (FileFolder root, bool recurse)
        {
                members = root ~ scan (root, recurse);
        }

        /***********************************************************************

                Iterate over the set of contained VfsFolder instances

        ***********************************************************************/

        final int opApply (scope int delegate(ref VfsFolder) dg)
        {
                int  result;

                foreach (folder; members)
                        {
                        VfsFolder x = folder;
                        if ((result = dg(x)) != 0)
                             break;
                        }
                return result;
        }

        /***********************************************************************

                Return the number of files in this group

        ***********************************************************************/

        @property final size_t files ()
        {
                size_t files;
                foreach (folder; members)
                         files += folder.stats.files;
                return files;
        }

        /***********************************************************************

                Return the total size of all files in this group

        ***********************************************************************/

        @property final ulong bytes ()
        {
                ulong bytes;

                foreach (folder; members)
                         bytes += folder.stats.bytes;
                return bytes;
        }

        /***********************************************************************

                Return the number of folders in this group

        ***********************************************************************/

        @property final size_t folders ()
        {
                if (members.length is 1)
                    return members[0].stats.folders;
                return members.length;
        }

        /***********************************************************************

                Return the total number of entries in this group

        ***********************************************************************/

        @property final size_t entries ()
        {
                return files + folders;
        }

        /***********************************************************************

                Return a subset of folders matching the given pattern

        ***********************************************************************/

        final VfsFolders subset (const(char)[] pattern)
        {  
                auto set = new FolderGroup;

                foreach (folder; members)
                         if (Path.patternMatch (Path.PathParser!(const(char))(folder.path).name, pattern))
                             set.members ~= folder;
                return set;
        }

        /***********************************************************************

                Return a set of files matching the given pattern

        ***********************************************************************/

        @property final FileGroup catalog (const(char)[] pattern)
        {
                bool foo (VfsInfo info)
                {
                        return Path.patternMatch (info.name, pattern);
                }

                return catalog (&foo);
        }

        /***********************************************************************

                Returns a set of files conforming to the given filter

        ***********************************************************************/

        @property final FileGroup catalog (VfsFilter filter = null)
        {
                return new FileGroup (this, filter);
        }

        /***********************************************************************

                Internal routine to traverse the folder tree

        ***********************************************************************/

        private final FileFolder[] scan (FileFolder root, bool recurse)
        {
                auto folders = root.folders (recurse);
                if (recurse)
                    foreach (child; folders)
                             folders ~= scan (child, recurse);
                return folders;
        }
}


/*******************************************************************************

        A host for folders, currently used to harbor create() and open() 
        methods only

*******************************************************************************/

private class FolderHost : VfsFolderEntry
{       
        private const(char)[]   path;
        private FileFolder      parent;

        /***********************************************************************

        ***********************************************************************/

        private this (FileFolder parent, const(char)[] path)
        {
                this.path = path;
                this.parent = parent;
        }

        /***********************************************************************

        ***********************************************************************/

        final VfsFolder create ()
        {
                return new FileFolder (parent, path, true);
        }

        /***********************************************************************

        ***********************************************************************/

        final VfsFolder open ()
        {
                return new FileFolder (parent, path, false);
        }

        /***********************************************************************

                Test to see if a folder exists

        ***********************************************************************/

        @property bool exists ()
        {
                try {
                    open();
                    return true;
                    } catch (IOException x) {}
                return false;
        }
}


/*******************************************************************************

        Represents things you can do with a file 

*******************************************************************************/

private class FileHost : VfsFile
{
        private Path.PathParser!(const(char)) path;

        /***********************************************************************

        ***********************************************************************/

        this (const(char)[] path = null)
        {
                this.path = Path.PathParser!(const(char))(path);
        }

        /***********************************************************************

                Return a short name

        ***********************************************************************/

        @property final const(char)[] name()
        {
                return path.file;
        }

        /***********************************************************************

                Return a long name

        ***********************************************************************/

        override final string toString ()
        {
                return path.toString().idup;
        }

        /***********************************************************************

                Does this file exist?

        ***********************************************************************/

        @property final bool exists()
        {
                return Path.exists (path.toString());
        }

        /***********************************************************************

                Return the file size

        ***********************************************************************/

        @property final ulong size()
        {
                return Path.fileSize(path.toString());
        }

        /***********************************************************************

                Create a new file instance

        ***********************************************************************/

        final VfsFile create ()
        {
                Path.createFile(path.toString());
                return this;
        }

        /***********************************************************************

                Create a new file instance and populate with stream

        ***********************************************************************/

        final VfsFile create (InputStream input)
        {
                create().output.copy(input).close();
                return this;
        }

        /***********************************************************************

                Create and copy the given source

        ***********************************************************************/

        VfsFile copy (VfsFile source)
        {
                auto input = source.input;
                scope (exit) input.close();
                return create (input);
        }

        /***********************************************************************

                Create and copy the given source, and remove the source

        ***********************************************************************/

        final VfsFile move (VfsFile source)
        {
                copy (source);
                source.remove();
                return this;
        }

        /***********************************************************************

                Return the input stream. Don't forget to close it

        ***********************************************************************/

        @property final InputStream input ()
        {
                return new File (path.toString());
        }

        /***********************************************************************

                Return the output stream. Don't forget to close it

        ***********************************************************************/

        @property final OutputStream output ()
        {
                return new File (path.toString(), File.WriteExisting);
        }

        /***********************************************************************

                Remove this file

        ***********************************************************************/

        final VfsFile remove ()
        {
                Path.remove (path.toString());
                return this;
        }

        /***********************************************************************

                Duplicate this entry

        ***********************************************************************/

        @property final VfsFile dup()
        {
                auto ret = new FileHost;
                ret.path = path.dup;
                return ret;
        }
        
        /***********************************************************************

                Modified time of the file

        ***********************************************************************/

        @property final Time modified ()
        {
                return Path.timeStamps(path.toString()).modified;
        }
}


debug (FileFolder)
{

/*******************************************************************************

*******************************************************************************/

import tango.io.Stdout;
import tango.io.device.Array;

void main()
{
        auto root = new FileFolder ("d:/d/import/temp", true);
        root.folder("test").create;
        root.file("test.txt").create(new Array("hello"));
        Stdout.formatln ("test.txt.length = {}", root.file("test.txt").size);

        root = new FileFolder ("c:/");
        auto set = root.self;

        Stdout.formatln ("self.files = {}", set.files);
        Stdout.formatln ("self.bytes = {}", set.bytes);
        Stdout.formatln ("self.folders = {}", set.folders);
        Stdout.formatln ("self.entries = {}", set.entries);
/+
        set = root.tree;
        Stdout.formatln ("tree.files = {}", set.files);
        Stdout.formatln ("tree.bytes = {}", set.bytes);
        Stdout.formatln ("tree.folders = {}", set.folders);
        Stdout.formatln ("tree.entries = {}", set.entries);

        //foreach (folder; set)
        //Stdout.formatln ("tree.folder '{}' has {} files", folder.name, folder.self.files);

        auto cat = set.catalog ("s*");
        Stdout.formatln ("cat.files = {}", cat.files);
        Stdout.formatln ("cat.bytes = {}", cat.bytes);
+/
        //foreach (file; cat)
        //         Stdout.formatln ("cat.name '{}' '{}'", file.name, file.toString());
}
}