Skip to content

Instantly share code, notes, and snippets.

@ducas
Created June 27, 2011 06:06
Show Gist options
  • Save ducas/1048378 to your computer and use it in GitHub Desktop.
Save ducas/1048378 to your computer and use it in GitHub Desktop.
Hash - A quick file hasher that recursively generates and compares hash files
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
namespace Hash
{
public class Program
{
private static readonly Output output = new Output();
private static readonly GenerateCommand generate = new GenerateCommand();
private static readonly CompareCommand compare = new CompareCommand();
private static readonly DiffCommand diff = new DiffCommand();
public static void Main(string[] args)
{
if (args.Length == 0 || args[0] == "?" || args[0] == "/?" || args[0] == "help") { output.Usage(); return; }
var command = args[0].ToLower();
if (!(new[] { "generate", "compare", "diff" }).Contains(command)) { output.Usage(); return; }
if (args.Length == 1) { output.WriteError("Please specify a root directory."); output.Usage(); return; }
var root = args[1];
if (!Directory.Exists(root)) { output.WriteError("Directory '" + root + "' does not exist."); return; }
switch (command)
{
case "generate":
generate.Execute(new DirectoryInfo(root), args.Length == 3 ? args[2] : "*");
break;
case "compare":
compare.Execute(new DirectoryInfo(root), args.Length == 3 ? args[2] : "*");
break;
case "diff":
if (args.Length == 2) { output.WriteError("Please specify a second root directory to diff."); output.Usage(); return; }
var root2 = args[2];
if (!Directory.Exists(root)) { output.WriteError("Directory '" + root2 + "' does not exist."); return; }
diff.Execute(new DirectoryInfo(root), new DirectoryInfo(root2));
break;
default:
output.Usage();
return;
}
output.Write("Done!");
}
}
public class Command
{
protected readonly HashGenerator generator = new HashGenerator();
protected readonly HashDictionarySerializer serializer = new HashDictionarySerializer();
protected readonly Output output = new Output();
protected void CompareHashes(Dictionary<string, string> left, Dictionary<string, string> right)
{
foreach (var item in left)
if (!right.ContainsKey(item.Key))
output.WriteMissing(item.Key + " missing from previously generated hashes.");
else if (right[item.Key] == item.Value)
output.WriteSame(item.Key + " same");
else output.WriteError(item.Key + " different");
foreach (var item in right)
if (!left.ContainsKey(item.Key)) output.WriteMissing(item.Key + " missing from file system.");
}
}
public class GenerateCommand : Command
{
public void Execute(DirectoryInfo directory, string filter)
{
output.Write("Generating hashes for " + directory.FullName);
var hashes = generator.GetHashes(directory, filter);
if (hashes.Count > 0)
serializer.Serialize(hashes, Path.Combine(directory.FullName, ".hashes"));
foreach (var child in directory.GetDirectories())
Execute(child, filter);
}
}
public class CompareCommand : Command
{
public void Execute(DirectoryInfo directory, string filter)
{
output.Write("Comparing " + directory.FullName);
var left = generator.GetHashes(directory, filter);
var right = serializer.Deserialize(Path.Combine(directory.FullName, ".hashes"));
if (left.Count > right.Count && right.Count == 0)
output.WriteMissing("No previously generated .hashes.");
else
CompareHashes(left, right);
foreach (var child in directory.GetDirectories())
Execute(child, filter);
}
}
public class DiffCommand : Command
{
public void Execute(DirectoryInfo directory, DirectoryInfo directory2)
{
output.Write("Comparing " + directory.FullName + " to " + directory2.FullName);
var leftHashFiles = directory.GetFiles(".hashes", SearchOption.AllDirectories);
var rightHashFiles = directory2.GetFiles(".hashes", SearchOption.AllDirectories);
foreach (var left in leftHashFiles)
{
var relativeFileName = left.FullName.Replace(directory.FullName, directory2.FullName);
var right = rightHashFiles.FirstOrDefault(f => f.FullName.Equals(relativeFileName, StringComparison.InvariantCultureIgnoreCase));
if (right == null)
{
output.WriteMissing(left.FullName + " - " + relativeFileName + " missing from file system.");
continue;
}
output.Write(left.FullName + " - comparing to " + right.FullName);
var leftHashes = serializer.Deserialize(left.FullName);
var rightHashes = serializer.Deserialize(right.FullName);
CompareHashes(leftHashes, rightHashes);
}
foreach (var right in rightHashFiles)
{
var relativeFileName = right.FullName.Replace(directory2.FullName, directory.FullName);
if (!leftHashFiles.Any(f => f.FullName == relativeFileName)) output.WriteMissing(right.FullName + " - " + relativeFileName + " missing from file system.");
}
}
}
public class HashGenerator
{
public Dictionary<string, string> GetHashes(DirectoryInfo directory, string filter)
{
var hashes = new Dictionary<string, string>();
foreach (var file in directory.GetFiles(filter).Where(f => f.Name != ".hashes"))
hashes.Add(file.Name, GetHash(file.FullName));
return hashes;
}
public string GetHash(string filePath)
{
using (var algorithm = new SHA1Managed())
using (var file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
byte[] hash = algorithm.ComputeHash(file);
return BitConverter.ToString(hash).Replace("-", "");
}
}
}
public class HashDictionarySerializer
{
public void Serialize(Dictionary<string, string> hashes, string filePath)
{
using (var writer = File.CreateText(filePath))
foreach (var item in hashes)
writer.WriteLine(item.Key + ":" + item.Value);
}
public Dictionary<string, string> Deserialize(string filePath)
{
var result = new Dictionary<string, string>();
if (!File.Exists(filePath)) return result;
using (var reader = File.OpenText(filePath))
{
var line = "";
while ((line = reader.ReadLine()) != null)
{
var parts = line.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
result.Add(parts[0], parts[1]);
}
}
}
return result;
}
}
public class Output
{
public void Usage()
{
Write(@"
hash - a simple hashing tool that recursively traverses a directory structure generating hash files (.hashes) for comparison.
hash <command> <options>
generate:
hash generate <root_directory> [filter]
recursively generates hash files in each directory of all files.
compare:
hash compare <root_directory> [filter]
recursively compares hash files to the current state of the file system.
diff:
hash diff <root_directory_1> <root_directory_2>
recursively compares all hash files under two root directories.
diff & compare:
search output for same, missing and different.
");
}
public void Write(string message)
{
Console.WriteLine(message);
}
public void WriteError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
Console.ResetColor();
}
public void WriteSame(string message)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(message);
Console.ResetColor();
}
public void WriteMissing(string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(message);
Console.ResetColor();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment