4
\$\begingroup\$

The following code creates a recursive backup of a directory in the form of interlinked HTML files, structured in the same form as the input directory.

It does not take the backup of the contents of a directory, just stores the names and sizes of all the files and folders contained in the directory.

(It can be thought of as a hyperlinked version of the dir /s or tree /f commands.)

For more details (including an example), refer to this.

As an example, this would be the output directory considering this as the input directory.

I cannot use C++ 14.

#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include <cassert>
#include <vector>
#include <cstdlib>
#include <boost/filesystem.hpp>

using namespace std;
using namespace boost::filesystem;

const path LogFileName = "DirectorySnapshotLog.txt";
ofstream Log;
stringstream LogErrorStream;  // Buffer soft errors to output them separately after the informational messages in the log file.

// Convert any type to its string representation
template<typename T> std::string ToString( const T obj )
{
    std::stringstream ss;
    ss << obj;
    return ss.str();
}

// Convert the input size ( in bytes ) to its nearest units in the ratio of 1024.
// ( Trying to do how Windows reports size of a file on right clicking and checking its properties )
string RoundSize( const long long& size )
{
    double ret = ( double )size;

    vector<string> units;
    units.push_back( "bytes" );
    units.push_back( "KB" );
    units.push_back( "MB" );
    units.push_back( "GB" );
    units.push_back( "TB" );

    const unsigned ratio = 1024;
    unsigned i = 0;

    while ( ret > ratio && i < units.size() - 1 )
    {
        ret /= ratio;
        i++;
    }

    return ToString( ret ) + " " + units[i];
}

// Iterate through a directory and store everything found ( regular files, directories or any other special files ) in the input container
void DirectoryIterate( const path& dirPath, vector<path>& dirContents )
{
    if ( exists( dirPath ) && is_directory( dirPath ) )
    {
        copy( directory_iterator( dirPath ), directory_iterator(), back_inserter( dirContents ) );
    }
}

// Create a set of HTML files containing information about source directory's contents and store it in the destination directory, in a directory structure similar to the source directory
// Returns the total size of the source directory
long long Snapshot( const path& sourcePath, const path& destinationPath )
{
    Log << sourcePath << endl;

    long long sourcePathSize = 0; // Total size of the source directory

    vector<path> dirContents, files, directories;
    try
    {
        DirectoryIterate( sourcePath, dirContents );
    }
    catch ( const filesystem_error& ex )
    {
        LogErrorStream << ex.what() << endl;
        return 0;
    }

    sort( dirContents.begin(), dirContents.end() ); // sort, since directory iteration is not ordered on some file systems
    for ( const auto& item : dirContents )
    {
        if ( is_directory( item ) )
        {
            directories.push_back( item );
        }
        else
        {
            files.push_back( item );
        }
    }

    path pwd = destinationPath / sourcePath.filename(); // Present working directory
    try
    {
        create_directory( pwd );
    }
    catch ( const filesystem_error& ex )
    {
        LogErrorStream << ex.what() << endl;
        return 0;
    }

    // Write the HTML file header.
    const path outFilePath = ( pwd / sourcePath.filename() ).string() + ".html";
    ofstream outFile( outFilePath.string() );
    if ( !outFile )
    {
        LogErrorStream << "Error creating " << absolute( outFilePath ) << " : " << strerror( errno ) << endl;
        return 0;
    }

    outFile << "<!DOCTYPE html>\n";
    outFile << "<meta charset=\"UTF-8\">\n";
    outFile << "<html>\n";
    outFile << "<title>" << sourcePath.filename() << "</title>\n";
    outFile << "<body>\n";

    // Write information about the files
    outFile << "<h1> Files </h1>\n";
    for ( const auto& file : files )
    {
        auto size = file_size( file );
        outFile << file.filename() << "----" << RoundSize( size ) << "<br>\n";
        sourcePathSize += size;
    }

    // Write information about the directories
    outFile << "<h1> Directories </h1>\n";
    for ( const auto& directory : directories )
    {
        long long size = Snapshot( sourcePath / directory.filename(), pwd );
        sourcePathSize += size;
        outFile << "<a href=\"" << ( directory.filename() / directory.filename() ).generic_string() << ".html\">" << directory.filename() << "</a>----" << RoundSize( size ) << "<br>\n";
    }

    // Write the footer
    outFile << "<br>\n";
    outFile << "<h3>Total directory size = " << RoundSize( sourcePathSize ) << "</h3><br>\n";
    outFile << "</body>\n";
    outFile << "</html>\n";

    return sourcePathSize;
}


int main()
{
    string sourcePath, destinationPath;

    cout << "Enter source directory path -:\n";
    getline( cin, sourcePath );
    if ( !is_directory( sourcePath ) )
    {
        cout << absolute( sourcePath ) << " is not a directory !\n";
        return -1;
    }

    cout << "Enter destination directory path -:\n";
    getline( cin, destinationPath );
    if ( !is_directory( destinationPath ) )
    {
        cout << absolute( destinationPath ) << " is not a directory !\n";
        return -1;
    }

    cout << "\n";
    Log.open( LogFileName.string() );
    if ( !Log )
    {
        cerr << "Error creating " << absolute( LogFileName ) << " : " << strerror( errno ) << endl;
    }

    Snapshot( sourcePath, destinationPath );

    if ( Log )
    {
        if ( LogErrorStream.str().empty() )
        {
            cout << "The program ran without any errors.\n";
        }
        else
        {
            Log << "\nERRORS -:\n\n" << LogErrorStream.str() << endl;
            cout << "There were some errors during the execution of this program !\n\nCheck " << absolute( LogFileName ) << " for details.\n";
        }
    }
}
\$\endgroup\$
5
  • 1
    \$\begingroup\$ No C++14, okay. And how about C++11? \$\endgroup\$
    – glampert
    Commented Mar 29, 2015 at 1:54
  • \$\begingroup\$ I have already used C++ 11 features in the code. \$\endgroup\$ Commented Mar 29, 2015 at 7:08
  • \$\begingroup\$ How about making an example of the input and output? \$\endgroup\$
    – edmz
    Commented Mar 29, 2015 at 14:28
  • \$\begingroup\$ Both the input as well as output are directories containing multiple files. So I prefer not to upload them here. Although I have given a link to the project and the example as well as the method to execute the program is mentioned there. \$\endgroup\$ Commented Mar 29, 2015 at 14:31
  • \$\begingroup\$ @black Still added the links to examples here itself. \$\endgroup\$ Commented Mar 29, 2015 at 14:39

3 Answers 3

5
\$\begingroup\$

I see a number of things that may help you improve your code.

Don't hardcode file names

The LogFileName might be something that a user of this program wants to place elsewhere and it will fail entirely if run from a read-only directory such as a CD or DVD.

Prefer command line parameters to runtime interaction

There is not an easy way to use this program in a script because it requires a response to a prompt rather than allowing command line parameters to be passed. You can provide both, if you wish, by looking for required command line parameters and then only prompting for missing ones.

Consider using standard SI units

The code calculates units in ratio of 1024 but then uses the abbreviation "MB" which is 1000*1000 rather than 1024*1024 which should be "MiB". See the Wikipedia article on Mebibyte for more details.

Avoid dynamically creating const structures

Within your RoundSize routine, the units vector is created and destroyed every time the function is called, which is not at all necessary. Further, since you're using C++11, you can simply create the vector using a std::initializer_list:

static const vector<string> units{ 
    "bytes", "KiB", "MiB", "GiB", "TiB" 
};

It would be even better as a constexpr, but we can't do that as written because std::vector has a non-trivial destructor.

Use for instead of while where appropriate

In that same RoundSize routine, the while loop would be much more idiomatic C++ if it were instead a for loop.

unsigned i;

for ( i = 0; ret > ratio && i < units.size() - 1; ++i)
{
    ret /= ratio;
}

Consider the performance cost of creating objects

I don't know if you had performance goals for your program, but it's useful to understand the performance characteristics of code you write. Consider this line, also from RoundSize:

return ToString( ret ) + " " + units[i];

The ToString() routine creates a string, then the space character is converted to a string and those two strings are concatenated, and then the [] operator is called on the vector and that string concatenated. Whew! One simple way to avoid some of that would be to simply have the space as part of the units.

Similarly, your sourcePath and destinationPath variables are declared as string objects but are used as path objects in most uses. This means that a conversion is done from string to path almost every time you use those variables. Better would be to declare both as path and then use explicit strings sourcePathStr and destinationPathStr for the few places you actually need strings.

Pass const references where possible

The ToString templated function should take const T &obj as its argument rather than const T obj to avoid making a unnecessary copy of the passed object.

Declare file scope items as static

Unless you intend to share the variables with other code in other files, your variables and functions should be declared static.

Catch exceptions

In a number of places within the code the underlying boost library call can throw an exception, but it is not caught by your program. An example of that is in this loop within Snapshot:

for ( const auto& file : files )
{
    auto size = file_size( file );
    outFile << file.filename() << "----" << RoundSize( size ) << "<br>\n";
    sourcePathSize += size;
}

The call to file_size can throw an error and did when I tried it on my Linux machine. The issue was a symlink that pointed to a nonexistent file. You could either catch the exception using try...catch or use the form of file_size that takes an error_code as a parameter.

Separate data manipulation from output

The Snapshot function really does two things. It creates the directories and files vectors and then it outputs those data structures as HTML. Better would be to split that one long function into the two logical halves. Or, even better...

Use object-oriented programming

Your files and directories are both objects. Why not instead create a filesystem object? That way, the files and directories would be parts of the filesystem and your Snapshot function would be better expressed as a constructor and a output member function. This would be much cleaner and also have the advantage of making it easier to create alternative output formats.

Use constant string concatenation

The Snapshot routine currently has these lines:

outFile << "<!DOCTYPE html>\n";
outFile << "<meta charset=\"UTF-8\">\n";
outFile << "<html>\n";
outFile << "<title>" << sourcePathName << "</title>\n";
outFile << "<body>\n";

But you don't really need to do it that way which potentially calls the << operator seven times. Instead, you could express the same thing as this:

outFile << "<!DOCTYPE html>\n"
           "<meta charset=\"UTF-8\">\n"
           "<html>\n"
           "<title>" << sourcePathName << "</title>\n"
           "<body>\n";

This only calls << three times. The compiler automatically concatenates the string literals together.

Use const where practical

In the Snapshot routine, pwd can and should be declared as const.

Fix relative paths

When I enter /home/Edward/test as the source path, which does indeed exist, the program reports:

"/home/Edward/test" is not a directory !

That is simply not correct, but I'm not sufficiently famiiliar with that portion of the boost libraries to troubleshoot it at the moment.

Reduce coupling

The use of global variables and passing the same variable to multiple routines both suggest excessive coupling which makes programs more difficult to maintain. Reduce the number of global variables and coupling in general by carefully considering how to break the task up into smaller pieces and by using objects.

Check and replace special HTML characters with entities

If I have a directory named this&that the & character should be converted into the HTML entity &amp; for correct output.

Additional HTML notes

There are a number of changes I'd recommend to the output HTML. Among them are to fix it (the <html> and <meta> tags are out of order). Also, consider changing the outputs to <table> rather than just lists. This would potentially allow nicer formatting via stylesheets.

Re-read the boost pages

As noted on the boost Filesystem home page, using

#define BOOST_FILESYSTEM_NO_DEPRECATED

above the line where you include the boost library is highly recommended for new code. Doing so now will help you preserve functionality as you expand this program and as the boost library evolves.

\$\endgroup\$
2
  • \$\begingroup\$ With reference to your Fix Relative Paths, can you elaborate a bit on where all the files ( the program, input and output ) were ? I am unable to reproduce this in my Linux system. Also, could you tell me if its still happening with the the improved code \$\endgroup\$ Commented Mar 31, 2015 at 10:32
  • 1
    \$\begingroup\$ @AnmolSinghJaggi: Seems to be fixed in the improved code, including handling the broken symlink without crashing (and correctly reporting in the log.) Good job! \$\endgroup\$
    – Edward
    Commented Apr 1, 2015 at 14:51
5
\$\begingroup\$

I'll be reviewing your implementation mainly, leaving the algorithm to another one.

  1. using the whole namespace is generally considered a bad practice although in this small example there's very little chance of troubles.

  2. LogFileName need not be global: it's only used in main.

  3. Avoid global variables.

  4. ToString<T> is not needed: since C++11 there's std::to_string, which also covers double types.

  5. unit may be initialized much better by using vectors initializer list:

    std::vector<std::string> units {elem1, elem2, elem3, ..., elemN};
    
  6. You may avoid multiple operator<< calls with string literals by letting the preprocessor chain them together:

    outFile << "<!DOCTYPE html>\n"
             "<meta charset=\"UTF-8\">\n"
             "<html>\n"
             "<title>" << sourcePath.filename() << "</title>\n"
             "<body>\n";
    
  7. In main you may want to check whether getline succeded.

  8. Log need not be global. See 2 & 3.

Error handling

Your error handling is awkward. Having a global object where you store possible errors and making the caller check for some is inefficient and, again, weird.

If you encounter an error, and your local code cannot do anything about that, either throw an exception (C++-style) or an error code with some related flag set accordingly (C-style). In the first case, you can decide to let the caller handle it (maybe adding some further information) until one of them is able to (or the program is exited) and keep throwing or locally, if you can.

\$\endgroup\$
3
  • \$\begingroup\$ I basically want my application to run for as long as it can, even with errors (which are not vital to its functioning). For example, if a directory inside another directory cannot be accessed due to some reason, I dont want the program to terminate, but rather simply ignore that directory and continue probing other directories. As a result, there are a very few situations which can prevent the program from further executing and forcing the program to terminate ( for eg, no memory available ). \$\endgroup\$ Commented Mar 30, 2015 at 19:11
  • \$\begingroup\$ Futher, I want all the errors encountered during the execution to be stored on a file safely rather than on stdout. What error-handling scheme do you suggest considering all these requirements ? \$\endgroup\$ Commented Mar 30, 2015 at 19:12
  • \$\begingroup\$ Ok, that's reasonable. What's wrong with writing to stdout (it's «safer» than to disk)? I would like to be notified as soon as something is wrong. Plus, generally, there's very little you can do when a system call fails (e.g. r/o fs or i/o error). And finally, you return from the function which will not keep carrying on as you want it to. \$\endgroup\$
    – edmz
    Commented Mar 31, 2015 at 13:38
1
\$\begingroup\$

Updated code after implementing some of the suggestions mentioned by others:

#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include <cassert>
#include <vector>
#include <cstdlib>
#define BOOST_FILESYSTEM_NO_DEPRECATED
#include <boost/filesystem.hpp>

using namespace std;
using namespace boost::filesystem;

static ofstream Log;
static stringstream LogErrorStream;  // Buffer soft errors to output them separately after the informational messages in the log file.

// Convert any type to its string representation
template<typename T> static string ToString( const T& obj )
{
    stringstream ss;
    ss << obj;
    return ss.str();
}

// Escape HTML special characters
static string EscapeHTMLSpecialChars( const path& PathName, const bool& href = false )
{
    string in = PathName.string();
    string ret;
    for ( decltype( in.size() ) i = 0; i < in.size(); i++ )
    {
        if ( in[i] == '&' )
        {
            ret += "&amp;";
        }
        else if ( in[i] == '<' )
        {
            ret += "&lt;";
        }
        else if ( in[i] == '>' )
        {
            ret += "&gt;";
        }
        else if ( in[i] == '"' )
        {
            ret += "&quot;";
        }
        else if ( in[i] == '\'' )
        {
            ret += "&#39;";
        }
        else if ( href && in[i] == '#' ) // If the input is an href attribute
        {
            ret += "%23";
        }
        else if ( href && in[i] == '?' )
        {
            ret += "%3F";
        }
        else
        {
            ret += in[i];
        }
    }
    return ret;
}

// Convert the input size ( in bytes ) to its nearest units in the ratio of 1024.
// ( Trying to do how Windows reports size of a file on right clicking and checking its properties )
static string RoundSize( const long long& size )
{
    double ret = ( double )size;
    static const vector<string> units {"bytes", "KiB", "MiB", "GiB", "TiB"};
    const unsigned ratio = 1024;
    unsigned i;
    for ( i = 0; ret > ratio && i < units.size() - 1; i++ )
    {
        ret /= ratio;
    }
    return ToString( ret ) + " " + units[i];
}

// Iterate through a directory and store everything found ( regular files, directories or any other special files ) in the input container
static void DirectoryIterate( const path& dirPath, vector<path>& dirContents )
{
    if ( is_directory( dirPath ) )
    {
        copy( directory_iterator( dirPath ), directory_iterator(), back_inserter( dirContents ) );
    }
}

// Create a set of HTML files containing information about source directory's contents and store it in the destination directory, in a directory structure similar to the source directory
// Returns the total size of the source directory
static long long Snapshot( const path& sourcePath, const path& destinationPath )
{
    Log << sourcePath << endl;

    boost::system::error_code ec;

    long long sourcePathSize = 0; // Total size of the source directory

    vector<path> dirContents, files, directories;
    try
    {
        DirectoryIterate( sourcePath, dirContents );
    }
    catch ( const filesystem_error& ex )
    {
        LogErrorStream << ex.what() << endl;
        return 0; // cannot iterate through the directory, so no point in going further
    }

    sort( dirContents.begin(), dirContents.end() ); // sort, since directory iteration is not ordered on some file systems
    for ( const auto& item : dirContents )
    {
        ec.clear();
        if ( is_directory( item, ec ) )
        {
            directories.push_back( item );
        }
        else if ( !ec )
        {
            files.push_back( item );
        }
        else
        {
            LogErrorStream << "Failed to determine if " << absolute( item ) << " is a directory or not : " << ec.message() << endl;
        }
    }

    const path pwd = destinationPath / sourcePath.filename(); // Present working directory
    ec.clear();
    create_directory( pwd, ec );
    if ( ec )
    {
        LogErrorStream << "Failed to create " << absolute( pwd ) << " : " << ec.message() << endl;
        return 0;
    }

    // Create the output file.
    const path outFilePath = ( pwd / sourcePath.filename() ).string() + ".html";
    ofstream outFile( outFilePath.string() );
    if ( !outFile )
    {
        LogErrorStream << "Failed to create " << absolute( outFilePath ) << " : " << strerror( errno ) << endl;
        return 0;
    }

    // Write the HTML file header.
    outFile << ""
                    "<!DOCTYPE html>\n"
                    "<html>\n"
                    "<head>\n"
                    "<meta charset=\"UTF-8\">\n"
                    "<title>" << EscapeHTMLSpecialChars( sourcePath.filename() ) << "</title>\n"
                    "</head>\n"
                    "<body>\n";

    // Write information about the files contained in the source directory
    outFile << ""
                    "<h1> Files </h1>\n"
                    "<table>\n";
    for ( const auto& file : files )
    {
        outFile << ""
                        " <tr>\n"
                        "  <td>" << EscapeHTMLSpecialChars( file.filename() ) << "</td>\n"
                        "  <td>";

        ec.clear();
        auto size = file_size( file, ec );
        if ( ec )
        {
            LogErrorStream << "Failed to read size of " << absolute( file ) << " : " << ec.message() << endl;
        }
        else
        {
            outFile << RoundSize( size );
            sourcePathSize += size;
        }
        outFile << ""
                        "</td>\n"
                        " </tr>\n";
    }
    outFile << "</table>\n";

    // Write information about the directories contained in the source directory
    outFile << ""
                    "<h1> Directories </h1>\n"
                    "<table>\n";
    for ( const auto& directory : directories )
    {
        long long size = Snapshot( sourcePath / directory.filename(), pwd );
        sourcePathSize += size;
        outFile << ""
                        " <tr>\n"
                        "  <td><a href=\"" << EscapeHTMLSpecialChars( ( directory.filename() / directory.filename() ).generic_string(), true ) << ".html\">" << EscapeHTMLSpecialChars( directory.filename() ) << "</a></td>\n" <<
                        "  <td>" << RoundSize( size ) << "</td>\n"
                        " </tr>\n";
    }
    outFile << "</table>\n";

    // Write the footer
    outFile << ""
                    "<br>\n"
                    "<h3>Total directory size = " << RoundSize( sourcePathSize ) << "</h3><br>\n"
                    "</body>\n"
                    "</html>\n";

    return sourcePathSize;
}

int main( int argc, char** argv )
{
    const path defaultLogFilePath = "DirectorySnapshotLog.txt";

    if( argc < 3 )
    {
        cout << "Usage : " << argv[0] << " <source_directory_path> <destination_directory_path> [log_file_path=" << defaultLogFilePath << "]\n";
        return -1;
    }

    const path LogFilePath = ( ( argc >= 4 ) ? path( argv[3] ) : defaultLogFilePath );
    Log.open( LogFilePath.string() );
    if ( !Log )
    {
        cerr << "Error creating " << absolute( LogFilePath ) << " : " << strerror( errno ) << endl;
        return -1;
    }

    Snapshot( argv[1], argv[2] );

    if ( Log )
    {
        if ( LogErrorStream.str().empty() )
        {
            cout << "The program ran without any errors.\n";
        }
        else
        {
            Log << "\nERRORS -:\n\n" << LogErrorStream.str() << endl;
            cout << "There were some errors during the execution of this program !\n\nCheck " << absolute( LogFilePath ) << " for details.\n";
            return -1;
        }
    }
}

The changes which I didn't do were:**

  • Use std::to_string() in place of ToString(), because the former works only for a limited number of built-in types, whereas the latter will work for any type for whom operator<< is defined.
  • Consider the performance cost of creating objects, because I don't think it will affect the performance much, but feel that units = "KB" is better than units = " KB" because of readability and semantics.
  • Use object-oriented programming; will do it later.
  • Reduce coupling, because I don't know how to do it, but maybe related to the above point.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.