Click here to Skip to main content
15,885,546 members
Articles / Programming Languages / C++11

Extending boost::filesystem for Windows and Linux: Part 1

Rate me:
Please Sign up or sign in to vote.
4.93/5 (15 votes)
19 Mar 2013CPOL16 min read 31.2K   977   33  
Extending boost::filesystem for Windows and Linux.
/****************************************************************************************
 *                                                                                      *
 *                          Written by Stanic Igor 2012/2013                            *
 *  This file contains examples of what FileSystemController and supporting classes     *
 *  can do and basic examples how to use it. It is the linking part between console     *
 *  frontend and underlying functionalities provided by FileSystemController.           *
 *  Most of functionalities are implemented as static functions. You need to actually   *
 *  instantiate FileSystemController only for purposes of underlying FileSystemWatcher  *
 *  class. This will automate the cleanup process on D-TOR calling once we don't need   *
 *  file system event notifications any more.                                           *
 *                                                                                      *
 ****************************************************************************************/

#ifndef COMMAND_PROCESSOR_H
#define COMMAND_PROCESSOR_H
#include "BaseDecl.h"

#ifdef WINDOWS
#include "CustomLocale.hpp"
#endif
#include <boost/program_options.hpp>
#include "FileSystemController.h"


using namespace std;
namespace bpo = boost::program_options;

#ifdef WINDOWS
#define TCOUT  wcout


class NotifyingClass
{
public:
    void process(EventData ek)
    {
        wstring action;
        switch(ek.first)
        {
            case EventKind::added:
            action = L"File Added";
            break;
            case EventKind::removed:
            action = L"File Removed";
            break;
            case EventKind::modified:
            action = L"File Modified";
            break;
            case EventKind::oldName:
            action = L"File Renamed - Old Name";
            break;
            case EventKind::newName:
            action = L"File Renamed - New Name";
            break;
            case EventKind::watcherSignOut:
            action = L"Watcher Signing Out!";
            break;
        }

        wcout <<L"**********************************************************************************" << endl;
        wcout << TO_STRING_TYPE("EVENT NOTIFY ON ACTION ->  ") << action << L" FOR FILE/FOLDER: " << ek.second << endl;
        wcout <<L"**********************************************************************************" << endl;
    }
};
#elif defined(LINUX)
#define TCOUT  cout

class NotifyingClass
{
public:
    void process(EventData ek)
    {


        string event;
        switch(ek.first)
        {
        case EventKind::fileRead:
            event = "Read File Event";
            break;
        case EventKind::fileModify:
            event = "Modify File Event";
            break;
        case EventKind::attribChange:
            event = "Attribute Change Event";
            break;
        case EventKind::openFile:
            event = "Opening File Event";
            break;
        case EventKind::fileClosed:
            event = "Closing File Event";
            break;
        case EventKind::fileReadClosed:
            event = "File Read Closed Event";
            break;
        case EventKind::movedFrom:
            event = "Moved From Event";
            break;
        case EventKind::movedTo:
            event = "Moved To Event";
            break;
        case EventKind::created:
            event = "Created Event";
            break;
        case EventKind::deleted:
            event = "Deleted Event";
            break;
        case EventKind::watchTargetDeleted:
            event = "Target Watch Deleted Event";
            break;
        case EventKind::watchTargetMoved:
            event = "Target Watch Moved Event";
            break;
        case EventKind::watcherSignOut:
            event = "SignOut Event";
            break;
        }

        cout << "****************************************************" << endl;
        cout << "Event ocurred -> " << event << " ON FILE/FOLDER: " << ek.second << endl;
        cout << "****************************************************" << endl;


    }
};
#endif

void Print(const STRING_TYPE& str)
{
    TCOUT <<CHRARR_CON("--------------------------------------------------------------------")<< endl;
    TCOUT << str << endl;
    TCOUT <<CHRARR_CON("--------------------------------------------------------------------")<< endl;
}

inline STRING_TYPE PrintSize(ULLONG size)
{

    STRING_TYPE addition = TO_STRING_TYPE(" bytes");
    STRING_TYPE result;
    if(size > 10*1024 && size < 800 * 1024)
    {
        addition = TO_STRING_TYPE(" Kb");
        result = Helpers::ToString<double,STR_SET>((double)size/1024) + addition;
    }
    else if(size >= 800 * 1024 && size < 800 * 1024 * 1024)
    {
        addition = TO_STRING_TYPE(" Mb");
        result = Helpers::ToString<double,STR_SET>((double)size/(1024 * 1024)) + addition;
    }
    else if( size >= 800 * 1024 * 1024)
    {
        addition = TO_STRING_TYPE(" Gb");
        result = Helpers::ToString<double,STR_SET>((double)size/(1024 * 1024 * 1024)) + addition;
    }
    else
        result = Helpers::ToString<ULLONG,STR_SET>(size) + addition;

    return result;
}

inline void PrintResults(fInfosPtr results, bool verbose = false)
{
    vector<F_INFO>::iterator iter;
    if(results == nullptr) TCOUT << CHRARR_CON("NO RESULTS!") << endl;
    for(iter = results->begin(); iter != results->end(); ++iter)
    {
        if(verbose)
            TCOUT << CHRARR_CON("-------------------------------------------------") << endl;
        TCOUT << (iter->isDirectory ? TO_STRING_TYPE("Directory-> ") + iter->path : TO_STRING_TYPE("File-> ") + iter->path);
        if(verbose)
            TCOUT << endl;
        if(verbose)
        {
              TCOUT <<  CHRARR_CON(" Size: ") << (iter->isDirectory ? PrintSize(FileSystemController::GetFileOrFolderSize(iter->path)) :
                                                                           PrintSize(iter->length)) << endl;
        }
        else
        {
            if(!iter->isDirectory)
                TCOUT <<  CHRARR_CON(" Size: ") << PrintSize(iter->length) << endl;
            else
                TCOUT << endl;
        }

        if(verbose)
        {
            TCOUT
#ifdef LINUX
              << (iter->isSymLink? TO_STRING_TYPE("** Symbolic Link ** "): TO_STRING_TYPE("** Regular File/Folder **"))
              << std::endl
              << (verbose && iter->isSymLink?  TO_STRING_TYPE(" Underlying Link: ") + iter->sym_Link_Path : EMPTYSTR)
#endif
                 << std::endl
                 << (verbose ? (TO_STRING_TYPE(" Last Written on:") + TO_STRING_TYPE(boost::posix_time::to_simple_string(iter->time))) : EMPTYSTR) << endl;

#ifdef WINDOWS
            if(iter->isArchive())
                TCOUT <<  TO_STRING_TYPE("Archive") << TO_STRING_TYPE(",");
            if(iter->isCompressed())
                TCOUT << TO_STRING_TYPE("Compressed") << TO_STRING_TYPE(",");
            if(iter->isEncrypted())
                TCOUT << TO_STRING_TYPE("Encrypted") << TO_STRING_TYPE(",");
            if(iter->isHidden())
                TCOUT << TO_STRING_TYPE("Hidden") <<  TO_STRING_TYPE(",");
            if(iter->isOffline())
                TCOUT << TO_STRING_TYPE("Offline") <<  TO_STRING_TYPE(",");
            if(iter->isReadOnly())
                TCOUT << TO_STRING_TYPE("ReadOnly") <<  TO_STRING_TYPE(",");
            if(iter->isSystem())
                TCOUT << TO_STRING_TYPE("System") <<  TO_STRING_TYPE(",");
            if(iter->isTemporary())
                TCOUT << TO_STRING_TYPE("Temporary") << endl;
            TCOUT << endl;// << CHRARR_CON("***********************") << endl;
#elif defined(LINUX)
            //TODO: add if needed
            //if(iter->file_statistics.st_mode & S_ISVTX )
                //TCOUT << CHRARR_CON("Has sticky bit") << endl;
            if(iter->isHidden())
                TCOUT << "Hidden" << endl;
            if(iter->permission_denied())
                TCOUT << "Permission Denied" << endl;
#endif
        }
    }

    Print(TO_STRING_TYPE("Number of entries: ") + Helpers::ToString<vector<F_INFO>::size_type, STR_SET>(results->size()));
}

#ifdef LINUX
inline void PrintTrashResults(fInfosPtr results)
{
    int count = 0;
    for(auto iter = results->begin(); iter != results->end(); ++iter)
    {
        TCOUT << CHRARR_CON("-----------------------") << endl;
        TCOUT << ++count << ". " << (iter->isDirectory ? TO_STRING_TYPE("Directory-> ") + iter->path : TO_STRING_TYPE("File-> ") + iter->path)
              /**/
              << (iter->isSymLink? TO_STRING_TYPE(" ** Symbolic Link ** "): EMPTYSTR)
                 << TO_STRING_TYPE(" Last Written on: ") << TO_STRING_TYPE(boost::posix_time::to_simple_string(iter->time)) << endl;


        TCOUT << CHRARR_CON(" Size: ") << PrintSize(FileSystemController::GetFileOrFolderSize(iter->orgTrashPath)) << endl;
    }

    Print(TO_STRING_TYPE("Number of results: ") + Helpers::ToString<vector<F_INFO>::size_type, STR_SET>(results->size()));
}
#endif

inline void PrintErrors(const OpResults& res)
{
    if(res.hasErrors())
    {
        OpResults errColl = res.errors();
        for(const OperationResult& r:errColl)
            TCOUT << TO_STRING_TYPE("Error-> ") << TO_STRING_TYPE("Error Message: ") << r.errorMsg << endl;
    }
}


void WaitUntil(const function<bool(const std::string&)>& pred)
{
    //wcin.ignore() doesn't seem to work with current version of MINGW 32
    std::string opt;
    while(!pred(opt))
    {
        cin >> opt;
        cin.ignore(INT_MAX, '\n');
    }
}

vector<STRING_TYPE> separatePaths(const std::string& arg)
{
    auto vTemp = Helpers::splitString(arg, ';');
#ifdef WINDOWS
    vector<STRING_TYPE> toRet(vTemp.size());
    transform(vTemp.begin(), vTemp.end(), toRet.begin(),[](const std::string& s)
    {
        return TO_STRING_TYPE(s);
    });

    return toRet;
#elif defined(LINUX)
    return vTemp;
#endif
}




OpResults exec_commands(bpo::variables_map& vmap)
{
    OpResults toReturn;
#ifdef WINDOWS
    //This is some custom implementation which will at least
    //show unicode char representations like sSzZcC...etc.
    //It's terrible but couldn't find anything else that works...
    custom_locale::init();
#endif

//test_wildcard BEGIN
    if(vmap.count("test_wildcard"))
    {
        STRING_TYPE path(TO_STRING_TYPE(vmap["test_wildcard"].as<string>()));

        //Actual resolving file/foledr paths represented by '*' wildcard combinations
        //Supported combinations are *.* | x*.* | *.y | x*.y | *x.y | *x*.y | where
        // 'x' is a part of file/folder name and 'y' is file extension defined
        // Only *.* and x*.* apply to both files and folders, other combinations
        //can be used to express multiple files only.
        vector<STRING_TYPE> resolved_paths(FileSystemController::TestWCard(path));

        Print(TO_STRING_TYPE("Resolving entered path with \'*\' wildcard(s):"));
        for(auto& p : resolved_paths)
            TCOUT << p << endl;

        TCOUT << endl;

        return toReturn;
    }
//test_wildcard END

//FileSystemWatcher BEGIN
    if(vmap.count("watch_location") || vmap.count("watch_here"))
    {
        vector<STRING_TYPE> locations;

        if(vmap.count("watch_here"))
            locations.push_back(CHRARR_CON(""));
        else
        {
            locations = separatePaths(vmap["watch_location"].as<string>());
            if(locations.empty())//OperationResult(bool result, T1&& source, T2&& errorMsg)
            {
                toReturn.AddResult(OperationResult(false, CHRARR_CON("watch_location"),
                                            CHRARR_CON("you didn't provide any locations to watch")));
                return toReturn;
            }

            for(const auto& loc : locations)
            {
                if(!boost::filesystem::exists(loc))
                {
                    toReturn.AddResult(OperationResult(false, CHRARR_CON("watch_location"), CHRARR_CON("invalid location provided")));
                    return toReturn;
                }
            }
        }

        //Set the kind of changes you want to watch in specified location. BasicChanges will handle most of expected stuff
        //like file/folder deletion, renaming, creation and such. If you want to be able to watch changes like attribute/permission
        //changes on Windows or file openread/openwrite on Linux use other enum option
        ChangeKind changesWatch = ((bool)vmap.count("watch_full") ? ChangeKind::FullChanges : ChangeKind::BasicChanges);

        //This functor will be used as an event handler.
        //Please note that you will probably need to implement some kind
        //of Consumer implementation to work properly in more serious
        //envirnment where events need to be processed sequentially.
        std::unique_ptr<NotifyingClass> notificator(new NotifyingClass);

        //Container for storing multiple instances of FileSystemController.
        //Each instance can watch only one file location at the time
        vector<unique_ptr<FileSystemController> > controllers;

        for(const auto& loc : locations)
        {
            try
            {
                //initialize instance of FileSystemController with path to location we want to watch
                //and changes we want to pay attention for.
                unique_ptr<FileSystemController> cont(new FileSystemController(loc, changesWatch));

                //Get internal instance of FileSystemWatcher
                FileSystemWatcher* fsw = cont->getFSWatcher();

                //Register event handler for handling filesystem notifications
                fsw->RegisterListener(notificator.get(), &NotifyingClass::process);

                Print(TO_STRING_TYPE("Starting Location Watching on path: ") + loc);

                //If there are errors which occured during initialization of FileSystemWatcher on
                //location specified, get them.
                if(cont->getFSWatcherErrors().hasErrors())
                {
                    toReturn += cont->getFSWatcherErrors();
                }

                controllers.push_back(std::move(cont));
            }
            catch(...)
            {
                //Add results to container dedicated to handle operation outcomes
                toReturn.AddResult(OperationResult(false, TO_STRING_TYPE("Initialization of FileSytemController"),
                                                   TO_STRING_TYPE("Undefined error")));
            }
        }

        function<bool(const std::string&)> func = [&controllers, &toReturn](const std::string& str)
        {
            std::string instr(str);
            if(!str.empty())
            {
                instr = Helpers::toLower(instr);
                if(instr == "q" || instr == "quit")
                {
                    for(auto& cont : controllers)
                    {
                        toReturn += cont.get()->getFSWatcherErrors();
                    }

                    //We don't actually need to do anything to stop watching file system location
                    //over FileSystemWatcher instance. All we need is make it go out of scope
                    //and that its D-TOR is called which will do appropriate cleanup
                    return true;
                }
                else if(instr == "p")
                {
                    for(auto& cont : controllers)
                    {
                        //Pause WATCHING on specific location
                        cont.get()->getFSWatcher()->setPaused(!cont.get()->getFSWatcher()->getPaused());
                    }
                }
                else
                    Print(TO_STRING_TYPE("Don't know what is: ") + TO_STRING_TYPE(instr) + CHRARR_CON(" Available options: q or quit and p for pause notification start/stop"));
            }

            return false;
        };

        if(!toReturn.hasErrors())
            WaitUntil(func);

        return toReturn;
    }
//FileSystemWatcher END

//list_trash Implementation BEGIN
        if(vmap.count("list_trash"))
        {

#ifdef WINDOWS

            //List recovery paths of items in Recycle Bin in case of Windows OS
            vector<std::wstring> recoveryPaths(FileSystemController::ListRecycleBin());
            int count = 0;
            Print(CHRARR_CON("Trash/Recycle Bin content:"));
            for_each(recoveryPaths.begin(), recoveryPaths.end(), [&count](const wstring& path)
            {
                    TCOUT << ++count << CHRARR_CON(".\t");
                    TCOUT << TO_STRING_TYPE("Recycle Bin Recovery path: ") << path << endl;
            });
#elif defined(LINUX)

            //List $Trash contents on linux. In case of Linux we have greater control over what
            //info we can get since the files are stored in usual way on hidden location.
            unique_ptr<vector<F_INFO> > recoveryPaths(FileSystemController::ListTrash(&toReturn));
            PrintTrashResults(recoveryPaths.get());
#endif
            return toReturn;
        }
//list_trash Implementation END
//list items on location BEGIN
        if(vmap.count("list") || vmap.count("l"))
        {
            STRING_TYPE listPath(TO_STRING_TYPE(vmap["list"].as<string>()));

            if(!listPath.empty())//this means list current location
            {
                if(!boost::filesystem::exists(listPath))
                {
                    toReturn.AddResult(OperationResult(false, listPath, CHRARR_CON("Location provided doesn't exist")));
                    return toReturn;
                }
            }

            //Define if hidden/system file system items should be present in the collection also
            bool allEntries = false;
            if(vmap.count("all"))
                allEntries = true;

            //Define how retrieved items should be sorted
            //over _SortInfo enum value
            _SortInfo sorting = _SortInfo::dirFirst_asc;
            if(vmap.count("desc"))
                sorting = _SortInfo::dirFirst_desc;

            //Get actual file/folder infos for specific location
            unique_ptr<vector<F_INFO> > files(FileSystemController::GetEntries(listPath, sorting, allEntries));
            PrintResults(files.get(), vmap.count("v"));
            return toReturn;
        }
//list items on location END
//make_path BEGIN
        if(vmap.count("make_path"))
        {
            vector<STRING_TYPE> valsMP = separatePaths(vmap["make_path"].as<string>());

            //Build path for each entry separated by ';'
            //If path already exists function won't do any changes.
            //Otherwise, it will build the missing parts of the path
            for(auto& path : valsMP)
                toReturn += FileSystemController::BuildPath(path);

            return toReturn;
        }
//make_path END
//copy/move option BEGIN
        if(vmap.count("copy") || vmap.count("move"))
        {
            string from;
            STRING_TYPE to;
            if(vmap.count("from"))
                from = vmap["from"].as<string>();
            if(vmap.count("to"))
                to = TO_STRING_TYPE((vmap["to"].as<string>()));

            string cpmvOpt;
            if(vmap.count("copy"))
                cpmvOpt = "copy";
            else
                cpmvOpt = "move";

            bool overwrite = false;
            bool merge = false;
            if(vmap.count("overwrite"))
                overwrite = true;
            if(vmap.count("merge"))
                merge = true;

            vector<STRING_TYPE> valsCM = separatePaths(from);
            if(valsCM.empty() || to.empty())
            {
                toReturn.AddResult(OperationResult(false, CHRARR_CON("from/to option"), CHRARR_CON("path isn't defined")));
                return toReturn;
            }

            //Copy and Move calls. First two args define source and destination.
            //Overwrite will overwite everything exisiting with the same name on destination location.
            //Merge will ensure that all item missing on destination location are added from source, and
            //all already present will stay unchanged after operation is done.
            for(auto& path : valsCM)
            {
                if(cpmvOpt == "copy")
                    toReturn += FileSystemController::Copy(path, to, overwrite, merge);
                else
                    toReturn += FileSystemController::Move(path, to, overwrite);//move doesn't allow merging
            }
            return toReturn;
        }
//copy/move option END

//Delete option BEGIN
        if(vmap.count("pdel"))
        {
            vector<STRING_TYPE> valsPDEL = separatePaths(vmap["pdel"].as<string>());
            for(auto& path : valsPDEL)
            {
                //Permanent delete item on defined destination
                toReturn += FileSystemController::Delete(path);
            }

            return toReturn;
        }
//Delete option END
#ifdef WINDOWS
//Move to R.Bin BEGIN

        if(vmap.count("to_trash"))
        {
            vector<STRING_TYPE> paths = separatePaths(vmap["to_trash"].as<string>());
            if(paths.empty())
            {
                toReturn.AddResult(OperationResult(false, TO_STRING_TYPE("to_trash option"),
                                   TO_STRING_TYPE("no valid paths")));
                return toReturn;
            }

            //Delete each location to Recycle Bin on Windows OS
            for(auto& p : paths)
                toReturn += FileSystemController::MoveToRecycleBin(p);

            return toReturn;
        }

//Move to R.Bin END
#elif defined(LINUX)
        if(vmap.count("to_trash"))
        {
            vector<STRING_TYPE> paths = separatePaths(vmap["to_trash"].as<string>());
            if(paths.empty())
            {
                toReturn.AddResult(OperationResult(false, TO_STRING_TYPE("to_trash option"),
                                   TO_STRING_TYPE("no valid paths")));
                return toReturn;
            }

            //Delete each location to Trash on Linux OS
            for(auto& p : paths)
                toReturn += FileSystemController::MoveToTrash(p);

            return toReturn;
        }

        if(vmap.count("size_trash"))
        {
            //Get summary of Trash content size on Linux OS
            auto trSize = FileSystemController::getTrashSize();
            Print(TO_STRING_TYPE("Current Trash size is:") + PrintSize(trSize));
            return toReturn;
        }

        if(vmap.count("from_trash"))
        {
            string restorationPath;
            string alternatePath;

            int restorationPathNumber = vmap["from_trash"].as<int>();
            if(restorationPathNumber < 1)
            {
                toReturn.AddResult(OperationResult(false, "from_trash option", "value is invalid"));
                return toReturn;
            }

            if(vmap.count("restore_path"))
            {
                alternatePath = vmap["restore_path"].as<string>();
                if(alternatePath.empty())
                {
                    toReturn.AddResult(OperationResult(false, "restore_path option", "value is invalid"));
                    return toReturn;
                }

            }

            unique_ptr<vector<F_INFO> > recoveryPaths(FileSystemController::ListTrash(&toReturn));
            if(toReturn.hasErrors())
                return toReturn;

            if(restorationPathNumber > (int)recoveryPaths->size())
            {
                toReturn.AddResult(OperationResult(false, "from_trash option", "value is invalid"));
                return toReturn;
            }

            F_INFO restorer = recoveryPaths->operator[](restorationPathNumber - 1);

            //Restore item from Trash on Linux OS. First arg is actual location of deleted file in Trash, overwrite bool arg defines if
            //item can be restored over presently existing one on recovery location and alternate path can be optionally used to define some
            //other more convenient path for item to be restored on.
            toReturn += FileSystemController::RestoreFile(restorer.orgTrashPath, (bool)vmap.count("overwrite"), (alternatePath.empty() ? EMPTYSTR : alternatePath));
            return toReturn;
        }

//undelete from trash;
#endif
//Rename BEGIN

        if(vmap.count("rn"))
        {
            STRING_TYPE from;
            STRING_TYPE to;
            if(vmap.count("from"))
                from = TO_STRING_TYPE(vmap["from"].as<string>());
            if(vmap.count("to"))
                to = TO_STRING_TYPE((vmap["to"].as<string>()));

            if(from.empty() || to.empty())
            {
                toReturn.AddResult(OperationResult(false, TO_STRING_TYPE("to/from options"), TO_STRING_TYPE("source or destination are empty")));
                return toReturn;
            }

            //Reneame file system file/folder. Second arg accepts ONLY NAME, not the full path as in case of other functions
            toReturn += FileSystemController::Rename(from, to);
            return toReturn;
        }

//Rename END
//SEARCH BEGIN

        if(vmap.count("search_loc"))
        {

            STRING_TYPE searchPath(TO_STRING_TYPE(vmap["search_loc"].as<string>()));
            if(!vmap.count("search_string"))
            {
                toReturn.AddResult(OperationResult(false, TO_STRING_TYPE("search_string option"),
                                                   TO_STRING_TYPE("you have to define at least search_loc and search_string options")));
                return toReturn;
            }

            STRING_TYPE searchFor(TO_STRING_TYPE(vmap["search_string"].as<string>()));
            if(searchFor.empty())
            {
                toReturn.AddResult(OperationResult(false, TO_STRING_TYPE("search_string option"),
                                                   TO_STRING_TYPE("you have to define at least search_loc and search_string options")));
                return toReturn;
            }

            //Create search criteria to filter file system items. You can define string to search for, if that string is part of name or
            //the whole path, if search is case-sensitive(by default it isn't), if you search for exact match to file name and
            //you can shorten search time in case you expect only couple of matches, spacially in case when start search location is
            //something as '/' or 'C:\'
            SearchCriteria sc(searchFor, !(bool)vmap.count("search_path"), (bool)vmap.count("case_sensitive"),
                              (bool)vmap.count("exact_match"), (int)vmap["result_count"].as<UINT>());

            Print(TO_STRING_TYPE("Searching for requested results:"));

            //Get file infos as a result of search operation with specific criteria
            //You can define search criteria as SearchCriteria and SearchFunction(contains function<> object with defined predicate)
            //Also, there is another overload of FindFile() function which will create SearchCriteria object for you.
            unique_ptr<vector<F_INFO> > finfos(FileSystemController::FindFile(searchPath, (SearchWrapperBase*)&sc, (bool)vmap.count("all")));
            PrintResults(finfos.get(), vmap.count("v"));
            Print(TO_STRING_TYPE("Number of matches: ") + Helpers::ToString<int, STR_SET>(finfos->size()));
            return toReturn;
        }

//SEARCH END
        return toReturn;
}

#endif

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Serbia Serbia
Software developer with couple of years of experience mostly with .NET programming and MS SQL databases currently interested in expanding knowledge to C++ and other operating systems.

Comments and Discussions