[PHP] 다중 파일을 zip으로 묶어받기[PHP] 다중 파일을 zip으로 묶어받기

Posted at 2014/03/16 15:29 | Posted in 프로그래밍

php로 여러 개의 파일을 묶는 방법은 여러가지가 있다. exec()으로 압축 프로그램을 실행시켜서 묶거나 , ZipArchive로 묶거나, 직접 묶거나. 이 글은 다 묶어질 때까지 기다릴 필요가 없는 직접 묶는 방법으로 코딩한 소스를 공유하고자 작성됐다!

소스

PKZip의 구조를 정리해놓은 문서PHP ZipArchive 클래스를 참고해서 직접 코딩했다. 이 소스는 zip 관리용이 아니라 묶어받기용이다.

<?php
class DirectZip
{
    private $currentOffset;
    private $entries;

    public function open($filename)
    {
        set_time_limit(0);
        ini_set('zlib.output_compression', 'Off');
        header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
        header('Pragma: no-cache');
        header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="'.$filename.'"');
        $this->currentOffset = 0;
        $this->entries = array();
    }

    public function addEmptyDir($dirname)
    {
        if ($this->addFile('php://temp', $dirname.'/') === false)
        {
            return false;
        }
    }

    public function addFromString($localname, $contents)
    {
        $tmp = tempnam(sys_get_temp_dir(), __CLASS__);

        $pointer = @fopen($tmp, 'wb');
        if ($pointer === false)
        {
            @unlink($tmp);
            return false;
        }

        fwrite($pointer, $contents);
        $result = $this->addFile($tmp, $localname);

        fclose($pointer);
        @unlink($tmp);

        if ($result === false)
        {
            return false;
        }
    }

    private function linkEntry(&$entry)
    {
        $this->entries[] = $entry;
    }

    public function addFile($filename, $localname = null)
    {
        $entry = new DirectZipEntry(empty($localname) ? basename($filename) : $localname, $this->currentOffset);
        if ($entry->open($filename) === false)
        {
            return false;
        }

        $this->linkEntry($entry);

        ob_start();

        self::printBinary(0x04034B50, 4); // sig entry
        self::printEntryStat($entry);
        echo $entry->name;

        $buffer = ob_get_clean();
        echo $buffer;
        flush();
        $this->currentOffset += strlen($buffer);

        while (!feof($entry->pointer))
        {
            $buffer = @fread($entry->pointer, 1024 * 1024 * 4); // 버퍼 크기는 적절하게 고쳐서 쓰세요. 현재 크기: 4MB
            echo $buffer;
            flush();
            $this->currentOffset += strlen($buffer);
        }

        $entry->close();
    }

    public function close()
    {
        ob_start();

        foreach ($this->entries as $entry)
        {
            self::printBinary(0x02014B50, 4); // sig index
            self::printBinary(0); // os
            self::printEntryStat($entry);
            self::printBinary(0, 10); // file comment len, disk # start, internal attr, external attr(4)
            self::printBinary($entry->offset, 4);
            echo $entry->name;
        }

        $buffer = ob_get_clean();
        echo $buffer;
        flush();

        self::printBinary(0x06054B50, 4); // sig end
        self::printBinary(0, 4); // disk number, disk # index start
        self::printBinary(count($this->entries), 2, 2);
        self::printBinary(strlen($buffer), 4);
        self::printBinary($this->currentOffset, 4);
        self::printBinary(0); // comment len
        flush();
    }

    private static function printEntryStat($entry)
    {
        self::printBinary(substr($entry->name, -1) == '/' ? 20 : 10);
        self::printBinary(2048, 4); // flags, compression
        self::printBinary($entry->mtime);
        self::printBinary($entry->mdate);
        self::printBinary($entry->crc, 4);
        self::printBinary($entry->size, 4, 2);
        self::printBinary(strlen($entry->name));
        self::printBinary(0); // extra field len
    }

    private static function printBinary($binary, $length = 2, $repeat = 1)
    {
        for ($i = 0; $i < $repeat; $i++)
        for ($j = 0; $j < $length; $j++)
        {
            self::printByte($binary >> $j * 8);
        }
    }

    private static function printByte()
    {
        foreach (func_get_args() as $arg)
        {
            echo pack('C', $arg);
        }
    }
}

class DirectZipEntry
{
    public $offset;
    public $pointer;

    public $name;
    public $crc;
    public $size;
    public $mtime;
    public $mdate;

    public function __construct($name, $offset)
    {
        $this->offset = $offset;
        $this->name = $name;
    }

    public function open($filename)
    {
        $this->pointer = @fopen($filename, 'rb');
        if ($this->pointer === false)
        {
            return false;
        }

        $this->crc = unpack('N', hash_file('crc32b', $filename, true))[1];

        $fstat = fstat($this->pointer);
        $this->size = $fstat['size'];

        $mtime = $filename == 'php://temp' ? time() : $fstat['mtime'];
        $this->mtime = date('s', $mtime) >> 1 | date('i', $mtime) << 5 | date('H', $mtime) << 11;
        $this->mdate = date('d', $mtime) | date('m', $mtime) << 5 | (date('Y', $mtime) - 1980) << 9;
    }

    public function close()
    {
        fclose($this->pointer);
    }
}
?>

사용법

$zip = new DirectZip;
$zip->open('브라우저로 보낼 압축파일 이름.zip');
$zip->addFile('/tmp/추가할 파일.jpg', '압축파일 내 파일 이름.jpg');
$zip->addEmptyDir('압축파일 내 폴더 이름'); // 안 해도 상관없음.
$zip->addFile('/tmp/추가할 파일2.png', '압축파일 내 폴더 이름/압축파일 내 파일 이름.png');
$zip->addFile('/tmp/추가할 파일3.jpg'); // 압축파일에 파일을 '추가할 파일3.jpg'로 추가
$zip->addFromString('바로 글쓰기.txt', '파일 내용'); // 압축파일에 '파일 내용'을 '바로 글쓰기.txt'로 추가
$zip->close();

php 5.4 이상의 환경에서 위와 같이 코딩하면 한 주소에서 파일 여러 개를 묶어서 내려받을 수 있다.

단점

다 묶일 때까지 기다리거나 임시파일 만들 필요가 없는 이 소스에도 단점이 있다. 파일 이어받기가 안 되고 언제 다 받을지 모른다는 것인데, 다 받아질 때까지 느긋하게 기다리라고 클라이언트에 알려두면 좋을 것 같다.

최대한 간단하게 코딩해서 그런지 파일의 만든 날짜, 엑세스한 날짜 등이 지워진다는 것을 단점이라고 해야 되나 모르겠다. 난 좋은데.

저작자 표시 비영리

http://blog.bloodcat.com/trackback/277 관련글 쓰기

  1. 질문
    4장의 jpg 파일을 zip으로 묶어서 다운받는 기능을 구현중입니다.
    인터넷 검색하다가 이 글을 발견하고 올레를 외쳤는데...php5.4 이상의 환경이라니...
    저는 php5.3 이거든요 T.T
    위 소스로 zip파일 생성도 되고, 다운도 되고, 다운 받은 걸 압축해제도 됩니다.
    근데, 압축 해제 시에 '압축파일이 손상되었습니다.(CRC에러)' 메세지가 떠요.
    혹시 관련해서 해결 방법을 아시면 조언 좀 부탁드립니다 T.T
  2. 질문
    우와우와-정말정말 감사합니다!!!!
    복 받으실 거예요 T.T
  3. 질문
    죄송한데요...질문 하나만 더 드릴게요...
    전 반디집을 사용해서 zip파일 해제에 문제가 없었는데요.
    윈도우의 기본 zip파일 해제도 문제 없습니다.
    그런데, 알집을 사용하는 컴에서는 zip파일 해제 시에 '압축파일의 헤더가 손상되었습니다'란 오류메세지가 떠요.
    이 부분 관련해서는 해결 방법이 없을까요?
    조언 부탁드리겠습니다...
    • 2014/04/12 20:49 [Edit/Del]
      압축파일을 메모장으로 열어보고 php 에러 메세지가 있는지 확인하고 알아서 해결해주세요.
      기본설정으로 했을 때 timezone관련 에러가 떴던 걸로...

      해결하기 귀찮으면 error_reporting(0);을 파일 제일 처음에 넣는 방법도 있습니다.

Name __

Password __

Link (Your Website)

Comment

1 2 3 4 5 ... 168