在 Yii 2.0 中,基于 FileHelper 创建多级目录时,报错:chmod(): Operation not permitted,进而导致每次只能成功创建一级 的分析与解决

1、上传选题素材时,服务器内部错误。如图1

图1

{
  "name": "Internal Server Error",
  "message": "服务器内部错误。",
  "code": 0,
  "status": 500
}

2、决定调整为开发模式 (dev),再次上传选题素材,以查看更为详细的错误信息,报错:chmod(): Operation not permitted,如图2

图2

{
  "name": "Exception",
  "message": "Failed to change permissions for directory \"/webtv/wangjie/pcs-api/tmp/2019/12/05\": chmod(): Operation not permitted",
  "code": 2,
  "type": "yii\\base\\Exception",
  "file": "/mcloud/www/pcs-api/vendor/yiisoft/yii2/helpers/BaseFileHelper.php",
  "line": 635,
  "stack-trace": [
    "#0 /mcloud/www/pcs-api/common/services/AssetService.php(77): yii\\helpers\\BaseFileHelper::createDirectory('/webtv/wangjie/...')",
    "#1 /mcloud/www/pcs-api/api/rests/asset/UploadAction.php(93): common\\services\\AssetService::uploadTempAssets(Array)",
    "#2 [internal function]: api\\rests\\asset\\UploadAction->run()",
    "#3 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Action.php(94): call_user_func_array(Array, Array)",
    "#4 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Controller.php(157): yii\\base\\Action->runWithParams(Array)",
    "#5 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Module.php(528): yii\\base\\Controller->runAction('upload', Array)",
    "#6 /mcloud/www/pcs-api/vendor/yiisoft/yii2/web/Application.php(103): yii\\base\\Module->runAction('v1/asset/upload', Array)",
    "#7 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Application.php(386): yii\\web\\Application->handleRequest(Object(yii\\web\\Request))",
    "#8 /mcloud/www/pcs-api/api/web/index.php(17): yii\\base\\Application->run()",
    "#9 {main}"
  ],
  "previous": {
    "name": "PHP Warning",
    "message": "chmod(): Operation not permitted",
    "code": 2,
    "type": "yii\\base\\ErrorException",
    "file": "/mcloud/www/pcs-api/vendor/yiisoft/yii2/helpers/BaseFileHelper.php",
    "line": 633,
    "stack-trace": [
      "#0 [internal function]: yii\\base\\ErrorHandler->handleError(2, 'chmod(): Operat...', '/mcloud/www/pcs...', 633, Array)",
      "#1 /mcloud/www/pcs-api/vendor/yiisoft/yii2/helpers/BaseFileHelper.php(633): chmod('/webtv/wangjie/...', 509)",
      "#2 /mcloud/www/pcs-api/common/services/AssetService.php(77): yii\\helpers\\BaseFileHelper::createDirectory('/webtv/wangjie/...')",
      "#3 /mcloud/www/pcs-api/api/rests/asset/UploadAction.php(93): common\\services\\AssetService::uploadTempAssets(Array)",
      "#4 [internal function]: api\\rests\\asset\\UploadAction->run()",
      "#5 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Action.php(94): call_user_func_array(Array, Array)",
      "#6 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Controller.php(157): yii\\base\\Action->runWithParams(Array)",
      "#7 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Module.php(528): yii\\base\\Controller->runAction('upload', Array)",
      "#8 /mcloud/www/pcs-api/vendor/yiisoft/yii2/web/Application.php(103): yii\\base\\Module->runAction('v1/asset/upload', Array)",
      "#9 /mcloud/www/pcs-api/vendor/yiisoft/yii2/base/Application.php(386): yii\\web\\Application->handleRequest(Object(yii\\web\\Request))",
      "#10 /mcloud/www/pcs-api/api/web/index.php(17): yii\\base\\Application->run()",
      "#11 {main}"
    ]
  }
}

3、查看挂载的目录,/webtv/wangjie/pcs-api/tmp/2019/12/05,连接上传了 2 次,第 1 次生成了目录:12,第 2 次生成了目录:05,如图3

图3

[root@d48dedd9fceb pcs-api]# ls
2019  tmp
[root@d48dedd9fceb pcs-api]# cd tmp
[root@d48dedd9fceb tmp]# ls
2019
[root@d48dedd9fceb tmp]# cd 2019
[root@d48dedd9fceb 2019]# ls
10  11  12
[root@d48dedd9fceb 2019]# cd 12
[root@d48dedd9fceb 12]# ls
05
[root@d48dedd9fceb 12]# cd 05
[root@d48dedd9fceb 05]# ls
[root@d48dedd9fceb 05]# pwd
/webtv/wangjie/pcs-api/tmp/2019/12/05
[root@d48dedd9fceb 05]# ls -l
total 0
[root@d48dedd9fceb 05]# cd ..
[root@d48dedd9fceb 12]# ls -l
total 0
drwxrwxrwx 2 root root 0 Dec  5 15:28 05
[root@d48dedd9fceb 12]#

4、分析原因,当前用户 为 nginx ,在开发环境中查看目录 的用户,而在报错的环境中,目录的用户为 root,且模式为 0777,当前用户指的是执行 PHP 的用户。很可能和通常的 shell 或者 FTP 用户不是同一个。在大多数系统下文件模式只能被文件所有者的用户改变。如图4

图4

[root@685b4b7fd197 /]# cd /webtv/wangjiedev/pcs-api/
[root@685b4b7fd197 pcs-api]# ls
2019  php-fpm1.conf  php-fpm.conf  tmp
[root@685b4b7fd197 pcs-api]# cd tmp
[root@685b4b7fd197 tmp]# ls -l
total 0
drwxrwxr-x 9 nginx nginx 101 Dec  2 16:26 2019

5、最终决定继续类:yii\helpers\FileHelper,覆写静态方法:createDirectory($path, $mode = 0775, $recursive = true),当 chmod() 改变文件模式失败后,返回 true,不抛出异常,继续后续的执行,新建类:\common\helpers\FileHelper.php

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/12/05
 * Time: 16:45
 */
namespace common\helpers;

/**
 * File system helper.
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */class FileHelper extends \yii\helpers\FileHelper
{
    /**
     * Creates a new directory.
     *
     * This method is similar to the PHP `mkdir()` function except that
     * it uses `chmod()` to set the permission of the created directory
     * in order to avoid the impact of the `umask` setting.
     *
     * @param string $path path of the directory to be created.
     * @param int $mode the permission to be set for the created directory.
     * @param bool $recursive whether to create parent directories if they do not exist.
     * @return bool whether the directory is created successfully
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */    public static function createDirectory($path, $mode = 0775, $recursive = true)
    {
        if (is_dir($path)) {
            return true;
        }
        $parentDir = dirname($path);
        // recurse if parent dir does not exist and we are not at the root of the file system.
        if ($recursive && !is_dir($parentDir) && $parentDir !== $path) {
            static::createDirectory($parentDir, $mode, true);
        }
        try {
            if (!mkdir($path, $mode)) {
                return false;
            }
        } catch (\Exception $e) {
            if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288
                throw new \yii\base\Exception("Failed to create directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
            }
        }
        try {
            return @chmod($path, $mode);
        } catch (\Exception $e) {
            throw new \yii\base\Exception("Failed to change permissions for directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
        }
    }
}

6、后续创建目录时,皆引用新创建的类:common\helpers\FileHelper,进入报错的环境,手动删除目录:/webtv/wangjie/pcs-api/tmp/2019/12,如图5

图5

7、再次上传选题素材,报错:”创建目录:/webtv/wangjie/pcs-api/tmp/2019/12/05,失败”,如图6

图6

{
  "name": "Internal Server Error",
  "message": "创建目录:/webtv/wangjie/pcs-api/tmp/2019/12/05,失败",
  "code": 202002,
  "status": 500,
  "type": "yii\\web\\ServerErrorHttpException"
}

8、查看挂载的目录,/webtv/wangjie/pcs-api/tmp/2019/12/05,已经第 1 次就创建成功,已经确定不能够一次性创建多个目录的问题已经得到解决,如图7

图7

9、分析原因在于,静态方法:createDirectory($path, $mode = 0775, $recursive = true),未返回 true,进而导致判断时,以为创建目录失败,抛出异常,因此,不论 chmod 结果如何,皆返回 true

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2019/12/05
 * Time: 16:45
 */
namespace common\helpers;

/**
 * File system helper.
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */class FileHelper extends \yii\helpers\FileHelper
{
    /**
     * Creates a new directory.
     *
     * This method is similar to the PHP `mkdir()` function except that
     * it uses `chmod()` to set the permission of the created directory
     * in order to avoid the impact of the `umask` setting.
     *
     * @param string $path path of the directory to be created.
     * @param int $mode the permission to be set for the created directory.
     * @param bool $recursive whether to create parent directories if they do not exist.
     * @return bool whether the directory is created successfully
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */    public static function createDirectory($path, $mode = 0775, $recursive = true)
    {
        if (is_dir($path)) {
            return true;
        }
        $parentDir = dirname($path);
        // recurse if parent dir does not exist and we are not at the root of the file system.
        if ($recursive && !is_dir($parentDir) && $parentDir !== $path) {
            static::createDirectory($parentDir, $mode, true);
        }
        try {
            if (!mkdir($path, $mode)) {
                return false;
            }
        } catch (\Exception $e) {
            if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288
                throw new \yii\base\Exception("Failed to create directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
            }
        }
        try {
            // return chmod($path, $mode);
            @chmod($path, $mode);
            return true;
        } catch (\Exception $e) {
            throw new \yii\base\Exception("Failed to change permissions for directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
        }
    }
}

10、手动删除目录:/webtv/wangjie/pcs-api/tmp/2019/12,再次上传选题素材,上传资源成功,符合预期,如图8

图8

11、查看挂载的目录,/webtv/wangjie/pcs-api/tmp/2019/12/05/1575537845.5862.1232301337.jpg 已经存在,符合预期,如图9

图9

永夜