34 Star 155 Fork 42

Anyon / ThinkLibrary

 / 详情

支持FTP服务器上传

待办的
创建于  
2020-11-05 14:14

输入图片说明
app/admin/view/config/index.html

<div class="think-box-shadow margin-bottom-15">
    <span class="color-green font-w7 text-middle">文件存储方式:</span>
    {foreach ['local'=>'本地服务器存储','ftp'=>'FTP服务器存储','qiniu'=>'七牛云对象存储','alioss'=>'阿里云OSS存储'] as $k => $v}
    {if sysconf('storage.type') eq $k}
    {if auth('storage')}<a data-title="配置{$v}" data-modal="{:url('storage')}?type={$k}" class="layui-btn layui-btn-sm">{$v}</a>{else}<a class="layui-btn layui-btn-sm">{$v}</a>{/if}
    {else}
    {if auth('storage')}<a data-title="配置{$v}" data-modal="{:url('storage')}?type={$k}" class="layui-btn layui-btn-sm layui-btn-primary">{$v}</a>{else}<a class="layui-btn layui-btn-sm">{$v}</a>{/if}
    {/if}
    {/foreach}
</div>

app/admin/view/config/storage-ftp.html

<form onsubmit="return false" data-auto="true" action="{:url()}" method="post" class='layui-form layui-card' autocomplete="off">
    <div class="layui-card-body">

        <div class="color-text margin-left-40 margin-bottom-20 layui-code text-center layui-bg-gray" style="border-left-width:1px">
            <p class="margin-bottom-5 font-w7">文件将上传到公司FTP文件服务器存储</p>
        </div>

        {include file='config/storage-0'}

        <div class="layui-form-item">
            <label class="layui-form-label label-required">
                <span class="color-green font-w7">访问协议</span><br><span class="nowrap color-desc">Protocol</span>
            </label>
            <div class="layui-input-block">
                {foreach ['http','https','auto'] as $pro}
                <label class="think-radio">
                    {if sysconf('storage.ftp_http_protocol') eq $pro}
                    <input checked type="radio" name="storage.ftp_http_protocol" value="{$pro}" lay-ignore> {$pro}
                    {else}
                    <input type="radio" name="storage.ftp_http_protocol" value="{$pro}" lay-ignore> {$pro}
                    {/if}
                </label>
                {/foreach}
                <p class="help-block">FTP服务器访问协议,其中 https 需要配置证书才能使用(auto 为相对协议)</p>
            </div>
        </div>
    
        <div class="layui-form-item">
            <label class="layui-form-label" for="storage.ftp_host">
                <span class="color-green font-w7">服务器</span><br><span class="nowrap color-desc">Host</span>
            </label>
            <div class="layui-input-block">
                <input id="storage.ftp_host" type="text" name="storage.ftp_host" required value="{:sysconf('storage.ftp_host')}" placeholder="请输入服务器IP地址" class="layui-input">
                <p class="help-block"></p>
            </div>
        </div>

        <div class="layui-form-item">
            <label class="layui-form-label" for="storage.ftp_port">
                <span class="color-green font-w7">端口号</span><br><span class="nowrap color-desc">Port</span>
            </label>
            <div class="layui-input-block">
                <input id="storage.ftp_port" type="text" name="storage.ftp_port" required value="{:sysconf('storage.ftp_port')}" placeholder="请输入服务器端口号" class="layui-input">
                <p class="help-block">填写服务器端口号,如:21</p>
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label" for="storage.ftp_bucket">
                <span class="color-green font-w7">空间目录</span><br><span class="nowrap color-desc">Bucket</span>
            </label>
            <div class="layui-input-block">
                <input id="storage.ftp_bucket" type="text" name="storage.ftp_bucket" required value="{:sysconf('storage.ftp_bucket')}" placeholder="请输入FTP服务器授权存储空间名" class="layui-input">
                <p class="help-block">请输入FTP服务器授权存储空间名,如:img</p>
            </div>
        </div>

        <div class="layui-form-item">
            <label class="layui-form-label" for="storage.ftp_http_domain">
                <span class="color-green font-w7">访问域名</span><br><span class="nowrap color-desc">Domain</span>
            </label>
            <div class="layui-input-block">
                <input id="storage.ftp_http_domain" type="text" name="storage.ftp_http_domain" required value="{:sysconf('storage.ftp_http_domain')}" placeholder="请输入服务器 Domain (访问域名)" class="layui-input">
                <p class="help-block">填写服务器存储访问域名,如:0img.xxx.com,1img.xxx.com,2img.xxx.com,3img.xxx.com,4img.xxx.com</p>
            </div>
        </div>

        <div class="layui-form-item">
            <label class="layui-form-label" for="storage.ftp_user">
                <span class="color-green font-w7">访问账号</span><br><span class="nowrap color-desc">User</span>
            </label>
            <div class="layui-input-block">
                <input id="storage.ftp_user" type="text" name="storage.ftp_user" required value="{:sysconf('storage.ftp_user')}" placeholder="请输入FTP账号" class="layui-input">
                <p class="help-block"></p>
            </div>
        </div>

        <div class="layui-form-item">
            <label class="layui-form-label" for="storage.ftp_pass">
                <span class="color-green font-w7">访问密码</span><br><span class="nowrap color-desc">Password</span>
            </label>
            <div class="layui-input-block">
                <input id="storage.ftp_pass" type="text" name="storage.ftp_pass" required value="{:sysconf('storage.ftp_pass')}" maxlength="50" placeholder="请输入FTP账号密码" class="layui-input">
                <p class="help-block"></p>
            </div>
        </div>

        <div class="hr-line-dashed margin-left-40"></div>
        <input type="hidden" name="storage.type" value="ftp">

        <div class="layui-form-item text-center padding-left-40">
            <button class="layui-btn" type="submit">保存配置</button>
            <button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
        </div>
    </div>
</form>

app/admin/controller/api/Upload.php 修改如下方法

use think\admin\storage\FtpStorage;
   /**
     * 获取文件上传方式
     * @return string
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function getType()
    {
        $this->uptype = strtolower(input('uptype', ''));
        if (!in_array($this->uptype, ['local', 'ftp', 'qiniu', 'alioss'])) {
            $this->uptype = strtolower(sysconf('storage.type'));
        }
        return strtolower($this->uptype);
    }
/**
     * 检查文件上传已经上传
     * @login true
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function state()
    {
        $this->name = input('name', null);
        $this->safe = boolval(input('safe'));
        $data = ['uptype' => $this->getType(), 'xkey' => input('xkey')];
        $log = new Log($this->app);
        $action="上传文件-检查";
        $content = $this->name;
        $log->info("操作人:".$this->app->session->get('user.username', '').",行为:".$action.',操作内容:'.$content);
        if ($info = Storage::instance($data['uptype'])->info($data['xkey'], $this->safe, $this->name)) {
            $data['url'] = $info['url'];
            $this->success('文件已经上传', $data, 200);
        } elseif ('local' === $data['uptype']) {
            $data['url'] = LocalStorage::instance()->url($data['xkey'], $this->safe, $this->name);
            $data['server'] = LocalStorage::instance()->upload();
        }elseif ('ftp' === $data['uptype']) {
            $data['url'] = FtpStorage::instance()->url($data['xkey'], $this->safe, $this->name);
            $data['server'] = FtpStorage::instance()->upload();
        } elseif ('qiniu' === $data['uptype']) {
            $data['url'] = QiniuStorage::instance()->url($data['xkey'], $this->safe, $this->name);
            $data['token'] = QiniuStorage::instance()->buildUploadToken($data['xkey'], 3600, $this->name);
            $data['server'] = QiniuStorage::instance()->upload();
        } elseif ('alioss' === $data['uptype']) {
            $token = AliossStorage::instance()->buildUploadToken($data['xkey'], 3600, $this->name);
            $data['url'] = $token['siteurl'];
            $data['policy'] = $token['policy'];
            $data['signature'] = $token['signature'];
            $data['OSSAccessKeyId'] = $token['keyid'];
            $data['server'] = AliossStorage::instance()->upload();
        }
        $data['safe'] = intval($this->safe);
        $this->success('获取上传参数', $data, 404);
}

think-library/src/storage/FtpStoreage.php

<?php

// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2020 广州楚才信息科技有限公司 [ http://www.cuci.cc ]
// +----------------------------------------------------------------------
// | 官方网站: https://gitee.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// +----------------------------------------------------------------------
// | gitee 仓库地址 :https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 :https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------

namespace think\admin\storage;

use Exception;
use think\admin\Storage;
use think\admin\extend\HttpExtend;
use think\admin\extend\FtpExtend;
use think\facade\Log;

/**
 * FTP存储支持
 * Class FtpStorage
 * @package think\admin\storage
 */
class FtpStorage extends Storage
{
    private $host;
    private $port;
    private $user;
    private $pass;
    private $domain;
    private $bucket;
    private $ftp;
    private $authFile;
    /**
     * 初始化入口
     * @return FtpStorage
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    protected function initialize(): Storage
    {
        // 读取配置文件
        $this->host = sysconf('storage.ftp_host');
        $this->port = sysconf('storage.ftp_port');
        $this->user = sysconf('storage.ftp_user');
        $this->pass = sysconf('storage.ftp_pass');
        $this->domain = sysconf('storage.ftp_http_domain');
        $this->bucket = sysconf('storage.ftp_bucket');
        $this->ftp = new FtpExtend($this->host, $this->port, $this->user, $this->pass);
        $this->authFile = "upload";
        //多个域名情况随机一个
        $domains = explode(",", $this->domain);
        if (count($domains) > 0) {
            $idx = mt_rand(0, count($domains)-1);
            $this->domain = $domains[$idx];
        }
        // 计算链接前缀
        $type = strtolower(sysconf('storage.ftp_http_protocol'));
        if ($type === 'auto') {
            $this->prefix = "//{$this->domain}/".$this->bucket."/".$this->authFile;
        } elseif ($type === 'http') {
            $this->prefix = "http://{$this->domain}/".$this->bucket."/".$this->authFile;
        } elseif ($type === 'https') {
            $this->prefix = "https://{$this->domain}/".$this->bucket."/".$this->authFile;
        } else {
            throw new \think\Exception('未配置FTP服务器访问域名哦');
        }
        // 初始化配置并返回当前实例
        return parent::initialize();
    }

    protected function getPrefixs(){
        $ftp_http_domain = sysconf('storage.ftp_http_domain');
        $ftp_bucket = sysconf('storage.ftp_bucket');
        $authFile = $this->authFile;
        $type = strtolower(sysconf('storage.ftp_http_protocol'));
        $domains = explode(",", $ftp_http_domain);
        $prefixs = [];
        if (count($domains) > 0) {
             foreach($domains as $domain){
                if ($type === 'auto') {
                    $prefix = "//{$domain}/".$ftp_bucket."/".$authFile;
                } elseif ($type === 'http') {
                    $prefix = "http://{$domain}/".$ftp_bucket."/".$authFile;
                } elseif ($type === 'https') {
                    $prefix = "https://{$domain}/".$ftp_bucket."/".$authFile;
                } else {
                    throw new \think\Exception('未配置FTP服务器访问域名哦');
                }
                $prefixs[] = $prefix;
             }
        }
        return $prefixs;
    }
    /**
     * 获取当前实例对象
     * @param null $name
     * @return FtpStorage
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public static function instance($name = null): Storage
    {
        return parent::instance('ftp');
    }

    /**
     * 上传文件内容
     * @param string $name 文件名称
     * @param string $file 文件内容
     * @param boolean $safe 安全模式
     * @param string $attname 下载名称
     * @return array
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function set($name, $file, $safe = false, $attname = null)
    {
        try {
            //判断是否已经存在
            if ($this->has($name, $safe)) {
                return $this->info($name, $safe, $attname);
            }
            $path = $this->path($name, $safe);
            if (!file_exists($path)) {
                @file_exists(dirname($path)) || mkdir(dirname($path), 0755, true);
                @file_put_contents($path, $file);
            }
            //上传到ftp
            $this->ftp->upFile($this->path($name, $safe), $this->ftpPath($name, $safe));
            //删除本地临时文件
            @unlink($this->path($name, $safe));
            return $this->info($name, $safe, $attname,true);
        } catch (\think\Exception $e) {
            echo $e->getMessage();  
            exit;
        } finally {
            $this->ftp->close(); // 关闭FTP连接
        }
    }

    /**
     * 根据文件名读取文件内容
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @return string
     */
    public function get($name, $safe = false)
    {
        $url = $this->url($name, $safe) . "?e=" . time();
        return file_get_contents($url);
    }

    /**
     * 删除存储的文件
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @return boolean|null
     */
    public function del($name, $safe = false)
    {
        if ($this->has($name, $safe)) {
            return $this->ftp->delFile($this->ftpPath($name, $safe));
        } else {
            return false;
        }
    }

    /**
     * 检查文件是否已经存在
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @return boolean
     */
    public function has($name, $safe = false)
    {
        return $this->ftp->hasFile($this->ftpPath($name, $safe));
    }

    /**
     * 获取文件当前URL地址
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @param string $attname 下载名称
     * @return string
     */
    public function url($name, $safe = false, $attname = null)
    {
        return "{$this->prefix}/{$this->delSuffix($name)}{$this->getSuffix($attname)}";
    }

        /**
     * 获取文件当前URL地址
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @param string $attname 下载名称
     * @return string
     */
    public function urls($name, $safe = false, $attname = null)
    {
        $prefixs = $this->getPrefixs();
        $urls = [];
        foreach($prefixs as $prefix){ 
            $urls[] = "{$prefix}/{$this->delSuffix($name)}{$this->getSuffix($attname)}";
        }
        return $urls;
    }
    /**
     * 获取FTP文件存储路径
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @return string
     */
    public function ftpPath($name, $safe = false)
    {
        return $this->authFile."/".$name;
    }

    /**
     * 获取本地文件存储路径
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @return string
     */
    public function path($name, $safe = false)
    {
        $root = $this->app->getRootPath();
        $path = $safe ? 'safefile' : 'public/upload';
        return strtr("{$root}{$path}/{$this->delSuffix($name)}", '\\', '/');
    }

    /**
     * 获取文件存储信息
     * @param string $name 文件名称
     * @param boolean $safe 安全模式
     * @param string $attname 下载名称
     * @return array
     */
    public function info($name, $safe = false, $attname = null,$checkAll= false)
    {
        $urls = $checkAll?$this->urls($name, $safe, $attname):[$this->url($name, $safe, $attname)];
        Log::info("是否全面检测:". $checkAll .",待检测列表:".json_encode($urls));
        $url_pass = $this->check_remote_file_exists($urls,$checkAll?true:false);
        if ($url_pass) {
            return  ['file' => $name, 'url' => $url_pass, 'key' => $name];
        } else {
            return [];
        }
    }

    
    /**
     * 获取文件上传地址
     * @return string
     */
    public function upload()
    {
        return url('@admin/api.upload/file')->build();
    }

    private function execWithRetry($url,$curl,$maxTimes,$currTimes,&$found){
        Log::info("请求:".$url.",第". $currTimes ."次,最大请求次数:".$maxTimes);
        $result = curl_exec($curl); 
        Log::info("请求:".$url.",第". $currTimes ."次,result:".$result);
        if ($result !== false) {     
            /** 再检查http响应码是否为200 */
            $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
            Log::info("请求:".$url.",第". $currTimes ."次,statusCode:".$statusCode);
            if ($statusCode == 200) {
                $found = true;
            }
            else{                
                $currTimes++;
                if($currTimes<=$maxTimes){
                    sleep(2*$currTimes-1);
                    //请求状态不为200,等待2n-1秒后重试
                    $this->execWithRetry($url,$curl,$maxTimes,$currTimes,$found);
                }
            }
        }
        else{
            Log::info("请求:".$url.",第". $currTimes ."次");
            $currTimes++;
            if($currTimes<=$maxTimes){
                sleep($currTimes);
                //请求发送失败,等待n秒后重试
                $this->execWithRetry($url,$curl,$maxTimes,$currTimes,$found);
            }
        }
    }

    private function check_remote_file_exists($urls,$retry=false)
    {
        $url_pass = "";
        foreach($urls as $url){
            $curl = curl_init($url); // 不取回数据
            curl_setopt($curl, CURLOPT_NOBODY, true);
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); // 发送请求
            
            // 在尝试连接时等待的秒数
            curl_setopt($curl, CURLOPT_CONNECTTIMEOUT , 5);
            // 最大执行时间
            curl_setopt($curl, CURLOPT_TIMEOUT, 120);
            // $result = curl_exec($curl);
            // $found = false; // 如果请求没有发送失败
            // if ($result !== false) {
        
            //     /** 再检查http响应码是否为200 */
            //     $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
            //     if ($statusCode == 200) {
            //         $found = true;
            //     }
            // }
            $found = false; // 如果请求没有发送失败
            if($retry){
                $this->execWithRetry($url,$curl,4,1,$found);
            }
            else{
                $this->execWithRetry($url,$curl,1,1,$found);
            }
            curl_close($curl);
            if($found){
                $url_pass = $url;
                break;
            }
        }
        return $url_pass;
    }
}

评论 (3)

King 创建了任务
King 关联仓库设置为Anyon/ThinkLibrary
King 修改了描述
King 修改了描述
展开全部操作日志

希望@Anyon 大大集成进该项目。

FtpExtend ?

是的,图片文件有时候需要上传到专门的文件服务器使用专用的域名上cdn的。

登录 后才可以发表评论

状态
负责人
里程碑
Pull Requests
关联的 Pull Requests 被合并后可能会关闭此 issue
分支
开始日期   -   截止日期
-
置顶选项
优先级
参与者(2)
1196728 mdpets 1578945850 126695 zoujingli 1578918740
PHP
1
https://gitee.com/zoujingli/ThinkLibrary.git
git@gitee.com:zoujingli/ThinkLibrary.git
zoujingli
ThinkLibrary
ThinkLibrary

搜索帮助