Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Smart Partition File Exchange

0.00/5 (No votes)
22 Dec 2019 2  
A utility for automatization of moving files from partition A to partition B and vice versa

Introduction

The Smart Partition File Exchange is a Windows utility that automatizes process of swapping the data between two different partitions in the file system. It is particularly useful for automatization of file exchange process between partitions that are very close to full in capacity – in fact, this was the main goal of this project in the first place.

Normally, in situations where the free space on the partitions is enough to just copy all the files from partition A to partition B and then vice versa – this would require two steps and is done manually quite easily. But given a more frequent scenario when there is not enough space on the partitions to transfer all the files in one go, you would have to transfer the files in smaller batches, A --> B then B --> A then again A --> B and so on. The next figure explains the concept:

Image 1

This utility will automatically calculate all the necessary steps and do all of them in one click.

Using the Code

Usage

The user first selects the paths between which he/she wants to make the exchange of file/folder structure. These can be either written inside the textboxes on the top side, or they can be chosen by using the Browse buttons.

As soon as a path is selected / entered, its contents will be shown in the Webbrowser control under the textbox, and the labels will show the partition's current usage info. NOTE: The size will, in some cases, take a bit longer to calculate (in case of large number of files). In those cases, Size label will say "(… calculating …)" and the transfer start will be disabled until the calculation is finished.

Image 2

After both the paths have been chosen and their sizes calculated, the user can press the "START TRANSFER" button. This will trigger the calculation and subsequently execution of steps.

NOTE: There is also a "Simulation" checkbox, which is currently checked as default. When this checkbox is checked, there will be no actual transfer of folders and files, only the steps will be calculated and simulated. With "Transfer speed" textbox, you can enter the desired speed of simulated transfer (if this is left empty or zero, maximum speed will be used).

After the transfer starts, an additional button will appear above the "START TRANSFER" button – the "Cancel" button. By clicking on this button, you get a choice to revert all the changes, or just cancel the transfer completely and leave the already transferred files in their new location.

Image 3

Algorithm

There are several stages of execution after the Transfer process is started.

  1. Preparation – Here, the files for both locations are enumerated and their size is calculated.
  2. * Collect steps – create folders – The first thing that needs to be done is folder creation, because the directory structure needs to exist before the files are moved, otherwise, the move will cause an exception if the destination folder does not exist. In this part of the code, the subfolders are enumerated from both paths and the base path strings in them are switched.

    i.e.,

    (If path1 = X:\ and path2 = Y:\)
    	X:\dir\subdir => [CREATE] Y:\dir\subdir
    	Y:\another_dir\another_subdir => [CREATE] X:\another_dir\another_subdir
  3. * Collect steps – move files – After the proper directory structure has been created, the files can be moved. In this step, the files are enumerated for both paths and in their path strings, those base paths are switched. Method CalculateIterations is called, and it attempts to calculate the minimum number of steps to transfer all the files.

    i.e.,

    (If path1 = X:\ and path2 = Y:\)
    	X:\dir\subdir\file1.txt => [MOVE TO] Y:\dir\subdir\file1.txt
    	Y:\another_dir\another_subdir\file2.txt => 
        [MOVE TO] X:\another_dir\another_subdir\file2.txt
  4. * Collect steps – Rename files – At this point, there is a possibility that some of the destination filenames match the names of the files that already exist in the destination path. For that purpose, for each of those files, a new temporary destination name is created, and an additional step is also created for that temporary filename to be renamed back to the original filename.

    i.e.,

    (If path1 = X:\ and path2 = Y:\)
    	X:\file1.txt => [MOVE TO] Y:\file1.txt !!! Y:\file1.txt ALREADY EXISTS !!!
    	X:\file1.txt => [MOVE TO] Y:\file1.txt_somespecialidentifier.tmp
    	Additional step (in the end): 
              Y:\ file1.txt_somespecialidentifier.tmp => [RENAME TO] Y:\file1.txt
  5. * Collect steps – Delete folders – After the files have been moved, it's time to clean up! In this stage, the source folders are enumerated on both paths, and they are marked to be deleted after all the files are moved (because these folders will at this point be empty).
  6. Write out the steps into an XML (for debugging)
  7. * Execution – The steps are executed in the given order; they are looped through a for loop and the loop also handles any errors that may happen during the execution + if the cancel button should be pressed, the user will be asked if the revert back is necessary, and if so, the loop will be reversed and the steps will be executed in reverse, from the break point downward all the way to the first step.

The stages marked with * will be explained more thoroughly in further text.

Step Class

The most important data structure used is the Step class.

The Step object has the following attributes:

public PROGRESS_STAGE ProgressStage;
public string FolderName;
public string SourceFile;
public string DestinationFile;
public long FileSize;
public int Iteration;
public bool StepDone = false;
private bool folderExists = false;

ProgressStage attribute marks the „type“ of the Step object. PROGRESS_STAGE is an enum with the following members:

enum PROGRESS_STAGE 
{ PREP_CALC, PREP_CALC_DONE, CREATE_FOLDERS, MOVE_FILES, RENAME_FILES, DELETE_FOLDERS };

FolderName is the folder path – used in CREATE_FOLDERS and DELETE_FOLDERS steps.

SourceFile and DestinationFile are file paths used in MOVE_FILES and RENAME_FILES steps.

FileSize is the size of the file being transferred (used also in MOVE_FILES and RENAME_FILES steps).

Iteration marks the iteration of the step. Only MOVE_FILES steps will have an iteration assigned.

StepDone is marked true once the step gets executed. The revert execution also resets this attribute.

Finally, folderExists is a private attribute that will control whether a directory will be created or not (if, in fact, it already exists on target).

Step class also defines the following methods:

public void DoStep();
public void RevertStep();
public string PrintStepXml(ref FileStream stream);

Methods DoStep() and RevertStep() define how a step is executed depending on its ProgressStage attribute, in normal or reverse direction. For example, DoStep() will move SourceFile to DestinationFile, while RevertStep() will move DestinationFile to SourceFile. DoStep() will set the StepDone to true, while the RevertStep() will set StepDone to false.

The method PrintStepXml(ref FileStream stream) returns the Step object's attributes in XML format string, and writes it to the provided FileStream. This is used for debugging purposes.

The steps shall be collected in dictionaries declared such as this:

private Dictionary<int, Step> _steps;

(The _steps Dictionary is a global variable and the main dictionary where all the steps will be gathered and executed from. There are several other temporary Dictionary objects like this one used during the calculation process.)

File/Folder and Step Enumerations – Using LINQ to Objects

The Step objects are being created during several calculation stages (already described in the chapter Algorithm). For most of the Step collections, LINQ to Objects is used to get the enumerations.

CREATE_FOLDERS steps

_steps = _steps.Concat(

                DirectoryAlternative.EnumerateDirectories(_path1, "*", 
                SearchOption.AllDirectories).Select(x => x.Replace
                (_drive1, _drive2)) // take all folders from path1 and switch drive name
                .Concat(DirectoryAlternative.EnumerateDirectories
                (_path2, "*", SearchOption.AllDirectories).Select
                (x => x.Replace(_drive2, _drive1))) // take all folders from 
                                                    // path1 and switch drive name
                .Where(x => !x.Contains(RECYCLE_BIN) && 
                  !x.Contains(SYSTEM_VOLUME_INFORMATION)) // exclude system folders
                .OrderBy(x => x) // ascending order so that first the upper level folders 
                                 // are created and then subfolders
                .ToDictionary(k => step_nr++, v => new Step
                             (PROGRESS_STAGE.CREATE_FOLDERS, v)) // add step_nr 
                                                                 // and CREATE_FOLDERS flag
                ).ToDictionary(k => k.Key, v => v.Value);

First, all the directories are enumerated from both paths, but their base path is switched; path1 to path2 and vice versa. Then, the system folders are excluded. The folders are sorted in ascending order to make sure that the parent folders are created before their children (i.e., X:\folder before X:\folder\subfolder). Finally, Step objects are created and the enumeration is cast into a Dictionary.

MOVE_FILES Steps

The MOVE_FILES calculation is a bit more complicated, as it attempts to simulate file transfer between the two paths, and tries to do the job in as few iterations as possible. This is done in a method called CalculateIterations.

private Dictionary<int, Step> CalculateIterations(TRANSFER_DIRECTION direction);

The method takes TRANSFER_DIRECTION as a parameter, which is an enum with 2 choices – LEFT2RIGHT and RIGHT2LEFT – marking if the transfer process imitation should begin from path1 (left) to path2 (right) or the other way around. The method will be called twice in separate threads, one thread for each option. For this purpose, Task objects were used, that are run asynchronously, to check both options simultaneously; the caller method is paused until the first (any) of the Task objects is done.

Task[] tasks = new Task[2];

Dictionary<int, Step> _steps1 = new Dictionary<int, Step>();
Dictionary<int, Step> _steps2 = new Dictionary<int, Step>();
Dictionary<int, Step> _steps_move = new Dictionary<int, Step>();

tasks[0] = Task.Factory.StartNew(() => _steps1 = 
                     CalculateIterations(TRANSFER_DIRECTION.LEFT2RIGHT));
tasks[1] = Task.Factory.StartNew(() => _steps2 = 
                     CalculateIterations(TRANSFER_DIRECTION.RIGHT2LEFT));

Task.Factory.ContinueWhenAny(tasks, x =>
{
…
}

RENAME_FILES Steps

These steps are calculated in a for loop that runs through all the MOVE_FILES steps, and checks if the destination file exists. If it does, it adds a unique suffix to the destination filename, and creates an additional step to be done after the MOVE_FILES steps are finished – to replace the temporary filename with the original one.

string suffix;
int step_nr_max = _steps_move.Max(x => x.Key);
int step_nr_min = _steps_move.Min(x => x.Key);
int i = 1;
Dictionary<int, Step> _steps_rename_back = new Dictionary<int, Step>();

// for all the files to be moved
for (step_nr = step_nr_min; _steps_move.ContainsKey(step_nr) && 
                            step_nr <= step_nr_max; step_nr++)
{
   // check if the destination file already exists
   if (File.Exists(_steps_move[step_nr].DestinationFile))
   {
      // create suffix for new filename
      suffix = "_" + DateTime.Now.ToString("yyyyMMddHHmmssfffffff") + ".tmp";
      // add a step to change the name back afterwards
      _steps_rename_back.Add(
         i++,
         new Step(PROGRESS_STAGE.RENAME_FILES, null, 
         _steps_move[step_nr].DestinationFile + suffix, _steps_move[step_nr].DestinationFile)
      );
      // change the destination filename for current step
      _steps_move[step_nr].DestinationFile += suffix;
      Thread.Sleep(1);
   }
}

DELETE_FOLDERS Steps

_steps = _steps.Concat(
   DirectoryAlternative.EnumerateDirectories
      (_path1, "*", SearchOption.AllDirectories)  // take all folders from path1
   .Concat(DirectoryAlternative.EnumerateDirectories
      (_path2, "*", SearchOption.AllDirectories)) // concatenate with all folders from path2
   .Except(_steps.Values.Where(x => x.ProgressStage == 
      PROGRESS_STAGE.CREATE_FOLDERS).Select(x => x.FolderName)) // except folders that are 
                                                                // to be created
   .Where(x => !x.Contains(RECYCLE_BIN) && 
          !x.Contains(SYSTEM_VOLUME_INFORMATION)) // exclude system folders
   .OrderByDescending(x => x) // descending order so that subfolders come first 
                              // (otherwise we get folder not empty exception)
   .ToDictionary(k => step_nr++, 
    v => new Step(PROGRESS_STAGE.DELETE_FOLDERS, v)) // add step_nr and DELETE_FOLDERS flag
).ToDictionary(k => k.Key, v => v.Value);

First, all the folders from path1 and path2 are enumerated. Then the folders that are to be created are removed (i.e., if there is the same folder name on both path1 and path2 – then this folder should be kept in both locations). Then the system folders are excluded. The descending order is used so that the children get to be deleted before their parents. The Step Dictionary is finally created.

Browsers

For the purpose of visually showing the contents of the source/destination filesystem locations, and the transfer progress itself, two WebBrowser controls are used. They are being constantly refreshed during the Transfer by a BackgroundWorker object called backgroundWorkerRefresh, along with the size and free space labels above them.

There are two buttons above each WebBrowser:

Image 4 - Reset the WebBrowser to show the original path

Image 5 - Browse up

Backgroundworkers

There are four BackgroundWorker objects used in total in this project. Three of them are used for various refresh operations, while the main one, backgroundWorkerFileTransfer is handling the actual work. All of them together ensure that during the operation, the main thread does not, at any point, get unresponsive.

The initialization of the BackgroundWorker objects is done in the default constructor of the form:

this.backgroundWorkerCalculate1.DoWork += BackgroundWorkerCalculate_DoWork;
this.backgroundWorkerCalculate1.RunWorkerCompleted += 
                                BackgroundWorkerCalculate_RunWorkerCompleted;
this.backgroundWorkerCalculate2.DoWork += BackgroundWorkerCalculate_DoWork;
this.backgroundWorkerCalculate2.RunWorkerCompleted += 
                                BackgroundWorkerCalculate_RunWorkerCompleted;
this.backgroundWorkerFileTransfer.DoWork += BackgroundWorkerFileTransfer_DoWork;
this.backgroundWorkerFileTransfer.ProgressChanged += 
                                BackgroundWorkerFileTransfer_ProgressChanged;
this.backgroundWorkerFileTransfer.RunWorkerCompleted += 
                                BackgroundWorkerFileTransfer_RunWorkerCompleted;
this.backgroundWorkerRefresh.DoWork += BackgroundWorkerRefresh_DoWork;
this.backgroundWorkerRefresh.ProgressChanged += BackgroundWorkerRefresh_ProgressChanged;
this.backgroundWorkerRefresh.RunWorkerCompleted += BackgroundWorkerRefresh_RunWorkerCompleted;

BackgroundWorkerCalculate1 and BackgroundWorkerCalculate2

These two BW objects share the same methods for DoWork and RunWorkerCompleted event handling, because they basically do the same thing, except the first is used for calculations on path1, and the latter is used for calculations on path2.

They are both run each time the method RefreshSizeFreeSpace is called, which is every time any of the locations changes (or technically, any time when one of the path textboxes loses focus). They calculate used and free space, and enumerate files, each for their respective path.

BackgroundWorkerRefresh

This BW object refreshes periodically the size and freespace labels, and also refreshes the WebBrowser objects during the File Transfer operation. It is also responsible for the calculation of the time remaining. The time remaining is calculated as:

TIME_REMAINING = SIZE_OF_REMAINING_FILES / (SIZE_OF_TRANSFERRED_FILES / TIME_ELAPSED)

BackgroundWorkerRefresh is started simultaneously with the backgroundWorkerFileTransfer, and is cancelled (stopped) when the file transfer is finished.

BackgroundWorkerFileTransfer

The backgroundWorkerFileTransfer is the central object of the whole project. It does all the important work.

It is triggered by buttonStart.Click event and it performs all the steps of the algorithm described earlier.

During the Preparation phase, it calls the method CalculateFilesAndSize for each path, which enumerates files and calculates disk usage and free space. If this method fails, an error is reported.

In the next few phases, the BW gathers the necessary steps for the proper execution of the file transfer. It uses LINQ to Object methods to populate the _steps Dictionary, all of which were described earlier.

After all the steps are enumerated, the method will use the WriteStepXml method of Step objects to write out all the steps into an XML file – for debugging purposes.

After this comes the execution part.

The steps execution is done in a for loop which loops through all the Step objects in the _steps Dictionary one by one, in the designated order. If the _revertback = true, it will execute the RevertStep() method of the Step object, otherwise it will execute the DoStep() method. If an exception occurs, it will offer the user 3 choices – abort/retry/ignore – and depending on the DialogResult, it will either retry the last step, ignore and continue with the next step or, in case of abort, ask the user if it should revert back all the steps – and if yes, it will set the _revertback = true, and revert the counter so the for loop now counts backwards – and all the steps that were already done shall be reverted by calling the RevertStep() method.

This BW also refreshes the size and freespace global variables for size and freespace for both paths, refreshes labelIterations, and writes out UI and log messages using the method GetMessage.

Pain Points

Enumerating Files

Normally, one would use System.IO.Directory .NET library methods to enumerate system entries on Windows system. However, there is a flaw in the standard Enumerate methods in .NET, which causes the methods to throw an Exception whenever they hit a system file or folder; for example, if you are enumerating files and/or folders on a partition root (i.e., D:\), then the Enumerate method will eventually run into $RECYCLEBIN folder, and will break, returning only a partial enumeration of filesystem entries.

To overcome this issue, I have created an alternative library to System.IO.Directory, named System.IO.DirectoryAlternative, which uses the same WinAPI functions as the original library, but without the described flaw, and it also runs faster then the .NET version. You can find the detailed description in the following article:

Single-Threading vs. Multi-Threading

In single-threaded operation, GUI typically becomes unresponsive while processor-demanding operations are performed. This is why several Backgroundworker objects are used in this project, which work each in a separate thread, to maintain a stable and responsive GUI. These BW objects are described in the previous chapters.

Difference in Calculation of Free Space

Since the actual free space of the volume to which the files are being moved may differ slightly than the calculated free space in the app, a buffer of 10 MB (can also be set differently) is used during the calculation process (i.e., there is always at least 10 MB left of free space on each volume).

const long BUFFER_SIZE = 10485760;

Destination Folder Already Exists

If there is a folder with the same name on both source and destination path, then it doesn't need to be created, but we also need to make sure that it doesn't get deleted if we are doing a revert back. Therefore, a private attribute folderExists in the Step class was introduced; for a folder that already exists on the destination, a CREATE_FOLDER step will still be created, but the attribute folderExists shall be set to true and so the folder will not actually be created.

App Freezing Because of Too Many Progress Changes

If there is a very large number of files to be moved, then for each file, the progress would be reported by the backgroundWorkerFileTransfer. This, however, caused the app to become unresponsive, and the message queue was congested and no messages were coming through to the main thread until the entire process was over.

This was solved by moving the label message setting commands to the DoWork event handler by using cross-threading. This ensured that the messages were synchronous to the operations, and no messages were sent to the queue.

this.Invoke(new Action(() => labelIterations.Text = _steps[step_nr].Iteration.ToString() + 
            " out of " + _num_iterations.ToString()));

Only the progressbar was left to be updated by the ProgressChanged method, and it was made only in discreet time slots because, anyhow, in most cases, anything below 1% change is unobservable in the ProgressBar.

// report progress every 10ms
if ((_revertback ? _revert_timer : _timer).ElapsedMilliseconds - tick >= PROGRESS_RATE)
{
   backgroundWorkerFileTransfer.ReportProgress(progress, msg.Split('\n')[0]);
   tick = (_revertback ? _revert_timer : _timer).ElapsedMilliseconds;
}

History

  • 22nd December, 2019: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here