【问题标题】:Verifying that two files are identical using pure PHP?使用纯 PHP 验证两个文件是否相同?
【发布时间】:2013-09-21 21:30:36
【问题描述】:

TL;DR:我有一个 CMS 系统,它使用文件内容的 SHA-1 作为文件名来存储附件(不透明文件)。鉴于我已经知道两个文件的 SHA-1 哈希匹配,如何验证上传的文件是否真的与存储中的一个匹配?我想要高性能。

加长版:

当用户向系统上传新文件时,我会计算上传文件内容的 SHA-1 哈希值,然后检查存储后端中是否已存在具有相同哈希值的文件。 PHP 在我的代码运行之前将上传的文件放在/tmp 中,然后我对上传的文件运行sha1sum 以获取文件内容的SHA-1 哈希。然后,我从计算的 SHA-1 哈希计算扇出,并在 NFS 挂载目录层次结构下确定存储目录。 (例如,如果文件内容的 SHA-1 哈希是 37aefc1e145992f2cc16fabadcfe23eede5fb094,则永久文件名是 /nfs/data/files/37/ae/fc1e145992f2cc16fabadcfe23eede5fb094。)除了保存实际文件内容之外,我将 INSERT 在 SQL 数据库中换行用户提交的元数据(例如Content-Type、原始文件名、日期戳等)。

我目前正在解决的极端情况是,新上传的文件的 SHA-1 哈希值与存储后端中的现有哈希值相匹配。我知道这种意外发生的变化是天文数字的低,但我想确定一下。 (特意案例请见https://shattered.io/)

给定两个文件名$file_a 和$file_b,如何快速检查两个文件的内容是否相同?假设文件太大而无法加载到内存中。对于 Python,我会使用filecmp.cmp(),但 PHP 似乎没有类似的东西。我知道这可以通过fread() 来完成,如果找到不匹配的字节则中止,但我宁愿不编写该代码。

【问题讨论】:

  • 您是否正在尝试对冲哈希冲突?
  • 使用散列是个好主意。正如您所提到的,碰撞的可能性非常低 - 所以您可以确定在一般情况下,它会没事的。如果没有 - 请让我们知道您对这些文件内容的情况:p
  • git 正在使用 sha1,所以我认为您可以安全地使用 sha1 :)
  • 我试图避免由于哈希冲突而丢失文件内容。是的,如果我看到碰撞,我会保留这两个文件。我敢打赌,在那种情况下,我会发现我的永久存储已经烂掉了。 (在任何存储设备上出现随机位错误的变化似乎比发现 SHA-1 冲突要高得多;在这种情况下,我仍然想要一个损坏文件的新副本。)
  • @Kakawait: @Kakawait: git 据我所知,在仅仅因为 SHA-1 哈希恰好匹配而相信文件相同之前,也会进行逐字节比较测试。

标签: php performance file-upload file-io


【解决方案1】:

如果您已经有一个 SHA1 总和,您可以这样做:

if ($known_sha1 == sha1_file($new_file))

否则

if (filesize($file_a) == filesize($file_b)
    && md5_file($file_a) == md5_file($file_b)
)

还要检查文件大小,以在一定程度上防止哈希冲突(这已经很不可能了)。也使用 MD5,因为它比 SHA 算法快得多(但不那么独特)。


更新:

这是准确比较两个文件的方法。

function compareFiles($file_a, $file_b)
{
    if (filesize($file_a) != filesize($file_b))
        return false;

    $chunksize = 4096;
    $fp_a = fopen($file_a, 'rb');
    $fp_b = fopen($file_b, 'rb');
        
    while (!feof($fp_a) && !feof($fp_b))
    {
        $d_a = fread($fp_a, $chunksize)
        $d_b = fread($fp_b, $chunksize);
        if ($d_a === false || $d_b === false || $d_a !== $d_b)
        {
            fclose($fp_a);
            fclose($fp_b);
            return false;
        }
    }
 
    fclose($fp_a);
    fclose($fp_b);
          
    return true;
}

【讨论】:

  • MD5 和 SHA-1 之间的差异很容易与实际从存储中获取位所需的 IO 相比相形见绌。永久文件存储使用 1Gbps 连接挂载 NFS,这显然是对整个文件进行哈希处理的瓶颈。
  • @MikkoRantalainen 我已经在我的答案中添加了代码,可以准确地比较这两个文件。
  • 您错过了两个fclose() 调用,如果您在filesize() 测试失败后立即返回,代码看起来会更好。遗憾的是 PHP 默认不提供这样的功能。
  • 对于 500 个大小为 1Mb - 10Mb 的图像文件,最佳做法是什么? SHA1、MD5还是直接比较?什么表现最好?
  • fread($fp_a, 4096) 在 EOF 处返回空字符串 ""。所以这个循环是无限的。你应该添加while (!feof($fp_a) && ($b = fread($fp_a, 4096)) !== false)
【解决方案2】:

更新

如果要确保文件相等,则应首先检查文件大小,如果它们匹配,则只需区分文件内容。这比使用散列函数要快得多,而且肯定会给出正确的结果。


如果您使用 md5_file() 或 sha1_file() 或其他 hash_function 对内容进行哈希处理,则不需要将整个文件内容加载到内存中。下面是一个使用md5的例子:

$hash = md5_file('big.file'); // big.file is 1GB  in my test
var_dump(memory_get_peak_usage());

输出:

int(330540)

在你的例子中是:

if(md5_file('FILEA') === md5_file('FILEB')) {
    echo 'files are equal';
}

进一步注意,当您使用哈希函数时,您总是会遇到这样一种情况,您需要在一方面的复杂性和另一方面的冲突概率(意味着两条不同的消息产生相同的哈希)之间做出决定.

【讨论】:

  • @hek2mgl:谢谢,我不知道 PHP 实现足够健全,不会将整个文件读入内存。我不再需要使用shell_exec() 和sha1sum 来处理大文件。
  • 是的,他们经常被遗忘 :) .. 还可以看看其他可能更快的哈希函数。但是这些必须再次使用shell_exec() 调用
  • 如果 md5 哈希匹配,我不会声称 files are equal。我会声明files are probably equal,当 SHA-1 哈希匹配时,我已经可以声明这种情况。
  • @MikkoRantalainen 如果您想确保它们是相等的哈希函数,则根本不适合。使用diff .. 速度更快,可以回答问题
  • @hek2mgl 哈希作为第一步非常聪明,因为情况是我有 2e6 文件在永久存储中并且我收到了一个新文件。我有每个存储文件的现有 SHA-1 列表,因此我首先为新文件计算 SHA-1。任何与存储的 SHA-1 的匹配都应被视为候选匹配,而不是真正的匹配。
【解决方案3】:

当您的文件很大并且是二进制文件时,您可以从几个偏移量中测试它的几个字节。它应该比任何散列函数都快得多,尤其是该函数按第一个不同字符返回结果。

但是,此方法不适用于只有几个不同字符的文件。它是大型档案、视频等的最佳选择。

function areFilesEqual($filename1, $filename2, $accuracy)
{

    $filesize1 = filesize($filename1);
    $filesize2 = filesize($filename2);

    if ($filesize1===$filesize2) {

        $file1 = fopen($filename1, 'r');
        $file2 = fopen($filename2, 'r');

        for ($i=0; $i<$filesize1 && $i<$filesize2; $i+=$accuracy) {
            fseek($file1, $i);
            fseek($file2, $i);
            if (fgetc($file1)!==fgetc($file2)) return false;
        }

        fclose($file1);
        fclose($file2);

        return true;
    }

    return false;
}

【讨论】:

  • 只检查几个随机字节并没有比仅仅相信 SHA-1 总和更好的结果。否则,如果您只想随意检查文件内容,代码看起来不错。
  • 上述代码中早期返回码路径缺少对fclose()的调用。
【解决方案4】:

使用 Sha1 哈希,就像你一样。如果它们相等,则还要比较它们的 md5 哈希值和文件大小。 如果您然后遇到在所有 3 个检查中都匹配但不相等的文件 - 您刚刚找到了圣杯:D

【讨论】:

  • 我已经做了一个 SHA-1 以避免比较永久存储中的所有文件。做另一个散列会让我无处可去,因为 SHA-1 已经是非常好的散列,获得明显更好结果的唯一方法是比较实际字节。做任何其他哈希需要从存储中重新读取整个文件,此时比较字节更有意义,因为如果我发现差异,我可以停在文件中间,就像我使用另一个哈希函数。
【解决方案5】:

所以我遇到了这个问题,然后找到了一个可以回答它并且确实有效的问题。

2021...事情发生了变化,所以我想我会发布一个指向该答案的链接Here

A) 基本上它使用fopen 和fread,如上所示,但它可以工作。接受的答案总是对我返回不同的结果,即使在同一个文件上也是如此。

B) fopen 和 fread 方法将比 sha1 或 md5 方法更快,如果你可以使用它,我不明白你为什么不能。

上面链接中的 Svish 版本....

function files_are_equal($a, $b)
{
  // Check if filesize is different
  if(filesize($a) !== filesize($b))
      return false;

  // Check if content is different
  $ah = fopen($a, 'rb');
  $bh = fopen($b, 'rb');

  $result = true;
  while(!feof($ah))
  {
    if(fread($ah, 8192) != fread($bh, 8192))
    {
      $result = false;
      break;
    }
  }

  fclose($ah);
  fclose($bh);

  return $result;
}

【讨论】:

    【解决方案6】:

    您可以使用 turbodepot 库。它是纯 PHP 的,只需一行代码就可以解决这个问题:

    require 'path/to/your/dependencies/folder/turbocommons-php-X.X.X.phar';
    require 'path/to/your/dependencies/folder/turbodepot-php-X.X.X.phar';
    
    use org\turbodepot\src\main\php\managers\FilesManager;
    
    $filesManager = new FilesManager();
    $filesManager->isFileEqualTo('path/to/file1', 'path/to/file2');
    

    你可以看这里的代码,它基本上是先按大小比较,然后按数据块比较:

    https://github.com/edertone/TurboDepot/blob/f74a12ac330ec49604403a2f60502ced591c6da8/TurboDepot-Php/src/main/php/managers/FilesManager.php#L129

    通过使用这个库,您还可以获得大量文件系统功能,例如比较两个文件夹、搜索文件夹、镜像文件夹等等

    更多信息在这里:

    https://turboframework.org/en/blog/2020-11-03/check-if-two-files-are-identical-using-javascript-typescript-php

    【讨论】:

    • -1 这是一个不正确的答案,并且接受的答案已经提到了这个替代方案(MD5 校验和)+ 提供了正确的答案。您链接到的库实际上不会比较文件内容,而是比较文件大小 + MD5 总和,但仍需要文件的完整 IO 负载,因此您在没有正确结果的情况下获得所有性能影响。请参阅此处的示例,使用 MD5 创建冲突是多么容易,您就会明白为什么这是个坏主意:stackoverflow.com/q/933497/334451
    • 完全正确!库代码已更新为使用正确的方法,所有单元测试通过与使用以前的哈希方法完全相同,所以这当然更好。已生成该库的新版本(7.0.2),并且此答案已更新,指向新代码。您现在可以重新考虑您的评论吗?
    • 很高兴看到该库已修复!我删除了反对票,但评论不能再修改(SO中的cmets只能修改5分钟)。
    • 太好了,非常感谢!
    【解决方案7】:

    以下代码帮助您检查文件是否相同。

    /***check equality of files*/
    
    $file1="pics/star.jpg";
    
    $file2="pics/dupe.jpg";
    
    if(sha1_file($file1)==sha1_file($file2))
    
    echo "Identical";
    
    else
    
    echo "Not Identical";
    

    【讨论】:

    • @Spooky: 好的。我贴的代码适合少字节的文件。
    • 问题已经说过“鉴于我已经知道两个文件的 SHA-1 哈希匹配”所以我知道如何计算 SHA-1 哈希(或“校验和”)是非常安全的假设)。我也知道文件可能不是相同的,尽管 SHA-1 哈希匹配(参见stackoverflow.com/questions/2479348/…)。
    猜你喜欢
    • 1970-01-01
    • 2011-01-28
    • 1970-01-01
    • 2020-01-02
    • 1970-01-01
    • 1970-01-01
    • 2014-08-23
    • 2014-06-11
    • 1970-01-01
    相关资源
    最近更新 更多