In part one I described how to get a list of removable storage devices (memory cards, USB sticks, etc). In this part, we’re going to be able to pick a memory card and transfer the contents into a catch-all “vault.” Picking out good pictures will come in a later post; for now, we’re just going to stick everything in the vault.
Getting Input
Since this is a relatively simple console application, we’re just going to use the classic “show the user a numbered list of choices and ask them for input” tactic. Since I’m going to be the main consumer of this application, and I’m lazy, I took the approach of just looking for a single key press instead of a number followed by enter. This is achieved via Console.ReadKey, which looks for a single key press.
I designed the following function to take an array of T objects so we could just pass an array of anything to the function, the user could pick which one they want, and that object would be returned. The objects are listed using their .ToString()part one I wrapped the DeviceInfo in a custom class — I wanted to control how it would display when passed to this function.
[code language=”csharp”]
private static T GetInput<T>(params T[] choices)
{
ConsoleKeyInfo cki = new ConsoleKeyInfo();
for (int idx = 0; idx < choices.Length; idx++)
Console.WriteLine($"t{idx + 1}: {choices[idx].ToString()}");
while (!(cki.Key >= ConsoleKey.D1 && cki.Key < (ConsoleKey.D1 + choices.Length)))
{
cki = Console.ReadKey(true);
// Number keys and Number pad keys don’t have the same Key, so if the user used the number pad
// just convert it to a normal number.
if (cki.Key >= ConsoleKey.NumPad1 && cki.Key < ConsoleKey.NumPad9)
cki = new ConsoleKeyInfo(cki.KeyChar, cki.Key – (ConsoleKey.NumPad1 – ConsoleKey.D1),
cki.Modifiers.HasFlag(ConsoleModifiers.Shift), cki.Modifiers.HasFlag(ConsoleModifiers.Alt),
cki.Modifiers.HasFlag(ConsoleModifiers.Control));
}
return choices[cki.Key – (ConsoleKey.D1)];
}
[/code]
Of course, there is a very obvious limitation to this function. If you have more than 9 memory cards (or 9 of any object you provide to this function), you probably don’t have a “10” or “11” key on your keyboard. The will still show up, but you won’t be able to select them. Since I am going to be the main consumer of this application, and since I’m hoping to replace the console application version with a WPF version, I’m not really too worried about this limitation for now.
Using the function is easy. Once you have your list of memory cards, simply pass them (as an array) to the GetInput function:
[code language=”csharp”]
var selectedMemoryCard = GetInput(memoryCards.ToArray());
[/code]
Now that we have the selected memory card, we just have to go about copying the files from the memory card to the vault. We want to build a file structure that keeps the pictures organized, and I like to organize my pictures by the date they were taken. This way we are also able to check and see if we’ve already imported pictures from the memory card — the chances of having an image file with the same name on the same day as an image already in the vault is pretty rare unless you have multiple people shooting with multiple cameras on the same day and are trying to aggregate the results together… but if that’s the case you’re probably a professional and aren’t using some random program you found on a random guy’s blog.
I created a “copier” class that takes a memory card, a path representing the vault, and the type of images to look for (default .jpg). Eventually the path to the vault and type of image would be supplied by a configuration file (for the console application) or a settings screen (in the WPF application), but for now I’m just going to hard code it to a location on my hard drive. Since we have to look up the date an image was taken in order to figure out where it should go anyway, we can also record the size of the image to keep a running total of how many bytes are being transferred to the vault. If the image is already in the vault, we don’t want to copy the image, but we do want to note that it was skipped. There’s always an off-chance that something bad happens during the copy operation, so keeping track of the number of errors is useful, too. In addition, we want to note the directories affected by the import so that we can dig around in them afterwards to pick out the good images that we want to keep. As I explained in part one, I want to keep all of the images in the vault, but only put the good ones into a different spot (say, “My Pictures”) that can then be automatically synced to OneDrive or some other cloud storage medium.
Since this is just a console application, the progress of the application is just spit out to the console.
[code language=”csharp”]
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Importer
{
public class Copier
{
private string SourceLocation { get; set; }
private string VaultDirectory { get; set; }
private string PictureDirectory { get; set; }
private string FileType { get; set; }
public List<string> Directories { get; private set;} = new List<string>();
public double TotalBytes { get; private set; }
public double TotalKilobytes { get { return Math.Round((double)(TotalBytes / 1024), 2); } }
public double TotalMegabytes { get { return Math.Round((double)(TotalBytes / Math.Pow(1024,2)), 2); } }
public double TotalGigabytes { get { return Math.Round((double)(TotalBytes / Math.Pow(1024,3)), 2); } }
public double CopiedBytes { get; private set; }
public double CopiedKilobytes { get { return Math.Round((double)(CopiedBytes / 1024), 2); } }
public double CopiedMegabytes { get { return Math.Round((double)(CopiedBytes / Math.Pow(1024, 2)), 2); } }
public double CopiedGigabytes { get { return Math.Round((double)(CopiedBytes / Math.Pow(1024, 3)), 2); } }
public int TotalFiles { get; private set; }
public int FilesCopied { get; private set; }
public int FilesSkipped { get; private set; }
public int Errors { get; private set; }
/// <summary>
/// Copies pictures from the provided memory card to the vault directory and creates the picture directory.
/// </summary>
/// <param name="memoryCard">Source Media</param>
/// <param name="vaultDirectory">Destination for storing all images</param>
/// <param name="pictureDirectory">Destination for storing selected ("good") images</param>
public Copier(MemoryCard memoryCard, string vaultDirectory, string pictureDirectory, string fileType = ".jpg")
{
SourceLocation = memoryCard.DriveLetter;
VaultDirectory = vaultDirectory;
PictureDirectory = pictureDirectory;
FileType = fileType;
}
public void Copy()
{
var imageFilePaths = Directory.EnumerateFiles(SourceLocation, $"*{FileType}", SearchOption.AllDirectories);
TotalFiles = imageFilePaths.Count();
foreach (var imageFilePath in imageFilePaths)
{
try
{
FileInfo imageFile = new FileInfo(imageFilePath);
TotalBytes += imageFile.Length;
var destinationDirectory = Path.Combine(VaultDirectory, imageFile.CreationTime.ToString("yyyyMMdd"));
if (!Directory.Exists(destinationDirectory)) Directory.CreateDirectory(destinationDirectory);
if (!Directories.Any(d => d.Equals(destinationDirectory, StringComparison.InvariantCultureIgnoreCase)))
Directories.Add(destinationDirectory);
var destinationPath = Path.Combine(destinationDirectory, imageFile.Name);
if (!File.Exists(destinationPath))
{
CopiedBytes += imageFile.Length;
Console.WriteLine($"Copying {imageFile.Name} ({Math.Round(((double)imageFile.Length / (1024 * 1024)), 2)} MB)");
File.Copy(imageFilePath, destinationPath);
FilesCopied++;
}
else
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"Skipping {imageFile.Name}, already exists in the destination");
Console.ForegroundColor = ConsoleColor.White;
FilesSkipped++;
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error copying file: {ex.Message}");
Console.ForegroundColor = ConsoleColor.White;
Errors++;
}
}
Console.WriteLine($"n———-n{ToString()}");
}
public override string ToString()
{
return $"{FilesCopied} / {TotalFiles} ({CopiedMegabytes} MB) copied ({FilesSkipped} skipped, {Errors} errors.";
}
}
}
[/code]
When we search for files, we’re using the SearchOption.AllDirectories flag. This will do a recursive dive into the folder structure and find all of the images with the specified type. That way, if I for some reason switch to a different camera brand that has a slightly different folder structure when saving images, my importer program will still be able to find everything.
And that’s it! Now we have all of the images from the memory card copied into our vault.