Hier jetzt ein aktualisiertes Beispielprogramm:
#include <array>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <unordered_map>
#include <vector>
namespace fs = std::filesystem;
static constexpr std::streamsize BufSize{ 4096 };
template<std::streamsize BufSize>
bool is_duplicate_file(const fs::directory_entry& file_a, const fs::directory_entry& file_b) {
if (!file_a.is_regular_file() || !file_b.is_regular_file()) {
return false;
}
if (fs::equivalent(file_a, file_b)) {
return true;
}
if (file_a.file_size() != file_b.file_size()) {
return false;
}
std::ifstream a_in{ file_a.path(), std::ios::binary };
std::ifstream b_in{ file_b.path(), std::ios::binary };
if (!a_in || !b_in) {
throw std::runtime_error("is_duplicate_file: Unable to open file(s) for reading");
}
if (a_in.peek() != b_in.peek()) {
return false;
}
std::array<char, BufSize> buf_a{};
std::array<char, BufSize> buf_b{};
while (a_in && b_in) {
a_in.read(buf_a.data(), BufSize);
b_in.read(buf_b.data(), BufSize);
/* Seems superfluous since what can fail besides hitting EOF on ifstream::read().
if (a_in.fail() && !a_in.eof() || a_in.bad() || b_in.fail() && !b_in.eof() || b_in.bad()) {
throw std::runtime_error("is_duplicate_file: Unexpected file read error while comparing");
}
*/
if (std::memcmp(buf_a.data(), buf_b.data(), BufSize))
return false;
}
return true;
}
auto build_file_index(const fs::path& start_path) {
std::unordered_multimap<std::uintmax_t, fs::directory_entry> files_by_size;
for (const auto& entry : fs::recursive_directory_iterator{ start_path }) {
if (entry.is_regular_file()) {
files_by_size.emplace(entry.file_size(), entry);
}
}
return files_by_size;
}
auto find_duplicate_files(const auto& files) {
std::unordered_map<fs::path, std::vector<fs::path>> dupes;
for (auto entry = files.begin(); entry != files.end(); ) {
auto range{ files.equal_range(entry->first) };
int idx_i{};
std::vector<bool> matched(std::distance(range.first, range.second), false); // std::unordered_map<fs::path, bool> works, too!
for (auto i = range.first; i != range.second; ++i, ++idx_i) {
int idx_j{ idx_i + 1 };
for (auto j = std::next(i); j != range.second; ++j, ++idx_j) {
if (!matched[idx_i] && !matched[idx_j] && is_duplicate_file<BufSize>(i->second, j->second)) {
dupes[i->second.path()].emplace_back(j->second.path());
matched[idx_j] = true;
}
}
}
entry = range.second;
}
return dupes;
}
void print_dupes(const auto& dupes) {
for (const auto& a : dupes) {
std::cout << "\nReference: " << a.first << '\n';
for (const auto& b : a.second) {
std::cout << "Duplicate: " << b << '\n';
}
}
std::cout << "\nFound a total of " << dupes.size() << " duplicate groups.\n\n";
}
int main(int argc, char* argv[])
try {
if (argc != 2) {
std::cout << "Program expects one argument: <Starting path>\n";
return 1;
}
fs::path start_path{ argv[1] };
if (!fs::exists(start_path)) {
std::cout << "Provided argument is not an existing path.\n";
return 2;
}
print_dupes(
find_duplicate_files(
build_file_index(start_path)));
}
catch (const fs::filesystem_error& e) {
std::cerr << e.what() << '\n';
return e.code().value();
}
catch (const std::runtime_error& e) {
std::cerr << e.what() << '\n';
return -2;
}
catch (...) {
std::cerr << "Unhandled exception!\n";
return -1;
}
Edit 1: Ich hatte beim Programmieren auch an std::unordered_map<fs::path, bool> gedacht, was sich performance-technisch nicht viel schenkt mit std::vector<bool>. Inzwischen wurde ich darauf hingewiesen, dass man auch einfach std::unordered_set<fs::path> verwenden könnte - auch von der Performance her ähnlich, aber wahrscheinlich eleganter als dieser Pfusch mit den Indizes.
Die Idee war klar, wenn z. B. Dateien A, B, C mit Größe X geprüft werden, und ich habe bereits erkannt, dass Datei B mit A identisch sind, dann brauche ich gar nicht erst versuchen, Datei B auch noch mit C zu vergleichen (das wäre sinnlos, wenn B==C, dann wäre C bereits gegen A gefunden worden).
Edit 2:
Jetzt so:
auto find_duplicate_files(const auto& files) {
std::unordered_map<fs::path, std::vector<fs::path>> duplicates;
std::unordered_set<fs::path> matched;
for (auto left = files.begin(); left != files.end(); ) {
auto range = files.equal_range(left->first);
left = range.second;
for (auto ref_it = range.first; ref_it != range.second; ++ref_it) {
if (matched.contains(ref_it->second.path())) {
continue;
}
for (auto file_it = std::next(ref_it); file_it != range.second; ++file_it) {
if (!matched.contains(file_it->second.path()) && is_duplicate_file<BufSize>(ref_it->second, file_it->second)) {
matched.emplace(file_it->second.path());
duplicates[ref_it->second.path()].emplace_back(file_it->second.path());
}
}
}
}
return duplicates;
}