2007. február 18., vasárnap

Copying Files in Delphi


Problem/Question/Abstract:

How do I copy a file in Delphi?

Answer:

Reminiscing on Days Gone By...

Back in the old DOS days, we took for granted copying a file from one place to another with the copy command. But with Windows, everything changed. We now use File Manager or Explorer to copy files from one place to another, which is a huge improvement over typing in the fully qualified path for both source and destination files.

But at the programming level, performing the copying of one file to another is not as apparent as one would think. In fact, there are no native Delphi calls for copying a file whatsoever. So what do you do if you want to copy a file? You have to write the routine yourself.

Interestingly enough, there is a pretty good example of copying a file that is in the FMXUTILS.PAS file in the Delphi\Demos\Doc\Filmanex directory that will perform a file copy using native Delphi file-related commands. While this method works just fine, I decided to go another route; that is, to use a file stream to copy a file from one place to another. Streams are interesting animals. They're used internally in Delphi to read and write components, forms and data, and they're pretty handy. Unfortunately, they aren't well-documented so they can be a bit tricky to use. I went through a lot of trial and error to get them to work, and referenced several sources outside of the online help (which is just about the only place you'll find anything on streams in Delphi) before I got a handle on streams. But once I figured them out, they became what I use for reading and writing files almost exclusively.

There's Almost Always More Than One Way of Doing Things...

Once you've programmed for a while, you realize that it's possible to solve a particular problem in a variety of ways; which way is valid is dependent upon your knowledge and experience (one way may be more optimized than another) or, at times, even the situation will dictate that one methodology is better suited for a task than another.

For instance, with file copying, there are times you just want to copy a file in the background using a quick and dirty method, and you don't care if the user knows what's going on at all. But there are other times, such as when file utilities are part of an interface, when you want the user to be aware of the copying progress.

What I'm going to show you here are two ways to perform file copying: one quick and dirty; the other, a more snazzy, graphical way of copying a file, though it uses a few more resources and is a bit slower.

Quick and Dirty Copying

Traditionally, copying a file involves using a loop to move a series of blocks from one file into a temporary buffer, then copying the contents of the buffer into another file. Let's look at the CopyFile function found in the FMXUTILS.PAS:

{=============================================================================
CopyFile procedure found in the FMXUTILS.PAS file in Delphi\Demos\Doc\Filmanex
This is an example of copying a file using a buffer.
=============================================================================}

procedure CopyFile(const FileName, DestName: TFileName);
var
  CopyBuffer: Pointer; { buffer for copying }
  TimeStamp, BytesCopied: Longint;
  Source, Dest: Integer; { handles }
  Destination: TFileName; { holder for expanded destination name }
const
  ChunkSize: Longint = 8192; { copy in 8K chunks }
begin
  Destination := ExpandFileName(DestName); { expand the destination path }
  if HasAttr(Destination, faDirectory) then { if destination is a directory... }
    Destination := Destination + '\' + ExtractFileName(FileName);
      { ...clone file name }
  TimeStamp := FileAge(FileName); { get source's time stamp }
  GetMem(CopyBuffer, ChunkSize); { allocate the buffer }
  try
    Source := FileOpen(FileName, fmShareDenyWrite); { open source file }
    if Source < 0 then
      raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));
    try
      Dest := FileCreate(Destination); { create output file; overwrite existing }
      if Dest < 0 then
        raise EFCreateError.Create(FmtLoadStr(SFCreateError, [Destination]));
      try
        repeat
          BytesCopied := FileRead(Source, CopyBuffer^, ChunkSize); { read chunk }
          if BytesCopied > 0 then { if we read anything... }
            FileWrite(Dest, CopyBuffer^, BytesCopied); { ...write chunk }
        until BytesCopied < ChunkSize; { until we run out of chunks }
      finally
        FileClose(Dest); { close the destination file }
      end;
    finally
      FileClose(Source); { close the source file }
    end;
  finally
    FreeMem(CopyBuffer, ChunkSize); { free the buffer }
  end;
end;

But Delphi implements a method of TStream called CopyFrom that allows you to copy the entire contents of one stream into another in one fell swoop. Here's an implementation of copying a file using the CopyFrom method:

{=============================================================
Quick and dirty copying using the CopyFrom method of TStream.
=============================================================}

procedure FileCopy(const FSrc, FDst: string);
var
  sStream,
    dStream: TFileStream;
begin
  sStream := TFileStream.Create(FSrc, fmOpenRead);
  try
    dStream := TFileStream.Create(FDst, fmCreate);
    try
      {Forget about block reads and writes, just copy
       the whole darn thing.}
      dStream.CopyFrom(sStream, 0);
    finally
      dStream.Free;
    end;
  finally
    sStream.Free;
  end;
end;

The declaration of the CopyFrom method is as follows:

function CopyFrom(Source: TStream; Count: LongInt): LongInt;

Source is the TStream you're going to copy from, and Count is the number of bytes to copy from the stream. If Count is zero (0), the entire contents of the source stream is copied over. This makes for a quick one-liner copying.

Notice that in both the examples above, all the functionality is enclosed in nested try..finally blocks. This is extremely important because just in case something goes wrong, all resources and pointers that are created are freed. You don't want to have stray pointers or unreleased memory in your system, so providing at least this level of exception handling is key to ensuring that you don't.

A Sexier File Copy

If you write robust user interfaces, practically everything that you do involves interacting with the user by providing visual cues to let the user know what's going on. File copying is one of those types of operations that when performed within the context of a user interface must provide some status as to the progress of the copy operation. Therefore, a quick and dirty copy like the one I just described above won't do. What we need then is something with a bit more pizazz.

In order to get status, we need to copy the file in chunks. That way, as we copy each chunk from one file to another, we can let the user know how far we've proceeded. What this implies is that we need two pieces. The first is the unit that performs the copying; the other a status window used for notification. For me, the best way to get both pieces to work in concert was to build a custom component which encapsulates the file copy operation and uses another unit to perform the notification.

The notification unit is just a simple form with a TGauge and a TButton placed on it. The unit code is as follows:

unit copyprg;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, Gauges;

type
  TFileProg = class(TForm)
    Gauge1: TGauge;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    fCancel: Boolean;
  public
    property CancelIt: Boolean read fCancel;
  end;

var
  FileProg: TFileProg;

implementation

{$R *.DFM}

procedure TFileProg.Button1Click(Sender: TObject);
begin
  fCancel := True;
end;

procedure TFileProg.FormCreate(Sender: TObject);
begin
  fCancel := False;
end;

end.

Nothing odd here. I simply added a custom property to the form called CancelIt, which is a simple Boolean flag used to cancel the copying operation midstream should the user desire to do so. The real work happens in the custom component itself. Let's look at its code, then discuss it:

unit FileCopy;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TFileCopy = class(TComponent)
  private
    FSource,
      FDest: string;
    procedure DoCopyFile(const SrcFile, DstFile: string);
  public
    procedure CopyFile; virtual;
  published
    property FileSource: string read FSource write FSource;
    property FileDestination: string read FDest write FDest;
  end;

procedure Register;

implementation

uses copyprg;

procedure TFileCopy.CopyFile;
begin
  DoCopyFile(FileSource, FileDestination);
end;

procedure TFileCopy.DoCopyFile(const SrcFile, DstFile: string);
const
  bufSize = 16384; {Use a 16K buffer. You can use whatever size suits you, though.}
var
  sStream,
    dStream: TFileStream;
  pBuf: Pointer;
  cnt: Integer;
  prgFrm: TFileProg;
  totCnt,
    X,
    strmSize: LongInt;
begin
  totCnt := 0;
  {Open up the Source File to read it}
  sStream := TFileStream.Create(SrcFile, fmOpenRead or fmShareDenyWrite);

  {Create the copying progress form and set property values}
  prgFrm := TFileProg.Create(Application);
  with prgFrm.Gauge1 do
  begin
    MinValue := 0;
    MaxValue := 100;
    Progress := 0;
  end;
  prgFrm.Show;

  {Get the size of the entire stream to use for the progress gauge. Note
   we have to call FileSeek first because it will place the pointer
   at the end of the file when we get the file first return value.}
  strmSize := sStream.size;

  try
    { Create the destination file. If it already exists,
      overwrite it. }
    dStream := TFileStream.Create(DstFile, fmCreate or fmShareExclusive);
    try
      GetMem(pBuf, bufSize);
      try
        {Read and write first bufSize bytes from source into the buffer
         If the file size is smaller than the default buffer size, then
         all the user will see is a quick flash of the progress form.}
        cnt := sStream.Read(pBuf^, bufSize);
        cnt := dStream.Write(pBuf^, cnt);

        totCnt := totCnt + cnt;
        {Loop the process of reading and writing}
        while (cnt > 0) do
        begin
          {Let things in the background proceed while loop is processing}
          Application.ProcessMessages;

          {Read bufSize bytes from source into the buffer}
          cnt := sStream.Read(pBuf^, bufSize);

          {Now write those bytes into destination}
          cnt := dStream.Write(pBuf^, cnt);

          {Increment totCnt for progress and do arithmetic to update the
           gauge}
          totcnt := totcnt + cnt;
          if not prgFrm.CancelIt then
            with prgFrm.Gauge1 do
            begin
              Progress := Round((totCnt / strmSize) * 100);
              Update;
            end
          else
            Break; {If user presses cancel button, then break out of loop}
          {which will make program go to finally blocks}
        end;

      finally
        FreeMem(pBuf, bufSize);
      end;
    finally
      dStream.Free;
      if prgFrm.CancelIt then {If copying was cancelled, delete the destination file}
        DeleteFile(DstFile); {after stream has been freed, which will close the file.}
    end;
  finally
    sStream.Free;
    prgFrm.Close;
  end;
end;

procedure Register;
begin
  {You can change the palette entry to something of your choice}
  RegisterComponents('BD', [TFileCopy]);
end;

end.

Like the CopyFile routine in FMXUTILS.PAS, the concept behind copying for this component is the same: Grab a chunk of the source file, then dump it into the destination file. Repeat this process until all possible data has been copied over. Notice that I used a TFileStream once again. But this time, I didn't copy the entire file over in one fell swoop. That would've defeated the whole purpose of providing user status.

I've commented the code extensively, so I won't go into real detail here. I'll leave it up to you to study the code to learn about what's going on in it.

Notice the method declaration for CopyFile is declared as a virtual method. I've done this on purpose so that this class can be used a template class for specialized copy operations. The CopyFile method is actually rather trivial at this level -- all it does is call the DoCopyFile method and pass the FileSource and FileDestination property values.

However, it is the only public interface for actually performing the copying operation. This is an important point for all you component designers out there. Providing limited method visibility ensures that the core features of your components remain intact. Remember, you want other users of your component to see only what is absolutely necessary.

How is this useful? It allows you to have a bit of control over how the hierarchy develops. By hiding the basic functionality from descendant classes, you can ensure that the basic functionality of your class is retained throughout the inheritance tree. Granted, users can completely override the behavior of the CopyFile method, but that doesn't mean that the original capability will be lost. It will still be there, just not implemented.

Obviously, the meat of the work is performed by the DoCopyFile method. Study the code to see what happens from point to point. Note that I used a Pointer for the buffer. You can use just about any type as a buffer, but a pointer makes sense because its a simple 4-byte value. If you are copying a text file and want to treat the pointer like a string, you can cast it as a PChar, so long as you append a #0 byte to the end of the buffer. Neat stuff, huh?

A Little Note About TFileStream

TFileStream is not a direct assignment of TStream. In fact, it's a descendant of THandleStream which, when created, fills a property value called Handle which is the handle to an external file. TFileStream inherits the Handle property. The significance of this is really neat: File operations that take a handle as input can be applied to a TFileStream. That has interesting implications in that you can do file operations on a TFileStream object before you write it to another place. Try experimenting with this.

Okay, we've come a long way. And no, I haven't delved into the depths of Stream classes. That's probably best left to another article or series of articles. In any case, play around with the TCopyFile class. It could prove to be a useful addition to your applications.

Nincsenek megjegyzések:

Megjegyzés küldése