实现文件上传管理功能
由 学院君 创建于5年前, 最后更新于 5年前
版本号 #1
68437 views
35 likes
0 collects
本节我们将在后台为博客应用实现文件上传管理(包括文件上传、预览及删除、目录创建及删除)功能,并且使用本地文件系统保存上传的文件。
1、配置本地文件系统
让我们从配置开始吧,我们在 public 目录下创建一个 uploads 目录用来存放上传的文件,这样所有上传文件都可以通过浏览器直接访问。
首先我们在博客项目目录下使用如下命令在 public 目录下创建 uploads 子目录:
很简单。接下来我们来编辑 config/blog.php:
return [
'title' => 'My Blog',
'posts_per_page' => 5,
'uploads' => [
'storage' => 'local',
'webpath' => '/uploads',
],
];
我们在 uploads 配置项中使用 storage 定义使用的文件系统,使用 webpath 定义 web 访问根目录。
最后,编辑 config/filesystems.php 如下:
//将如下区块代码
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
// 修改成这样
'disks' => [
'local' => [
'driver' => 'local',
'root' => public_path('uploads'),
],
我们将本地存储的根目录修改为前面创建的 public/uploads 目录。
2、创建帮助函数文件
在 Laravel 5.1 项目中有时我们会需要一些不依赖于类的辅助函数,通常我们会将这些辅助函数定义在一个单独文件如 helpers.php 中。我们在 app 目录下创建这个名为 helpers.php 的文件,并编辑其内容如下:
/**
* 返回可读性更好的文件尺寸
*/
function human_filesize($bytes, $decimals = 2)
{
$size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) .@$size[$factor];
}
/**
* 判断文件的MIME类型是否为图片
*/
function is_image($mimeType)
{
return starts_with($mimeType, 'image/');
}
其中 human_filesize() 函数返回一个易读的文件尺寸,is_image() 函数在文件类型为图片的时候返回 true。
要让应用能够正确找到 helpers.php 文件,还要修改项目根目录下的 composer.json:
{
...
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"App\\": "app/"
},
"files": [
"app/helpers.php"
]
},
...
}
在 autoload 配置项的 files 数组中指定要被加载的文件/文件夹。修改完成后记得运行 composer dumpauto 确保修改生效:
现在 helpers.php 中的所有函数都会载入到自动加载器中,你可以在博客应用的代码中任意使用其中的函数。
3、创建文件上传管理服务
现在基本配置已经完成了,让我们创建一个服务类来管理上传文件。
检测文件 MIME 类型
我们想要基于不同类型的上传文件进行不同的操作,这可以通过检测上传文件 MIME 类型轻松实现。
PHP 有一个内置函数 mime_content_type() 用于检测文件的MIME类型,但是该函数已经废弃了,我们使用另一个解决方案。
在 Packagist 中搜索 “mime” 会查询到一个名为 dflydev 的包,我们在博客项目中使用 Composer 安装该依赖包:
composer require "dflydev/apache-mime-types"
我们将使用该依赖包提供的方法来检测文件的 MIME 类型。
创建UploadsManager类
在 app/Services 目录下创建 UploadsManager.php,编辑其内容如下:
namespace App\Services;
use Carbon\Carbon;
use Dflydev\ApacheMimeTypes\PhpRepository;
use Illuminate\Support\Facades\Storage;
class UploadsManager
{
protected $disk;
protected $mimeDetect;
public function __construct(PhpRepository $mimeDetect)
{
$this->disk = Storage::disk(config('blog.uploads.storage'));
$this->mimeDetect = $mimeDetect;
}
/**
* Return files and directories within a folder
*
* @param string $folder
* @return array of [
* 'folder' => 'path to current folder',
* 'folderName' => 'name of just current folder',
* 'breadCrumbs' => breadcrumb array of [ $path => $foldername ]
* 'folders' => array of [ $path => $foldername] of each subfolder
* 'files' => array of file details on each file in folder
* ]
*/
public function folderInfo($folder)
{
$folder = $this->cleanFolder($folder);
$breadcrumbs = $this->breadcrumbs($folder);
$slice = array_slice($breadcrumbs, -1);
$folderName = current($slice);
$breadcrumbs = array_slice($breadcrumbs, 0, -1);
$subfolders = [];
foreach (array_unique($this->disk->directories($folder)) as $subfolder) {
$subfolders["/$subfolder"] = basename($subfolder);
}
$files = [];
foreach ($this->disk->files($folder) as $path) {
$files[] = $this->fileDetails($path);
}
return compact(
'folder',
'folderName',
'breadcrumbs',
'subfolders',
'files'
);
}
/**
* Sanitize the folder name
*/
protected function cleanFolder($folder)
{
return '/' . trim(str_replace('..', '', $folder), '/');
}
/**
* 返回当前目录路径
*/
protected function breadcrumbs($folder)
{
$folder = trim($folder, '/');
$crumbs = ['/' => 'root'];
if (empty($folder)) {
return $crumbs;
}
$folders = explode('/', $folder);
$build = '';
foreach ($folders as $folder) {
$build .= '/'.$folder;
$crumbs[$build] = $folder;
}
return $crumbs;
}
/**
* 返回文件详细信息数组
*/
protected function fileDetails($path)
{
$path = '/' . ltrim($path, '/');
return [
'name' => basename($path),
'fullPath' => $path,
'webPath' => $this->fileWebpath($path),
'mimeType' => $this->fileMimeType($path),
'size' => $this->fileSize($path),
'modified' => $this->fileModified($path),
];
}
/**
* 返回文件完整的web路径
*/
public function fileWebpath($path)
{
$path = rtrim(config('blog.uploads.webpath'), '/') . '/' .ltrim($path, '/');
return url($path);
}
/**
* 返回文件MIME类型
*/
public function fileMimeType($path)
{
return $this->mimeDetect->findType(
pathinfo($path, PATHINFO_EXTENSION)
);
}
/**
* 返回文件大小
*/
public function fileSize($path)
{
return $this->disk->size($path);
}
/**
* 返回最后修改时间
*/
public function fileModified($path)
{
return Carbon::createFromTimestamp(
$this->disk->lastModified($path)
);
}
}
4、实现文件上传管理列表
现在 UploadsManager 服务类已经创建,接下来我们来实现控制器的 index 方法。
创建 index 方法
编辑 app/Http/Controllers/Admin 目录下的UploadController.php 文件内容如下:
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\UploadsManager;
use Illuminate\Http\Request;
class UploadController extends Controller
{
protected $manager;
public function __construct(UploadsManager $manager)
{
$this->manager = $manager;
}
/**
* Show page of files / subfolders
*/
public function index(Request $request)
{
$folder = $request->get('folder');
$data = $this->manager->folderInfo($folder);
return view('admin.upload.index', $data);
}
}
构造方法中注入了 UploadsManager 依赖,在 index()方法中只需传入 folderInfo() 返回的数据到要渲染的视图并返回即可。
你可能已经注意到 $folder 从请求中获取,是的,我们只需要通过请求参数即可实现文件夹修改。
创建 index 视图
在 resources/views/admin 目录下新建 upload 目录,并在该目录下创建 index.blade.php 文件,编辑该文件内容如下:
@extends('admin.layout')
@section('content')
{{-- 顶部工具栏 --}}
Uploads
New Folder
Upload
@include('admin.partials.errors')
@include('admin.partials.success')
NameTypeDateSizeActions
{{-- 子目录 --}}
@foreach ($subfolders as $path => $name)
{{ $name }}
Folder--Delete
@endforeach
{{-- 所有文件 --}}
@foreach ($files as $file)
@if (is_image($file['mimeType']))
@else
@endif
{{ $file['name'] }}
{{ $file['mimeType'] or 'Unknown' }}{{ $file['modified']->format('j-M-y g:ia') }}{{ human_filesize($file['size']) }}Delete
@if (is_image($file['mimeType']))
Preview
@endif
@endforeach
@include('admin.upload._modals')
@stop
@section('scripts')
// 确认文件删除
function delete_file(name) {
$("#delete-file-name1").html(name);
$("#delete-file-name2").val(name);
$("#modal-file-delete").modal("show");
}
// 确认目录删除
function delete_folder(name) {
$("#delete-folder-name1").html(name);
$("#delete-folder-name2").val(name);
$("#modal-folder-delete").modal("show");
}
// 预览图片
function preview_image(path) {
$("#preview-image").attr("src", path);
$("#modal-image-view").modal("show");
}
// 初始化数据
$(function() {
$("#uploads-table").DataTable();
});
@stop
尽管这个模板文件很长,但是理解起来并没有什么困难,所有文件上传和下载管理都将在这里进行。
有没有注意到我们在最后包含了 admin.upload._modals?是的,我们将模态对话框放到了一个单独的视图模板中。现在,我们在 resources/views/admin/upload 目录下创建一个空的 _modals.blade.php 文件。
上传管理界面
打开浏览器,进入博客应用后台管理页面,点击顶部导航条的“上传”(Uploads)链接,将会跳转到如下页面:
既漂亮又清爽,有木有?接下来让我们来实现所有的模态对话框及其背后的业务逻辑。
5、完成文件上传管理功能
对于完整的文件上传管理器而言剩下的工作已经不多了,现在是时候完成所有功能了。
添加路由
我们需要为上传管理器定义所有需要的路由,编辑 app/Http/routes.php 添加如下路由:
// 在这一行下面
get('admin/upload', 'UploadController@index');
// 添加如下路由
post('admin/upload/file', 'UploadController@uploadFile');
delete('admin/upload/file', 'UploadController@deleteFile');
post('admin/upload/folder', 'UploadController@createFolder');
delete('admin/upload/folder', 'UploadController@deleteFolder');
定义所有模态对话框
编辑我们之前创建的 _modals.blade.php 文件内容如下:
{{-- 创建目录 --}}
{{-- 删除文件 --}}
{{-- 删除目录 --}}
{{-- 上传文件 --}}
{{-- 浏览图片 --}}
在该文件中总共有5个不同的模态弹出框,分别对应上面定义的5个路由。
添加表单请求验证类
使用 Artisan 命令创建 UploadFileRequest,并编辑其内容如下:
namespace App\Http\Requests;
use App\Http\Requests\Request;
class UploadFileRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'file' => 'required',
'folder' => 'required',
];
}
}
使用 Artisan 命令创建 UploadNewFolderRequest,并编辑其内容如下:
namespace App\Http\Requests;
use App\Http\Requests\Request;
class UploadNewFolderRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'folder' => 'required',
'new_folder' => 'required',
];
}
}
同样,这些请求类用于验证表单输入。
完成 UploadController 所有方法
编辑 UploadController.php 文件内容如下:
// 添加如下三个use语句到UploadController控制器顶部
use App\Http\Requests\UploadFileRequest;
use App\Http\Requests\UploadNewFolderRequest;
use Illuminate\Support\Facades\File;
// 添加如下四个方法到UploadController控制器类
/**
* 创建新目录
*/
public function createFolder(UploadNewFolderRequest $request)
{
$new_folder = $request->get('new_folder');
$folder = $request->get('folder').'/'.$new_folder;
$result = $this->manager->createDirectory($folder);
if ($result === true) {
return redirect()
->back()
->withSuccess("Folder '$new_folder' created.");
}
$error = $result ? : "An error occurred creating directory.";
return redirect()
->back()
->withErrors([$error]);
}
/**
* 删除文件
*/
public function deleteFile(Request $request)
{
$del_file = $request->get('del_file');
$path = $request->get('folder').'/'.$del_file;
$result = $this->manager->deleteFile($path);
if ($result === true) {
return redirect()
->back()
->withSuccess("File '$del_file' deleted.");
}
$error = $result ? : "An error occurred deleting file.";
return redirect()
->back()
->withErrors([$error]);
}
/**
* 删除目录
*/
public function deleteFolder(Request $request)
{
$del_folder = $request->get('del_folder');
$folder = $request->get('folder').'/'.$del_folder;
$result = $this->manager->deleteDirectory($folder);
if ($result === true) {
return redirect()
->back()
->withSuccess("Folder '$del_folder' deleted.");
}
$error = $result ? : "An error occurred deleting directory.";
return redirect()
->back()
->withErrors([$error]);
}
/**
* 上传文件
*/
public function uploadFile(UploadFileRequest $request)
{
$file = $_FILES['file'];
$fileName = $request->get('file_name');
$fileName = $fileName ?: $file['name'];
$path = str_finish($request->get('folder'), '/') . $fileName;
$content = File::get($file['tmp_name']);
$result = $this->manager->saveFile($path, $content);
if ($result === true) {
return redirect()
->back()
->withSuccess("File '$fileName' uploaded.");
}
$error = $result ? : "An error occurred uploading file.";
return redirect()
->back()
->withErrors([$error]);
}
完成 UploadsManager 服务类
最后编辑 app/Services/UploadsManager.php 内容如下:
// 在该类中新增以下4个方法
/**
* 创建新目录
*/
public function createDirectory($folder)
{
$folder = $this->cleanFolder($folder);
if ($this->disk->exists($folder)) {
return "Folder '$folder' already exists.";
}
return $this->disk->makeDirectory($folder);
}
/**
* 删除目录
*/
public function deleteDirectory($folder)
{
$folder = $this->cleanFolder($folder);
$filesFolders = array_merge(
$this->disk->directories($folder),
$this->disk->files($folder)
);
if (! empty($filesFolders)) {
return "Directory must be empty to delete it.";
}
return $this->disk->deleteDirectory($folder);
}
/**
* 删除文件
*/
public function deleteFile($path)
{
$path = $this->cleanFolder($path);
if (! $this->disk->exists($path)) {
return "File does not exist.";
}
return $this->disk->delete($path);
}
/**
* 保存文件
*/
public function saveFile($path, $content)
{
$path = $this->cleanFolder($path);
if ($this->disk->exists($path)) {
return "File already exists.";
}
return $this->disk->put($path, $content);
}
至此,已经完成了文件上传管理的所有工作,可以去浏览器完成上传文件、创建目录、删除文件等操作了。
6、测试文件上传和删除功能
在浏览器中访问 http://blog.app/admin/upload,点击“New Folder”创建新目录,在弹出的模态对话框中填写表单:
点击“Create Folder”提交表单,创建目录成功:
点击进入新创建的子目录 laravelacademy,在该目录下点击“Upload”按钮上传文件:
点击“Upload File”上传文件,上传成功后跳转到文件列表:
但是去 public/uploads/laravelacademy 目录下查看,上传的文件 Laravel学院 并没有扩展名,而且上面列表里 Type 类型值为 Unkown,预览按钮没显示出来也说明了有问题,正确的文件名应该包含扩展名,看来是上传图片时填写的文件名称有问题,应该这样填写为 LaravelAcademy.jpg,这样重新上传后文件列表显示如下:
这样数据都对了,预览按钮也显示出来了,点击“Preview”按钮,页面显示如下:
最后我们将无效的Laravel学院文件删除,点击其对应的“Delete”按钮,页面弹出确认删除对话框:
点击“Delete File”,确认删除。