Thursday, 7 November 2013

Portable file copying in C++

Background

If any of you ever had an Amiga (and used it for anything but playing games) you will likely remember a commercial file manager called Directory Opus. The 4.xx series was the de facto two paned file commander for the Amiga. Directory Opus - or just DOpus - later evolved into a fully featured replacement for the Amiga's desktop, Workbench, which gave great joy to a lot of people and caused general outrage from a whole lot more.

I remember both fondly, and on my Windows box, I still use DOpus as an explorer replacement. However, for many people DOpus 4.12 was the embodiment of the Amiga Experience. Simple, clean and highly efficient.

Which brings me to the actual topic, I want to discuss. DOpus, in all its incarnations, had (and has) a very nice progress dialog for copying and moving files. The layout goes sorta like this:
  • The name of the file currently being copied,
  • source and destination,
  • a progress bar for that file,
  • a summary of the entire copy process (all selected files and dirs) and
  • a progress bar for the entire operation.
An interesting thing about the bottom progress bar is that it shows percent of the entire operation measured in bytes. This makes the progress fairly accurate even when multiple files of vastly different sizes are being copied.

My current attempt to copy files portably in C++ comes from a (yes, childish indeed!) desire to re-implement that progress dialog.
Most modern APIs already have a way to copy files, and they're generally quite easy to use. However, they usually take a source file name and a destination directory and return when the file is copied. With all the progress information I want to display, I need to be able to poke into the copy function itself and find out how it's coming along. This suggests that I have to put together a file copy function myself, so that's what I'm trying to do in this post.

Requirements

 A module that copies a file has to know certain things:
  • The name of the file to be copied.
  • The name of the destination file name (the name itself need not be the same as the source file name).
  • If the destination file already exists, should we overwrite or abort?
The first two will likely be arguments to the function. The last may be an argument or we could make the non-existence of the destination file a precondition. For now, I will make it an argument to the function, because it is fairly simple but still puts some logic in place that can be extended later on.

C++ and C already has machinery that can operate on files: C has FILE* handles and C++ has <iostream>s. Using these facilities and standard algorithms it is actually possible to implement the entire copy operation as a one-liner, but then I'm back where I started: I know of no way to get the current state out of an algorithm invocation.

The idea then is to copy the file in batches. The size of each batch does not matter too much, though it should be small enough that progress updates make sense, yet we also do not want to copy everything byte-wise. I chosen (somewhat arbitrarily) to use a chunk size of 32Kb. This will give no updates for files smaller than that, of course, but that's hardly a limitation.

To represent each file name I have chosen to use the Boost.FileSystem library version 3. This library is the basis of the current standardisation efforts, so we may as well get acquainted with it anyway.

Note that Boost.FileSystem already has a function, copy(from, to), that does exactly what it says on the tin. Again, though, I really want those status updates and I can't get them from that function!

With this little initial though about the function, we're really not quite ready to implement anything! I never had quite that much patience, though, so let's get to it. We're only talking about copying files! How hard can it be...?

Copying files

Sooner or later the entire logic for this operation will be rolled into a class; that class will then maintain the source files, sizes and destination directory. For now though we will concentrate on the core function: From file (path and file name), to file (path and file name) and whether to overwrite if it exists. That suggests the following function signature:


 bool copyFile(boost::filesystem::path const& from,
               boost::filesystem::path const& to, bool failIfExists);


The function will return true if the file copy succeeded, false otherwise. from must exist and to must be a complete path with a file name.

Without further ado here's an implementation that matches what I've discussed so far. We'll walk through it afterwards.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bool copyFile(boost::filesystem::path const& from,
              boost::filesystem::path const& to, bool failIfExists)
{
    std::size_t const chunkSize = 32768;

    boost::filesystem::ifstream infile{from};
    if(!infile.good())
        return false;
    if(failIfExists && exists(to))
        return false;
    try
    {
        boost::filesystem::ofstream outfile{to,
                                            std::ios_base::out |
                                            std::ios_base::trunc |
                                            std::ios_base::binary};
        std::unique_ptr<char[]> buffer{new char[chunkSize]};
        std::streamsize n{chunkSize};
        while(n == chunkSize)
        {
            n = infile.rdbuf()->sgetn(&buffer[0], chunkSize);
            outfile.rdbuf()->sputn(&buffer[0], n);
        }
    }
    catch(boost::filesystem::filesystem_error& e)
    {
        infile.close();
        return false;
    }
    return true;
}
 
Line 4 simply sets the chunkSize to 32Kb. It is used in line 17 when we allocate a buffer to use.

On line 6 we open the source file for reading as a standard stream. Boost.FileSystem includes stream classes that are identical to the standard streams, except for taking path arguments instead of string-based ones. On line 7 we check that the source file opened correctly.

Line 9 checks if the destination file exists. exists() is a function from namespace boost::filesystem and will be found through argument dependent lookup.

Any trouble with the destination file will be flagged using exceptions, so we wrap the next section in a try-clause.

The flags to the ofstream constructor make sure we create an output file, remove its contents and use binary for transfer. Strictly speaking the binary flag is not necessary, as do not use the high-level parts of the library; instead we access the buffer directly. That makes sense in this case: We don't want to interpret anything, we just want a byte-for-byte true copy.

In line 17 we allocate a buffer on the heap. Using std::unique_ptr here takes care of the cleanup automatically.

std::streamsize is used for counting the number of bytes transferred in an I/O operation, or for the size of a buffer. It is basically a signed version of std::size_t.

On line 21 we fill the buffer from the input stream's underlying buffer. rdbuf() gives a pointer to the underlying buffer and sgetn(b, n) fills buffer b with up to n bytes from the input. It returns the actual number of bytes transferred.

The operation on line 22 is analogous, but for the output stream. sputn(b, n) takes n bytes from buffer b and puts it into the output.

The buffer address is taken as &buffer[0]. This works because std::unique_ptr provides operator[] when it is used for arrays.

In the catch-clause I only check for the Boost.FileSystem provided errors. Anything else must be handled further up in the system.

Wrapping up

So, what have we achieved? Copying from one file to another sounds a little like re-inventing the wheel, doesn't it? In some sense, yes, but as I said, I want to get progress information out of the copy operation, and that means implementing it myself. As said, this will end up being a class, with member data and member functions. Some of that data just might be bytes-copied-so-far, both for the current file and for the total. These counters can now be updated by inserting appropriate += expressions after line 22 in the loop.

In a simple test run the above function used between 1.298 and 1.556 seconds on several successive runs to copy a 1Gb random file from one folder to another on a local hard drive. GNU cp used on average 1.3 seconds for the same file, but did have the quickest copy at 1.272 seconds. So at this point we could start fine-tuning the buffer size, but frankly I don't think it's worth it.

I may change that tune once we start having to do other stuff in the core loop, though. We'll see.

Wednesday, 6 November 2013

Why am I here...?

No, this is not a philosophical post, despite the flowery title. I'll leave the meaning of life to someone else. Monty Python most likely; they seem quite capable.

Rather I'm sitting here wondering why I'm typing at all right now. I've considered blogging before, but something always came up so it just didn't happen. Now that's a lame excuse if ever I heard one, but that's OK, since I made it myself.

So, to put a little actual content into this post, I'll try to put into writing just what is likely to show up on this blog.

First of all, this is unlikely to be a frequently updated blog. I've never been good at just writing because of the writing; there has to be some purpose to the text that ends up on paper or screen. So, updates are likely to happen only when something has caught my attention that I want to formalise or share.

What will end up here then? What can be expected? I think I need to give a little background about myself first.

First things first then: I am not a programmer. Not by vocation anyway. I have a degree in marine engineering and make a living making sure the container ships that transport goods around the world make it from A to B in good shape and as economically as possible. I am not a software engineer, I am a mechanical engineer. Why is this a blog about "Experiments in programming" then?

Well, I am not a programmer by education, but I have been programming on-and-off now since I was around 12 years old, when I first got my hands on a programmable piece of hardware. The hardware in question was a Commodore 64 with a tape station (I couldn't afford a disk station); the programming language (if you will) was the "BASIC 2.0" that was built-in to the unit. I progressed to Amiga in various incarnations and with them to various versions of more advanced BASICs. (Anyone remember AMOS Pro or Blitz BASIC?)

Then in the mid-nineties, I got hold of my first compiled programming language: AmigaE, and my view on programming changed radically. I sniffed briefly at C but then got my hands on a SAM's "Teach yourself" that covered C++, which was at the time in the final stages of initial standardisation. C++ has been my main programming language since.

On the other hand, I also had an education to follow, and later on a job to take care of, so from time to time programming has been low on my list of priorities. It's really just a hobby to me, but one I like very much. Working exclusively on pet projects, I am also not particularly hampered by any particular coding style, which has allowed my to experiment with tips, tricks and hacks on the very bleeding edge of the development of C++. The current rush in the C++ community, with the new standard being by now fully implemented by major compilers, and another new standard likely just around the corner... I feel quite excited to see what C++ code will look like in just a few years time.

What claim do I have to call myself a programmer at all then, given above curriculum vitae? None at all! I have no programming education, as I said. I do have (and have read!) a fairly extensive list of books on the subject by various notable authorities on the subject, and I pay close attention to what's going on in the C++ community at large. I guess that'll have to do.


So, about "Experiments in programming." The blog will contain posts about experiments I make with -primarily- C++ and related technologies. If I learn something I think is interesting or just cool, I'll put it up here. If I find a cool C++ library out there, I may post a link. If I manage to get clang to build itself, libc++ and libc++abi, so that I can completely remove any dependencies on GPLed libraries, I'll probably make a note here.

I am currently working on something ridiculously simple: I want to copy files. Using C++ and Boost. With decent error handling. And a nice progress bar (or two!). As soon as I have something that seems noteworthy... I'll be back.