// Package digest 负责给文件算"身份证号"(MD5),以及一种偷懒的指纹算法。 // // 大白话理解: // - MD5:把整个文件的内容算出一串固定长度的数字,内容一样 MD5 就一样, // 所以可以用它来判断"两个文件是不是一模一样"。 // - 指纹:大文件(几 GB 的视频)算全量 MD5 很慢。于是只读文件"开头 256KB // 和结尾 256KB"各算一个 MD5,加上文件大小,凑成一个"指纹"。 // 指纹一样基本就说明是同一个文件——先用它去数据库碰碰运气, // 能对上就直接复用已有的 MD5,省掉全量计算。 package digest import ( "crypto/md5" "encoding/hex" "io" "os" ) const ( // FingerprintThreshold 文件大于这个字节数(1MB)才值得用指纹偷懒; // 小文件全量算 MD5 也没多慢,没必要麻烦。 FingerprintThreshold = 1 << 20 fingerprintHeadSize = 256 << 10 // 文件开头采样 256KB fingerprintTailSize = 256 << 10 // 文件结尾采样 256KB ) // CalcFileMd5 计算整个文件的 MD5。分块读取,大文件也能吃下不占太多内存。 func CalcFileMd5(filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() h := md5.New() if _, err := io.Copy(h, file); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } // CalcFileFingerprint 计算文件的"指纹":开头 256KB 的 MD5 + 结尾 256KB 的 MD5。 // 只读很少一点字节,用来快速判断"是不是和之前见过的文件一样"。 // 注意:文件大小必须大于开头+结尾采样的大小(即大于 512KB),否则读不满,会报错。 func CalcFileFingerprint(filePath string, size int64) (headMd5, tailMd5 string, err error) { file, err := os.Open(filePath) if err != nil { return "", "", err } defer file.Close() // 开头 256KB h := md5.New() if _, err := io.CopyN(h, file, fingerprintHeadSize); err != nil { return "", "", err } headMd5 = hex.EncodeToString(h.Sum(nil)) // 跳到"结尾往前 256KB"的位置,再读 256KB h = md5.New() if _, err := file.Seek(size-fingerprintTailSize, io.SeekStart); err != nil { return "", "", err } if _, err := io.CopyN(h, file, fingerprintTailSize); err != nil { return "", "", err } tailMd5 = hex.EncodeToString(h.Sum(nil)) return headMd5, tailMd5, nil }