需求场景说明
对接的三方商家需要进行文件传输,并且对方提供的方式是 sftp 的服务器账号,我们需根据他们提供的目录进行下载和上传指定文件。
安装
composer require phpseclib/phpseclib:~3.0
使用sftp功能
新建并设置config/sftp.php
文件
return [ 'sftp' => [ 'host' => env('SFTP_HOST', '127.0.0.1'), 'port' => env('SFTP_PORT', 22), 'user' => env('SFTP_USE', null), 'password' => env('SFTP_PASSWORD', null), ],];
配置.env
文件
SFTP_HOST=127.0.0.1SFTP_PORT=22SFTP_USE=userSFTP_PASSWORD=password
封装 app/Utils/SftpHelper.php
调用库文件,通过单例可设置不同的 sftp 服务器
namespace App\Utils;use phpseclib3\Net\SFTP;class SftpHelper{ private static $instance = []; public static function getInstance($key='sftp') { if (!isset(self::$instance[$key])) { $config = ConfigHelper::getInstance()->read('sftp.'.$key); self::$instance[$key] = new SFTP($config['host'], $config['port']); self::$instance[$key]->login($config['user'], $config['password']); } return self::$instance[$key]; }}
使用方法说明
nlist
:获取指定目录下的文件列表,包括子目录,(默认不会递归子目录下的文件)is_readable
: 判断文件是否有读权限chmod
:修改文件/目录权限,默认不递归get
:获取文件,默认获取文件内容。is_dir
:是否存在该目录mkdir
:创建目录rename
: 将文件重命名put
:上传文件
访问 sftp 服务器并下载文件到本地
1 读取指定服务器下的文件,并循环处理每个文件
2 下载远程文件到当前服务器的指定位置,并创建待处理文件记录表
说明:创建文件处理表可使文件读取逻辑失败时,可重复处理,并且不需要多次访问 sftp 服务器,进行逻辑解耦
3 创建文件记录数据后将服务器上的文件移到归档目录,避免重复读取
// 连接sftp服务器并登录$sftp = SftpHelper::getInstance('sftp');// 获取目录下的文件列表(不递归)$file_list = $sftp->nlist($remote_dir);// 循环文件列表,获取处理数据foreach ($file_list as $file_name) { // 跳过不处理的目录 if (in_array($file_name, ['.', '..', 'Archive'])) { continue; } // 拼接完整的服务器文件路径 $remote_file = $remote_dir.$file_name; // 设置本地存储的目录 $save_path = env('FILE_PATH', '/data/storage/sftp/')."{$file_type}/"; File::exists($save_path) or (File::makeDirectory($save_path, 0777, true) && @chmod($save_path, 0777)); // 完整的本地路径 $local_file = $save_path. $file_name; // 拉取sftp文件到本地目录 if (!file_exists($local_file)) { if (!$sftp->is_readable($remote_file)) { $sftp->chmod('0777', $remote_file); } $sftp->get($remote_file, $local_file); } // 添加文件日志(同一个远程文件不重复拉取) // 后续可单独增加文件读取逻辑,使文件内容处理失败时可重复处理,并且不需要重复访问 sftp 服务器去读取远程文件 SftpFile::updateOrCreate([ 'remote_dir' => $remote_file, ], [ 'action' => $file_type, // 文件类型 'filename' => $file_name, // 文件名 'filepath' => $local_file, // 本地服务器路径 ]); // 日志创建成功之后再将文件移到Archive目录下,避免重复读取 if (!$sftp->is_dir($remote_dir.'Archive/')) { // 没有则创建Archive目录 $sftp->mkdir($remote_dir.'Archive/'); } // 已读取的文件移到子目录Archive $sftp->rename($remote_file, "Archive/{$remote_file}");}
上传文件到 sftp 服务器的指定位置
// 读取待处理的文件列表$file_list = SftpFile::where([ 'action' => $file_type, 'state' => 1])->get();if (count($file_list) <= 0) { return;}// 连接sftp服务器并登录$mk_sftp = SftpHelper::getInstance('sftp');foreach ($file_list as $file) { // 校验推送的文件是否存在 if (!file_exists($file->filepath)) { throw new ParamsException('推送的文件不存在'); } $file_path = $file->filepath; $remote_file = $file->remote_dir; // 推送文件到sftp服务器 // SFTP::SOURCE_LOCAL_FILE 表示以文件的形式,不设置时表示是按字符串形式上传 $put_res = $mk_sftp->put($remote_file, $file_path, SFTP::SOURCE_LOCAL_FILE); if ($put_res) { $file->state = 1; $file->save(); }}
读取文件内容
// 当前php.ini配置的是128Mini_set('memory_limit', '300M');$local_file = $file_info['filepath'];$remote_file = $file_info['remote_dir'];// 读取文件数据$fp = fopen($local_file, 'r');$file_data = [];while (!feof($fp)) { $row_str = fgets($fp); // 逐行读取。如果fgets不写length参数,默认是读取1k。 $item = explode(',', trim($row_str)); // 跳过表头 // 将行数据转成指定的键值对}return $file_data;