ZipArchiveでZipファイルの生成に失敗する場合の対処法

肌年齢が若くなる!?【厳選された植物原料100%】納得の無添加 化粧水 

シットリ素肌の秘密は・・【究極の無添加化粧水】全成分は植物由来100%!

化粧品のモニターサイトに登録してみたら、「モニターの参加を宣言するブログ記事を書いたらこのコードを貼り付けてください!」ってのがあったのですが、よく読まずに最新記事に貼ったら、このPHPの内容のページが「化粧品のモニターに参加する宣言記事」指定されてしまった様でなんやかんや。
勝手が分からぬ(╹◡╹)

とりあえずこのままにするけれど、これは当たらないだろうなぁ…笑

→当たりましたびっくり

[レビュー] 無添加化粧水
すごくさっぱりしてるのにしっとりする無添加工房OKADA ローションのレビュー

問題

ZipArchiveを使用してzipファイルを作成し、ダウンロードさせるWebページを作成していたが、
アカウントによってダウンロードできていない人がいた

コード


    // Zipフォルダにコピーするファイル
    $files = array("./a.txt","./b.txt","./c.txt");

    // Zip作成用一時フォルダを作る
    $tmp = ".\\tmp";
    if(!file_exists($tmp))
        if (!mkdir($tmp, 0777, true)){
            echo "{$tmp}の作成に失敗しました\n";
            exit();
        }

    // zipフォルダを作る
    $zipfile = "{$tmp}\\zip1.zip");

    // すでにファイルがある場合削除
    if(file_exists($zipfile))
        unlink ($zipfile);

    // Zipを作成
    $zip = new ZipArchive;
    if ($zip->open($zipfile, ZipArchive::CREATE) === FALSE) {
        echo "zipの作成に失敗しました\n";
        exit();
    }

    // ファイルをコピーする
    foreach($files as $file){
        if($zip->addFile("{$file}",basename($file)) === FALSE){
            echo "{$file}のコピーに失敗しました\n";
            $zip->close();
            exit();
        }
    }

    $zip->close();

    // ダウンロードさせる
    header('Content-Type: application/force-download');
    header('Content-Length: '.filesize($zipfile));
    header('Content-disposition: attachment; filename="'.basename($zipfile).'"');

    // readfile()だと大容量のときに時間がかかりすぎるので分割する

    // out of memoryエラーが出る場合に出力バッファリングを無効
    while (ob_get_level() > 0) {
      ob_end_clean();
    }
    ob_start();

    // ファイル出力
    if ($file = fopen($zipfile, 'rb')) {
        while(!feof($file) and (connection_status() == 0)) {
            echo fread($file, '4096'); //指定したバイト数ずつ出力
            ob_flush();
        }
        ob_flush();
        fclose($file);
    }
    ob_end_clean();

    // 一時ファイルを削除する
    unlink ($zipfile);


期待する動作

  • ダウンロードボタンがクリックされたらzipの生成を開始
  • zipの生成が完了したらダウンロード

現象

  • PHPのエラーログには

    [09-Jan-2018 09:59:35 Asia/Seoul] PHP Warning: fopen(zip1.zip): failed to open stream: No such file or directory in 該当ページ.php on line * *

  • zipを作成する一時フォルダには、zip1.zip.a06572といった、zipになりきれなかったファイルが残っている
    ファイルサイズはあるため、ファイルのコピーは成功しているっぽい

    • zip->close()に失敗している?
      • ただし、zip->close()の戻り値を見てみても、trueを返してきている

原因

  • zipを生成する一時フォルダに、現象発生ユーザーの”変更”の権限がついていなかった
    • ファイルの作成、コピーは「書き込み」の権限でできるが、zip->close()は「変更」の権限が必要?
    • 権限がなくclose()に失敗している場合でもTrueが返ってきてしまうらしい

解決策

  • 該当ユーザーに、「書き込み」・「読み込み」だけでなく、「変更」 の権限も付ける

  • ついでにzip->close()にもエラー処理を追加

<?php
    // Zipフォルダにコピーするファイル
    $files = array("./a.txt","./b.txt","./c.txt");

    // Zip作成用一時フォルダを作る
    $tmp = ".\\tmp";
    if(!file_exists($tmp))
        if (!mkdir($tmp, 0777, true)){
            echo "{$tmp}の作成に失敗しました\n";
            exit();
        }

    try {
        // zipフォルダを作る
        $zipfile = "{$tmp}\\zip1.zip");

        if(!file_exists($zipfile)){
            $zip = new ZipArchive;
            $res = $zip->open($zipfile, ZipArchive::CREATE);
            if ($res != TRUE) {
                throw new Exception("open FALSE code:{$res}");
            }

            // ファイルをコピーする
            foreach($files as $file){
                if($zip->addFile("{$file}",basename($file)) === FALSE){
                    if(!$zip->close())
                        throw new Exception("addFile FALSE AND close FALSE");
                    else
                        throw new Exception("addFile FALSE");
                }
            }

            if(!$zip->close()){
                throw new Exception("close False");
            }
        }
    } catch (Exception $e) {
        echo "zipの作成に失敗しました\n";
        error_log(date("Y/m/d H:i:s")."\t".$e->getFile().":".$e->getLine()." ".$e->getMessage() ."(".$e->getCode().")[".get_class($e)."]\t{$zipfile}\r\n", 3, "error.log");
        exit();
    }

    if(!file_exists($zipfile)){
        echo "zipが存在しません\n";
        error_log(date("Y/m/d H:i:s")."\t{$zipfile}が存在しません\r\n", 3, "error.log");
        exit();
    }

    // ダウンロードさせる
    header('Content-Type: application/force-download');
    header('Content-Length: '.filesize($zipfile));
    header('Content-disposition: attachment; filename="'.basename($zipfile).'"');

    // readfile()だと大容量のときに時間がかかりすぎるので分割する

    // out of memoryエラーが出る場合に出力バッファリングを無効
    while (ob_get_level() > 0) {
      ob_end_clean();
    }
    ob_start();

    // ファイル出力
    if ($file = fopen($zipfile, 'rb')) {
        while(!feof($file) and (connection_status() == 0)) {
            echo fread($file, '4096'); //指定したバイト数ずつ出力
            ob_flush();
        }
        ob_flush();
        fclose($file);
    }
    ob_end_clean();

?>

ZipArchive自体はエラーになっていないので、上記で正しくエラーログが出力できるかはわかりません(´⊙ω⊙`)