PHP x86如何获取大于2GB的文件大小而不使用外部程序?

26

我需要获取一个大小超过2GB的文件的文件大小(测试对象是4.6GB的文件)。是否有不使用外部程序的方法来实现这一点?

当前状态:

  • filesize()stat()fseek() 失败
  • fread()feof() 可以正常工作

可以通过读取文件内容的方式来获取文件大小(速度极慢),但存在这种可能性。

$size = (float) 0;
$chunksize = 1024 * 1024;
while (!feof($fp)) {
    fread($fp, $chunksize);
    $size += (float) $chunksize;
}
return $size;

我知道如何在64位平台上获取它(使用fseek($fp,0,SEEK_END)ftell()),但我需要32位平台的解决方案。


解决方案:我已经开始了一个开源项目来解决这个问题。

大文件工具

大文件工具是一组必要的黑科技,用于在PHP中处理超过2 GB的文件(即使在32位系统上也可以)。


7
如果您无法在x86的C代码中解决此问题,那么从PHP内部解决它几乎是不可能的。这是一种系统性的限制,您无法通过自己的约束来克服它。 - mario
2
浮点数在某些时候会变得不太精确。不知道在哪个php x86版本上。如果手动管理结果的上下24位将会更好。if($size>=0x1000000) { $upper+=1; $size-=0x1000000 }。您的文件读取方法肯定是有效的,但不实用。遗憾的是,PHP的fseek(SEEK_CUR)接口不返回跳过的数量,否则这将更容易。 - mario
2
浮点类型具有固有的精度损失。毫无疑问,如果您想知道为什么,请查阅关于计算和数字存储的良好参考资料。disk_free_space()在处理大数字时确实存在偏差错误,但由于其本质,它不可能100%准确。个别文件系统实现、群集大小等可能会影响实际可用空间。因此,disk_free_space()受到不可避免的浮点偏差影响,但在那个级别上并不需要精确。文件大小是精确数字,没有误差容忍度。搞砸文件大小,您将丢失数据。 - Unsigned
好的,现在我明白了你的问题所在!非常感谢,下午我会发布更新。 - Honza Kuchař
嗨,我指的是外部可执行文件。外部可执行文件通常会使事情变得复杂。它会使开发变得复杂(不再是多平台),使工作流程变得复杂(需要在开发机器上进行设置)和部署(需要正确设置right、open_basedir等)。因此,启动一个PHP项目,允许您在大多数系统上获得精确的文件大小而无需外部依赖项,我认为这是足够好的解决方案。没有理由将当前行为报告为错误,因为它是32位整数(x86平台)的预期行为。请参阅项目维基了解更多信息:https://github.com/jkuchar/BigFileTools - Honza Kuchař
显示剩余3条评论
13个回答

23

以下是一种可能的方法:

首先尝试使用适用于平台的Shell命令(Windows Shell替代符或*nix / Mac的 stat 命令)。如果失败,则尝试使用COM(如果在Windows上),最后回退到filesize()

/*
 * This software may be modified and distributed under the terms
 * of the MIT license.
 */

function filesize64($file)
{
    static $iswin;
    if (!isset($iswin)) {
        $iswin = (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN');
    }

    static $exec_works;
    if (!isset($exec_works)) {
        $exec_works = (function_exists('exec') && !ini_get('safe_mode') && @exec('echo EXEC') == 'EXEC');
    }

    // try a shell command
    if ($exec_works) {
        $cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : "stat -c%s \"$file\"";
        @exec($cmd, $output);
        if (is_array($output) && ctype_digit($size = trim(implode("\n", $output)))) {
            return $size;
        }
    }

    // try the Windows COM interface
    if ($iswin && class_exists("COM")) {
        try {
            $fsobj = new COM('Scripting.FileSystemObject');
            $f = $fsobj->GetFile( realpath($file) );
            $size = $f->Size;
        } catch (Exception $e) {
            $size = null;
        }
        if (ctype_digit($size)) {
            return $size;
        }
    }

    // if all else fails
    return filesize($file);
}

哇,这正是我正在寻找的。 ;) 我会做一些小改进,然后我会在这里发布它。(支持> 2GB文件而无需exec和COM支持) - Honza Kuchař
如何将更多的答案标记为解决方案? - Honza Kuchař
1
您只能选择一个答案。我很好奇,您是否对此答案有问题,以至于不断寻找其他替代方案? - Unsigned
2
不,这太棒了!但它只是解决方案的一部分,因为问题是如何在没有外部程序的情况下获取文件大小...(在所有 Web 服务器上都不允许使用 exec)。 - Honza Kuchař
如果可以打勾,我只会选择一个答案,因为它是最佳答案(但对于其他问题而言;) - Honza Kuchař
显示剩余4条评论

8
我已经开始了一个名为 Big File Tools 的项目。它已经证明可以在Linux、Mac和Windows上运行(包括32位变体)。即使对于巨大的文件(>4GB),它也提供字节精确的结果。它在内部使用 brick/math - 任意精度算术库。
使用 composer 安装它。
composer install jkuchar/BigFileTools

并使用它:

<?php
$file = BigFileTools\BigFileTools::createDefault()->getFile(__FILE__);
echo $file->getSize() . " bytes\n";

结果是 BigInteger,因此您可以使用结果进行计算。

$sizeInBytes = $file->getSize();
$sizeInMegabytes = $sizeInBytes->toBigDecimal()->dividedBy(1024*1024, 2, \Brick\Math\RoundingMode::HALF_DOWN);    
echo "Size is $sizeInMegabytes megabytes\n";

Big File Tools内部使用驱动程序来可靠地确定所有平台上的确切文件大小。以下是可用驱动程序列表(更新于2016-02-05)

| Driver           | Time (s) ↓          | Runtime requirements | Platform 
| ---------------  | ------------------- | --------------       | ---------
| CurlDriver       | 0.00045299530029297 | CURL extension       | -
| NativeSeekDriver | 0.00052094459533691 | -                    | -
| ComDriver        | 0.0031449794769287  | COM+.NET extension   | Windows only
| ExecDriver       | 0.042937040328979   | exec() enabled       | Windows, Linux, OS X
| NativeRead       | 2.7670161724091     | -                    | -

您可以使用BigFileTools与任何一个,或者默认选择最快的(BigFileTools::createDefault())

 use BigFileTools\BigFileTools;
 use BigFileTools\Driver;
 $bigFileTools = new BigFileTools(new Driver\CurlDriver());

恭喜!太棒了这个项目!我报告一个问题:https://github.com/jkuchar/BigFileTools/issues/26,关于PHP 7.3和7.4,在旧版本中工作正常。 - Guilherme Nascimento

4
<?php
  ######################################################################
  # Human size for files smaller or bigger than 2 GB on 32 bit Systems #
  # size.php - 1.1 - 17.01.2012 - Alessandro Marinuzzi - www.alecos.it #
  ######################################################################
  function showsize($file) {
    if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
      if (class_exists("COM")) {
        $fsobj = new COM('Scripting.FileSystemObject');
        $f = $fsobj->GetFile(realpath($file));
        $file = $f->Size;
      } else {
        $file = trim(exec("for %F in (\"" . $file . "\") do @echo %~zF"));
      }
    } elseif (PHP_OS == 'Darwin') {
      $file = trim(shell_exec("stat -f %z " . escapeshellarg($file)));
    } elseif ((PHP_OS == 'Linux') || (PHP_OS == 'FreeBSD') || (PHP_OS == 'Unix') || (PHP_OS == 'SunOS')) {
      $file = trim(shell_exec("stat -c%s " . escapeshellarg($file)));
    } else {
      $file = filesize($file);
    }
    if ($file < 1024) {
      echo $file . ' Byte';
    } elseif ($file < 1048576) {
      echo round($file / 1024, 2) . ' KB';
    } elseif ($file < 1073741824) {
      echo round($file / 1048576, 2) . ' MB';
    } elseif ($file < 1099511627776) {
      echo round($file / 1073741824, 2) . ' GB';
    } elseif ($file < 1125899906842624) {
      echo round($file / 1099511627776, 2) . ' TB';
    } elseif ($file < 1152921504606846976) {
      echo round($file / 1125899906842624, 2) . ' PB';
    } elseif ($file < 1180591620717411303424) {
      echo round($file / 1152921504606846976, 2) . ' EB';
    } elseif ($file < 1208925819614629174706176) {
      echo round($file / 1180591620717411303424, 2) . ' ZB';
    } else {
      echo round($file / 1208925819614629174706176, 2) . ' YB';
    }
  }
?>

使用方法如下:

<?php include("php/size.php"); ?>

你可以选择放置在哪里:

<?php showsize("files/VeryBigFile.rar"); ?>

如果您想改进它,欢迎您!


1
你绝对不应该使用那个特定于操作系统的脚本,而且没有使用管道stdout/in/err是一个非常糟糕的做法。 - user257319
对于 %F in ("" . $file . "") do @echo %~zF这段代码是什么作用? - EricP

4
$file_size=sprintf("%u",filesize($working_dir."\\".$file));

这对我在Windows电脑上有效。
我在这里查看了错误日志:https://bugs.php.net/bug.php?id=63618,并找到了这个解决方案。

1
这是错误的,因为它仍然使用32位整数。所以只有在32位系统上达到4 GB时,它才会打印正确的值。 - Honza Kuchař

2

我找到了一个适用于Linux/Unix的简洁解决方案,可以使用32位php获取大文件的文件大小。

$file = "/path/to/my/file.tar.gz";
$filesize = exec("stat -c %s ".$file);

你应该将$filesize处理为字符串。尝试将其强制转换为整数会导致如果文件大小大于PHP_INT_MAX,则filesize = PHP_INT_MAX。

但是,即使将其处理为字符串,以下人类可读的算法也可以正常工作:

formatBytes($filesize);

public function formatBytes($size, $precision = 2) {
    $base = log($size) / log(1024);
    $suffixes = array('', 'k', 'M', 'G', 'T');
    return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
}

当我处理大于4GB的文件时,我的输出如下:

4.46G

此解决方案使用exec(),但并非始终允许使用。 - Honza Kuchař
这样做会不会在文件名中含有空格的情况下出错?你正在使用args创建一个平面字符串,只用空格分隔文件名。stat本身是一个很好的实用程序,但它是GNU coreutils的一部分,甚至没有被POSIX指定。 - Peter Cordes

1

最简单的方法是给你的数字添加一个最大值。这意味着在x86平台上,长数字加2^32:

if($size < 0) $size = pow(2,32) + $size;

例子:Big_File.exe - 3.30Gb(3,554,287,616 b),您的函数返回-740679680,因此您需要添加2 ^ 32(4294967296),然后得到3554287616。

您得到负数是因为系统保留了一位内存作为负号,因此您只剩下2 ^ 31(2,147,483,648 = 2G)最大值的正数或负数。当系统达到这个最大值时,它不会停止,而是简单地覆盖了最后一个保留位,使您的数字现在强制为负数。简单地说,当您超过最大正数时,您将被迫进入最大负数,因此2147483648 + 1 = -2147483648。进一步的加法向零和再次向最大数字进行。

如您所见,它就像一个循环,最高和最低的数字闭合循环。

x86架构可以在一个节拍中“消化”的最大总数是2 ^ 32 = 4294967296 = 4G,因此只要您的数字低于该数字,这个简单的技巧就会始终起作用。在更高的数字中,您必须知道自己已经通过了循环点多少次,然后将其乘以2 ^ 32并将其添加到结果中:

$size = pow(2,32) * $loops_count + $size;

当然,在基础的PHP函数中,这很难做到,因为没有任何函数会告诉你它已经通过循环点多少次,所以对于超过4G的文件,这种方法行不通。

这是一个相当理论的答案。正如你所说,这仅适用于小于4G的文件。看看BigFileTools的实现。有一些使用大数字的技巧被实现了。(并且它们适用于任何文件大小) - Honza Kuchař

0

一个选项是寻找到2GB标记,然后从那里读取长度...

function getTrueFileSize($filename) {
    $size = filesize($filename);
    if ($size === false) {
        $fp = fopen($filename, 'r');
        if (!$fp) {
            return false;
        }
        $offset = PHP_INT_MAX - 1;
        $size = (float) $offset;
        if (!fseek($fp, $offset)) {
            return false;
        }
        $chunksize = 8192;
        while (!feof($fp)) {
            $size += strlen(fread($fp, $chunksize));
        }
    } elseif ($size < 0) {
        // Handle overflowed integer...
        $size = sprintf("%u", $size);
    }
    return $size;
}

基本上,这个程序旨在寻找PHP中可表示的最大正有符号整数(32位系统为2GB),然后使用8KB块从那时开始读取(这应该是最佳内存效率与磁盘传输效率之间的公平权衡)。

还要注意,我没有将$chunksize添加到大小中。原因是fread可能会根据许多可能性返回更多或更少的字节。因此,使用strlen来确定解析字符串的长度。


我认为是的,这看起来像一个解决方案。待修复的小错误:在Windows上,文件大小返回了溢出的文件大小。因此,我们必须使用fseek($fp,0,SEEK_END)=== -1而不是$size === false - Honza Kuchař
1
@Honza:其实不是这样的,因为 === false 仍然与溢出不同。所以要修复溢出,只需执行 return sprintf('%u', $size) 强制转换为有符号数... - ircmaxell
我认为你说的有一定道理,但只是一点点。因为当我在一个4.6GB的文件上调用filesize时,它返回给我int(41385984)。所以真正的解决方案只有fseek... - Honza Kuchař
真的在Windows上不起作用。因为它溢出了两次。 ;) 对于4.6GB的文件,它返回39MB。 ;) - Honza Kuchař
这段代码有一个错误。如果文件小于4GB,你会得到无意义的结果。 - Honza Kuchař
filesize()不可靠,它返回的是无意义的值(而非false)。 - Honza Kuchař

0
如果您有FTP服务器,您可以使用fsockopen:
$socket = fsockopen($hostName, 21);
$t = fgets($socket, 128);
fwrite($socket, "USER $myLogin\r\n");
$t = fgets($socket, 128);
fwrite($socket, "PASS $myPass\r\n");
$t = fgets($socket, 128);
fwrite($socket, "SIZE $fileName\r\n");
$t = fgets($socket, 128);
$fileSize=floatval(str_replace("213 ","",$t));
echo $fileSize;
fwrite($socket, "QUIT\r\n");
fclose($socket); 

(在ftp_size页面的评论中发现)

谢谢你的解决方案。是的,这也是一种方法,但它并不通用。我需要代码的可重用性。因为它被用作 Nette 框架的插件。 - Honza Kuchař
如果系统不允许使用“exec”,那么我会使用True作为备选方案。 - dave1010
好的,如何将文件路径转换为FTP URL? ;) - Honza Kuchař
取决于您的服务器。在许多情况下,$hostName将是$_SERVER['HTTP_HOST']。$fileName可能会有所不同,这取决于FTP根目录。WordPress可以使用FTP服务器进行更新。 - dave1010

0

不能通过检查filesize()是否为负数来可靠地获取32位系统上文件的大小,正如一些答案所建议的那样。这是因为如果一个文件在32位系统上介于4到6GB之间,filesize将报告一个正数,然后从6到8变为负数,然后从8到10再次变为正数,以此类推。它循环,可以这么说。

因此,您需要使用在32位系统上可靠工作的外部命令。

但是,一个非常有用的工具是能够检查文件大小是否大于某个特定大小,即使是非常大的文件也可以可靠地进行检查。

以下代码尝试读取一个字节并寻找50兆字节。在我的低配测试机上非常快,并且即使大小远大于2GB,也可以可靠地工作。

您可以使用此方法检查文件是否大于2147483647字节(2147483648是32位系统上的最大整数),然后对文件进行不同处理或让您的应用程序发出警告。

function isTooBig($file){
        $fh = @fopen($file, 'r');
        if(! $fh){ return false; }
        $offset = 50 * 1024 * 1024; //50 megs
        $tooBig = false;
        if(fseek($fh, $offset, SEEK_SET) === 0){
                if(strlen(fread($fh, 1)) === 1){
                        $tooBig = true;
                }
        } //Otherwise we couldn't seek there so it must be smaller

        fclose($fh);
        return $tooBig;
}

是的,这是我的解决方案的一部分... https://dev59.com/jm035IYBdhLWcg3wTuJu#5505610 - Honza Kuchař

0
以下代码在任何 PHP / 操作系统 / Web 服务器 / 平台的任何文件大小下都能正常工作。
// http head request to local file to get file size
$opts = array('http'=>array('method'=>'HEAD'));
$context = stream_context_create($opts);

// change the URL below to the URL of your file. DO NOT change it to a file path.
// you MUST use a http:// URL for your file for a http request to work
// SECURITY - you must add a .htaccess rule which denies all requests for this database file except those coming from local ip 127.0.0.1.
// $tmp will contain 0 bytes, since its a HEAD request only, so no data actually downloaded, we only want file size
$tmp= file_get_contents('http://127.0.0.1/pages-articles.xml.bz2', false, $context);

$tmp=$http_response_header;
foreach($tmp as $rcd) if( stripos(trim($rcd),"Content-Length:")===0 )  $size= floatval(trim(str_ireplace("Content-Length:","",$rcd)));
echo "File size = $size bytes";

// example output
File size = 10082006833 bytes

好的,这是一种解决方案,但我需要接受文件路径并返回文件大小的东西。因为这个解决方案使用了.htaccess,所以它依赖于Web服务器,当移动到IIS时,会创建安全问题。我找到了一个使用curl的黑客技巧,可以在本地文件上执行相同的操作(因此没有http请求和不需要设置环境-URL转换;这是我的解决方案的一部分)。 - Honza Kuchař

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接