【项目C++】重复(内容)文件管理工具

功能

本项目旨在实现一个文件管理工具,主要功能是删除磁盘中的重复文件。如何判定两个文件是否完全一致,采用计算文件指纹的方法,所谓的文件指纹即数字签名。

实现过程

(一) 计算文件指纹(了解MD5算法实现)

MD5是由Ron Rivest在1991设计的一种 信息摘要(message-digest ) 算法,当给定任意长度的信息,MD5会产生一个固定的128位 “指纹”或者叫信息摘要。从理论的角度,所有的信息产生的MD5值都不同,也无法通过给定的MD5值产生任何信息,即不可逆。MD5算法在RFC 1321(Request For Comments,征求修正意见书)做了详细描述。

MD5功能特点

1.输入任意长度的信息,经过处理,输出为128位的信息(数字指纹)
2.不同的输入得到的不同的结果(唯一性)。要使两个不同的信息产生相同的摘要,操作数量级在2^64次方。
3.根据128位的输出结果不可能反推出输入的信息。根据给定的摘要反推原始信息,它的操作数量级在2^128次。

MD5算法步骤
1. 添加填充位

信息的最尾部(不是每一块的尾部)要进行填充,使其最终的长度length(以bit为单位)满足length % 512 = 448,这一步始终要执行,即使信息的原始长度恰好符合上述要求。
填充规则:第一个bit填充位填 ‘1’ ,后续bit填充位都填 ‘0’ ,最终使消息的总体长度满足上述要求。总之,至少要填充 1 bit,至多填充 512 bit。

2.添加bit长度

在上一步之后,添加一个64bit大小的length,length表示原始消息(未填充之前)的bit长度,极端情况,如果消息长度超过2^64, 那么只使用前2^64bit消息。
这一步完成之后,消息的最终长度变为(length + 64) % 512 = 0,即length为512的整数倍。从这里再去看第一步,至少需要填充 8 bit,为什么?我们假设几种情况分析一下:
首先要明确一个字符占1byte(8bit, 中文字符的话占16bit),所以原始信息bit长度一定是8的倍数。

  • 假设1:消息原始长度 % 512 = 448
      这时候原始长度符合要求,但是根据填充规则,仍然要至少填1bit的 ‘1’,后面还剩63bit,不够添加长度,所以需要再加一块数据(512bit),这样后面63bit填0,新加的数据前448bit填0,最后64bit填数据原始长度,一定要记住长度值是放在最后一块数据的最后64bit。
    在这里插入图片描述
  • 假设2:消息原始长度 % 512 < 448
      此时最后一块数据长度不大于440bit,最后64bit填数据长度,需要填充的bit数不小于8。
    在这里插入图片描述
  • 假设3:消息原始长度 % 512 > 448
      此时最后一块数据长度不小于456,最多504,剩余bit不够添加64位长度,和假设1相同,需要增加一块数据,最后64位添加长度,其余填充0。
    在这里插入图片描述
3. 初始化MD buffer

用4-word buffer(A, B, C, D)计算摘要,这里A,B,C,D各为一个32bit的变量,这些变量初始化为下面的十六进制值,低字节在前。MD5算法根据文件数据来更新A,B,C,D的值,最终根据这4个值得出唯一的字符串。

/*
word A: 01 23 45 67
word B: 89 ab cd ef
word C: fe dc ba 98
word D: 76 54 32 10
*/
// 初始化A,B,C,D
_A = 0x67452301;
_B = 0xefcdab89;
_C = 0x98badcfe;
_D = 0x10325476;
4. 按512位数据逐块处理输入信息

512bit数据为一个处理单位,暂且称为一个数据块chunk,每个chunk经过4个函数(F, G, H, I)处理,这四个函数输入为3个32位(4字节)的值,产生一个32位的输出。四个函数如下所示:

F(x,y,z) = (x & y) | ((~x) & z)
G(x,y,z) = (x & z) | ( y & (~z))
H(x,y,z) = x ^ y ^ z
I(x,y,z) = y ^ (x | (~z))

处理过程中要用一个含有64个元素的表K[1…64],表中的元素值由sin函数构建,K[i]等于2^(32) * abs(sin(i))的整数部分,即:

K[i] = floor(2^(32) * abs(sin(i + 1))) // 因为此处i从0开始,所以需要sin(i + 1)
for (int i = 0; i < 64; i++) {
	_k[i] = (size_t)(abs(sin(i + 1)) * pow(2, 32));
}

512bit = 64byte * 8 = 16 * 4byte
在处理一个chunk(512bit)的数据时,会把这个chunk再细分成16组4字节数据,一个chunk经过4轮进行处理,每轮都会把chunk的所有数据处理一遍,每轮有16个相似的子操作,所以一个chunk的数据要进行64个子操作。
计算之前先保存MD buffer的当前值:
    a = A, b = B, c = C, d = D
在这里插入图片描述

图中的<<<s表示循环左移操作。

根据上图得到四轮处理过程:
第一轮:F函数处理(0 <= i <= 15)

F = F(b, c, d)
d = c
c =b
b = b + shift((a + F + k[i] + chunk[g]), s[i])
a = d

第二轮:G函数处理 (16 <= i <= 31)

G = G(b, c, d)
d = c
c =b
b = b + shift((a + G + k[i] + chunk[g]), s[i])
a = d

第三轮:H函数处理(32 <= i <= 47)

H = H(b, c, d)
d = c
c =b
b = b + shift((a + H + k[i] + chunk[g]), s[i])
a = d

第四轮:I函数处理(48 <= i <= 63)

I = I(b, c, d)
d = c
c =b
b = b + shift((a + I + k[i] + chunk[g]), s[i])
a = d

上面式子里的s[] 和 g 满足按照下面所给计算:

size_t s[] = { 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7,
12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 };
if (0 <= i < 16) g = i;
if (16 <= i < 32) g = (5 * i + 1) % 16;
if (32 <= i < 48) g = (3 * i + 5) % 16;
if(48 <= i < 63) g = (7 * i) % 16;

一个chunk数据处理完之后,更新MD buffer的值A, B, C, D

_A = a + _A;
_B = b + _B;
_C = c + _C;
_D = d + _D;

处理完一个chunk数据块,接着处理下一块

5. 文件指纹输出

这一步拼接4个buffer(_A,_B,_C,_D)中的摘要信息,以_A中的低位字节开始,_D的高位字节结束。最终的输出是128bit摘要信息的16进制表示,故最后输出一个32长度的摘要信息。
举例:比如一个数,它的16进制表示为: 0x23456789, 他所对应的8个摘要信息为从低位字节的89开始,高位字节的23结束,即: 89674523。

MD5算法核心接口
class MD5 {
public:
	MD5();
	void init();
	string getStringMD5(const string& str);
	string getFileMD5(const char* filePath);
private:
	//从文件获取的512bit的数据块
	char _chunk[64];
	//最后要返回的4个整型值 _A _B _C _D
	uint32_t _A, _B, _C, _D;
	//循环左移位数
	static size_t s[64];
	//中间运算式的固定变量	_k[i] = (size_t)(abs(sin(i + 1)) * pow(2, 32))
	uint32_t _k[64];
	//最后一块数据的字节数
	uint32_t _lastByte;
	//总字节数
	uint32_t _totalByte;
	//num循环左移n位
	uint32_t leftShift(uint32_t num, int n) {
		return (num << n) | (num >> (32 - n));
	}
	uint32_t F(uint32_t x, uint32_t y, uint32_t z) {
		return (x & y) | ((~x) & z);
	}
	uint32_t G(uint32_t x, uint32_t y, uint32_t z) {
		return (x & z) | (y & (~z));
	}
	uint32_t H(uint32_t x, uint32_t y, uint32_t z) {
		return x ^ y ^ z;
	}
	uint32_t I(uint32_t x, uint32_t y, uint32_t z) {
		return y ^ (x | (~z));
	}
	//一个chunk的MD5运算
	void calMD5(const uint32_t* Chunk);
	//把一个整数数据转换为一个16进制字符串
	string transHex(uint32_t n);
	void calFinalMD5();
};
string MD5::getStringMD5(const string& str) {
	if (str.empty()) {
		return transHex(_A).append(transHex(_B)).append(transHex(_C)).append(transHex(_D));
	}
	_totalByte = str.size();
	uint32_t chunkNum = _totalByte / 64;
	const char* strPtr = str.c_str();
	for (int i = 0; i < chunkNum; ++i) {
		memcpy(_chunk, strPtr + i * 64, 64);
		calMD5((uint32_t*)_chunk);
	}
	//计算最后一块数据:需要填充
	_lastByte = _totalByte % 64;
	memcpy(_chunk, strPtr + chunkNum * 64, _lastByte);
	calFinalMD5();
	return transHex(_A).append(transHex(_B)).append(transHex(_C)).append(transHex(_D));
}
string MD5::getFileMD5(const char* filePath) {
	ifstream fin(filePath);
	if (!fin.is_open()) {
		cout << filePath << " ";
		perror("file open error");
		return "";
	}
	while (!fin.eof()) {
		//每次只读取一块数据(64个字节)
		fin.read(_chunk, 64);
		//gcount返回上次读取的字节数
		//如果不是64个字节,则是最后一块数据
		if (fin.gcount() != 64) {
			break;
		}
		_totalByte += 64;
		calMD5((uint32_t*)_chunk);
	}
	_lastByte = fin.gcount();
	_totalByte += _lastByte;
	calFinalMD5();
	return transHex(_A).append(transHex(_B)).append(transHex(_C)).append(transHex(_D));
}
(二) 扫描文件夹

参照文档:https://www.cnblogs.com/ranjiewen/p/5960976.html
用 _findfirst 和 _findnext 查找文件

  1. _findfirst函数:long _findfirst(const char *, struct _finddata_t *);
    第一个参数为文件名,可以用"*.*“来查找所有文件,也可以用”*.cpp"来查找.cpp文件。第二个参数是_finddata_t结构体指针。若查找成功,返回文件句柄,若失败,返回-1。
  2. _findnext函数:int _findnext(long, struct _finddata_t *);
    第一个参数为文件句柄,第二个参数同样为_finddata_t结构体指针。若查找成功,返回0,失败返回-1。
  3. _findclose()函数:int _findclose(long);
    只有一个参数,文件句柄。若关闭成功返回0,失败返回-1。
struct _finddata_t {
    unsigned    attrib;
    time_t      time_create;   
    time_t      time_access;   
    time_t      time_write;
    _fsize_t    size;
    char        name[260];
};
//不遍历子目录
void transferDir(const string& folderPath, unordered_set<string>& fileString);
//递归遍历
void dfsFolder(const string& folderPath, unordered_set<string>& fileString) {
	_finddata_t FileInfo;
	string strfind = folderPath + "\\*";
	long Handle = _findfirst(strfind.c_str(), &FileInfo);
	if (Handle == -1L) {
		cout << "can not match the folder path" << endl;
		exit(-1);
	}
	do {
		if (FileInfo.attrib & _A_SUBDIR) {
			if ((strcmp(FileInfo.name, ".") != 0) && (strcmp(FileInfo.name, "..") != 0)) {
				string newPath = folderPath + "\\" + FileInfo.name;
				dfsFolder(newPath, fileString);
			}
		}
		else {
			fileString.insert(folderPath + "\\" + FileInfo.name);
			//cout << folderPath.c_str() << "\\" << FileInfo.name << endl;
		}
	} while (_findnext(Handle, &FileInfo) == 0);
	_findclose(Handle);
}
(三) 选择数据结构(STL容器)保存重复的文件来封装文件管理工具
  1. 选择unordered_set容器保存当前目录下的所有文件名称
    选择原因:因为文件名称既包括该文件名又包含该文件的路径,所以不可能有重复的文件名,所以不选用multiset。又因为只保存名称不注重顺序,为了提高效率,选用了底层为哈希表的unordered_set。
  2. 选择unordered_map保存文件名称到MD5值的映射关系
    选择原因:因为名称不会重复,所以不必要选择multimap,哈希表结构的unordered_map可以提高效率。
  3. 选择unordered_multimap保存MD5值到文件名称的映射关系
    选择原因:因为MD5值可能重复,所以要使用multimap,又使用unordered_multimap提高效率。

下述代码只给出声明未给出定义,实现函数主要注意逻辑和对熟悉几种容器的接口

class FileManager {
public:
	void init();
	//扫描当前目录(包括子目录)的文件
	void scanFolder(const string& filePath);
	//显示选项 不包括子目录
	void scanFileNoR(const string& filePath);
	void showRepetitiveFile();
	//通过文件名删除文件
	void deleteFileByName(const string& filepath);
	//通过MD5值删除文件
	void deleteFileByMD5(const string& md5);
	//删除所有保留一个文件
	void deleteFileRemainOne(const string& md5, const string& filename);
	//删除所有重复文件
	void deleteAllRepetitive();
private:
	unordered_set<string> _filename;
	//filename ---> MD5
	unordered_map<string, string> file_MD5;
	//MD5 ---> filename
	unordered_multimap<string, string> MD5_file;
	MD5 _md5;
};
(四) 最后制作菜单选项并与管理工具接口关联
class menu {
public:
	char init() {
		cout << "******************************************************" << endl;
		cout << "\t\t管理重复内容的文件工具" << endl;
		cout << "\t\t  1. 扫描当前目录下文件" << endl;
		cout << "\t\t  2. 递归扫描当前目录下所有文件" << endl;
		cout << "******************************************************" << endl;
		cout << "请选择:" << endl;
		char c = getchar();
		char tmp = getchar();
		return c;
	}
	void press(const char& c) {
		string dir;
		if (c == '1') {
			cout << "请输入要扫描的目录路径 : " << endl;
			getline(cin, dir);
			fmer.scanFileNoR(dir);
		} else if(c == '2') {
			cout << "请输入要扫描的目录路径 : " << endl;
			getline(cin, dir);
			fmer.scanFolder(dir);
		} else {
			cout << "违规操作, 请重新选择" << endl;
			char ch = init();
			press(ch);
		}
	}
	char deletOptions() {
		cout << endl << "  删除选项" << endl;
		cout << "\t 1. 打印所有相同文件" << endl;
		cout << "\t 2. 通过文件名删除文件" << endl;
		cout << "\t 3. 通过MD5删除文件(随机保留一个文件)" << endl;
		cout << "\t 4. 通过MD5删除文件(选择要保留的文件)" << endl;
		cout << "\t 5. 删除所有重复文件" << endl;
		cout << "\t 6. 返回上一级" << endl;
		cout << "请选择:" << endl;
		char c = getchar();
		char tmp = getchar();
		return c;
	}
	void deletPress(const char& c) {
		string filename;
		string md5;
		char re;
		switch (c) {
		case '1' :
			fmer.showRepetitiveFile();
			re = deletOptions();
			deletPress(re);
			break;
		case '2' :
			cout << "请输入要删除的文件名:" << endl;
			getline(cin, filename);
			fmer.deleteFileByName(filename);
			break;
		case '3' :
			cout << "请输入要删除的MD5值:" << endl;
			getline(cin, md5);
			fmer.deleteFileByMD5(md5);
			break;
		case '4' :
			cout << "请输入要删除的MD5值:" << endl;
			getline(cin, md5);
			cout << "请输入要保留的文件名:" << endl;
			getline(cin, filename);
			/*debug
			for (const auto& e : fmer.MD5_file) {
				cout << e.first << "---" << e.second << endl;
			}
			*/
			fmer.deleteFileRemainOne(md5, filename);
			/*
			for (const auto& e : fmer.MD5_file) {
				cout << e.first << "---" << e.second << endl;
			}
			*/
			break;
		case '5' :
			fmer.deleteAllRepetitive();
			break;
		case '6' :
			break;
		default:
			cout << "输入违规, 请重新输入" << endl;
			re = deletOptions();
			deletPress(re);
			break;
		}
		return;
	}
private:
	FileManager fmer;
};
int main() {
	while (1) {
		menu m;
		char c = m.init();
		m.press(c);
		char ch = m.deletOptions();
		m.deletPress(ch);
		system("cls");
	}
	return 0;
}
运行效果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值