作者 赵彬吉

update

  1 +<?php
  2 +
  3 +
  4 +namespace App\Helper;
  5 +
  6 +/**
  7 + * 计算文件hash
  8 + *
  9 + * 七牛云的计算规则
  10 + */
  11 +class FileEtag
  12 +{
  13 + const BLOCK_SIZE = 4194304; //4*1024*1024 分块上传块大小,该参数为接口规格,不能修改
  14 +
  15 + private static function packArray($v, $a)
  16 + {
  17 + return call_user_func_array('pack', array_merge(array($v), (array)$a));
  18 + }
  19 +
  20 + private static function blockCount($fsize)
  21 + {
  22 + return intval(($fsize + (self::BLOCK_SIZE - 1)) / self::BLOCK_SIZE);
  23 + }
  24 +
  25 + private static function calcSha1($data)
  26 + {
  27 + $sha1Str = sha1($data, true);
  28 + $err = error_get_last();
  29 + if ($err !== null) {
  30 + return array(null, $err);
  31 + }
  32 + $byteArray = unpack('C*', $sha1Str);
  33 + return array($byteArray, null);
  34 + }
  35 +
  36 + private static function base64_urlSafeEncode($data)
  37 + {
  38 + $find = array('+', '/');
  39 + $replace = array('-', '_');
  40 + return str_replace($find, $replace, base64_encode($data));
  41 + }
  42 +
  43 + public static function sum($filename)
  44 + {
  45 + $fhandler = fopen($filename, 'r');
  46 + $err = error_get_last();
  47 + if ($err !== null) {
  48 + return array(null, $err);
  49 + }
  50 +
  51 + $fstat = fstat($fhandler);
  52 + $fsize = $fstat['size'];
  53 + if ((int)$fsize === 0) {
  54 + fclose($fhandler);
  55 + return array('Fto5o-5ea0sNMlW_75VgGJCv2AcJ', null);
  56 + }
  57 + $blockCnt = self::blockCount($fsize);
  58 + $sha1Buf = array();
  59 +
  60 + if ($blockCnt <= 1) {
  61 + array_push($sha1Buf, 0x16);
  62 + $fdata = fread($fhandler, self::BLOCK_SIZE);
  63 + if ($err !== null) {
  64 + fclose($fhandler);
  65 + return array(null, $err);
  66 + }
  67 + list($sha1Code,) = self::calcSha1($fdata);
  68 + $sha1Buf = array_merge($sha1Buf, $sha1Code);
  69 + } else {
  70 + array_push($sha1Buf, 0x96);
  71 + $sha1BlockBuf = array();
  72 + for ($i = 0; $i < $blockCnt; $i++) {
  73 + $fdata = fread($fhandler, self::BLOCK_SIZE);
  74 + list($sha1Code, $err) = self::calcSha1($fdata);
  75 + if ($err !== null) {
  76 + fclose($fhandler);
  77 + return array(null, $err);
  78 + }
  79 + $sha1BlockBuf = array_merge($sha1BlockBuf, $sha1Code);
  80 + }
  81 + $tmpData = self::packArray('C*', $sha1BlockBuf);
  82 + list($sha1Final,) = self::calcSha1($tmpData);
  83 + $sha1Buf = array_merge($sha1Buf, $sha1Final);
  84 + }
  85 + $etag = self::base64_urlSafeEncode(self::packArray('C*', $sha1Buf));
  86 + return array($etag, null);
  87 + }
  88 +}
  1 +<?php
  2 +
  3 +
  4 +namespace App\Http\Controllers\Bside;
  5 +
  6 +
  7 +use App\Services\Facades\Upload;
  8 +use Illuminate\Http\Request;
  9 +use Illuminate\Support\Facades\Storage;
  10 +
  11 +class FileController extends BaseController
  12 +{
  13 + /**
  14 + * 上传
  15 + * @param Request $request
  16 + * @return \Illuminate\Http\JsonResponse
  17 + * @throws \Psr\Container\ContainerExceptionInterface
  18 + * @throws \Psr\Container\NotFoundExceptionInterface
  19 + * @author zbj
  20 + * @date 2023/4/20
  21 + */
  22 + public function upload(Request $request){
  23 + // 上传文件
  24 + $files = Upload::puts('files', $this->param['config']);
  25 + foreach ($files as &$file){
  26 + $file = Storage::disk('upload')->url($file);
  27 + }
  28 + return $this->success($files);
  29 + }
  30 +
  31 + /**
  32 + * 下载
  33 + * @param Request $request
  34 + * @return \Symfony\Component\HttpFoundation\StreamedResponse
  35 + * @author zbj
  36 + * @date 2023/4/20
  37 + */
  38 + public function download(Request $request){
  39 + $path = Upload::url2path($this->param['url']);
  40 + return Storage::disk('upload')->download($path);
  41 + }
  42 +}
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 2
3 namespace App\Providers; 3 namespace App\Providers;
4 4
5 -use App\Services\PaginatorServer; 5 +use App\Services\PaginatorService;
6 use Illuminate\Support\ServiceProvider; 6 use Illuminate\Support\ServiceProvider;
7 7
8 class AppServiceProvider extends ServiceProvider 8 class AppServiceProvider extends ServiceProvider
@@ -16,7 +16,7 @@ class AppServiceProvider extends ServiceProvider @@ -16,7 +16,7 @@ class AppServiceProvider extends ServiceProvider
16 { 16 {
17 //自定义分页 17 //自定义分页
18 $this->app->bind('Illuminate\Pagination\LengthAwarePaginator',function ($app,$options){ 18 $this->app->bind('Illuminate\Pagination\LengthAwarePaginator',function ($app,$options){
19 - return new PaginatorServer($options['items'], $options['total'], $options['perPage'], $options['currentPage'] , $options['options']); 19 + return new PaginatorService($options['items'], $options['total'], $options['perPage'], $options['currentPage'] , $options['options']);
20 }); 20 });
21 } 21 }
22 22
@@ -13,13 +13,14 @@ class BaseService @@ -13,13 +13,14 @@ class BaseService
13 { 13 {
14 /** 14 /**
15 * @notes: 手动抛出异常 15 * @notes: 手动抛出异常
  16 + * @param string $msg
16 * @param string $code 17 * @param string $code
17 * @throws BsideGlobalException 18 * @throws BsideGlobalException
18 * @author:wlj 19 * @author:wlj
19 * @date: 2022/7/19 14:25 20 * @date: 2022/7/19 14:25
20 */ 21 */
21 - public function fail(string $code = Code::SYSTEM_ERROR) 22 + public function fail(string $msg, string $code = Code::SYSTEM_ERROR)
22 { 23 {
23 - throw new BsideGlobalException($code); 24 + throw new BsideGlobalException($code, $msg);
24 } 25 }
25 } 26 }
  1 +<?php
  2 +
  3 +namespace App\Services\Facades;
  4 +
  5 +use Illuminate\Support\Facades\Facade;
  6 +
  7 +/**
  8 + * @see \App\Services\UploadService
  9 + * @method static string put(string $input_name="file", string|array $config="default") 单个文件上传
  10 + * @method static array puts(string $input_name="file", string|array $config="default") 批量获取上传的文件
  11 + * @method static array filePut(string $filename, string $content, string|array $config="default")
  12 + * @method static string url2path(string $url, string|array $disk="upload")
  13 + */
  14 +class Upload extends Facade
  15 +{
  16 + protected static function getFacadeAccessor()
  17 + {
  18 + return '\App\Services\UploadService';
  19 + }
  20 +}
@@ -11,7 +11,7 @@ use Illuminate\Pagination\LengthAwarePaginator; @@ -11,7 +11,7 @@ use Illuminate\Pagination\LengthAwarePaginator;
11 * @author zbj 11 * @author zbj
12 * @date 2023/4/14 12 * @date 2023/4/14
13 */ 13 */
14 -class PaginatorServer extends LengthAwarePaginator 14 +class PaginatorService extends LengthAwarePaginator
15 { 15 {
16 public function toArray() 16 public function toArray()
17 { 17 {
  1 +<?php
  2 +
  3 +namespace App\Services;
  4 +
  5 +
  6 +use App\Helper\FileEtag;
  7 +use Illuminate\Support\Facades\DB;
  8 +use Illuminate\Support\Facades\Storage;
  9 +use Illuminate\Support\Str;
  10 +use Illuminate\Http\UploadedFile;
  11 +
  12 +/**
  13 + * Class UploadServer
  14 + * @package App\Services
  15 + */
  16 +class UploadService extends BaseService
  17 +{
  18 +
  19 + /**
  20 + * 默认配置
  21 + * @see $defaultConfig
  22 + * @var array
  23 + */
  24 + public $config = [];
  25 +
  26 + /**
  27 + * 默认配置
  28 + * @var array
  29 + */
  30 + public $defaultConfig = [
  31 + // 上传文件的大小范围
  32 + 'size' => [
  33 + 'max' => 1024 * 1024 * 2, // 2M
  34 + ],
  35 + // 磁盘
  36 + 'disk' => 'upload',
  37 + // 扩展名
  38 + 'ext' => [
  39 + 'png', 'jpg', 'jpeg', 'gif'
  40 + ],
  41 + // 目录
  42 + 'path' => '',
  43 + ];
  44 +
  45 + /**
  46 + * @param string|array $config
  47 + * @author:dc
  48 + * @time 2022/1/7 14:38
  49 + */
  50 + private function config($config = 'default')
  51 + {
  52 + if (is_array($config)) {
  53 + $driver = $config['driver'];
  54 + } else {
  55 + $driver = $config;
  56 + }
  57 +
  58 + // 加载配置
  59 + $this->config = $this->defaultConfig;
  60 + if ($driver != 'default') {
  61 + $conf = config('upload.' . $driver);
  62 + if ($conf) {
  63 + // 合并默认配置
  64 + $this->config = array_merge($this->config, $conf);
  65 + }
  66 + }
  67 +
  68 + // 追加目录
  69 + if ($config['path'] ?? '') {
  70 + $this->config['path'] = $this->config['path'] . (strpos($config['path'], '/') !== 0 ? '/' : '') . $config['path'];
  71 + }
  72 + }
  73 +
  74 + /**
  75 + *
  76 + * @param string $input_file
  77 + * @param string $config
  78 + * @return string
  79 + * @throws \Exception
  80 + * @author:dc
  81 + * @time 2021/12/30 16:37
  82 + */
  83 + public function put($input_file = "file", $config = 'default'): string
  84 + {
  85 + $this->config($config);
  86 +
  87 + if ($input_file instanceof UploadedFile) {
  88 + $file = &$input_file;
  89 + } else {
  90 + // 是否是已存在服务器的图片
  91 + if (Str::contains($input_file, env('APP_URL'))) {
  92 + return $input_file;
  93 + } else {
  94 + // 获取post上传文件
  95 + $file = request()->file($input_file);
  96 + // 如果不是上传,是否本地文件
  97 + if (!$file && is_file($input_file)) {
  98 + // 获取文件的名字
  99 + $origin_name = explode(DIRECTORY_SEPARATOR, $input_file);
  100 + !is_array($origin_name) && $origin_name = [$origin_name];
  101 + // 创建一个上传对象
  102 + $file = UploadedFile::createFromBase(new \Symfony\Component\HttpFoundation\File\UploadedFile($input_file, array_pop($origin_name)));
  103 + }
  104 + }
  105 +
  106 + }
  107 +
  108 + if ($file) {
  109 + // 验证扩展名
  110 + if (!in_array($file->extension(), $this->config['ext'])) {
  111 + $this->fail('文件扩展名不匹配,文件扩展名支持' . implode(',', $this->config['ext']));
  112 + }
  113 +
  114 + // 验证文件大小
  115 + if ($file->getSize() > $this->config['size']['max']) {
  116 + $this->fail('文件大小超出' . ($this->config['size']['max'] / 1024 / 1024) . 'M');
  117 + }
  118 +
  119 + //判断文件hash是否已存在
  120 + if ($filename = $this->getFileHash($file)) {
  121 + return $filename;
  122 + }
  123 +
  124 + $pathDate = date('Y-m');
  125 +
  126 + // 保存处理过的图片
  127 + if (!empty($img)) {
  128 + // 保存的图片名称
  129 + $save_name = $this->config['path'] . '/' . $pathDate . '/' . $file->hashName();
  130 + // 保存文件
  131 + if (!Storage::disk($this->config['disk'])
  132 + ->put($save_name, $img->getEncoded())) {
  133 + $save_name = '';
  134 + }
  135 + } else {
  136 + // 保存文件
  137 + $save_name = $file->storeAs($this->config['path'] . '/' . $pathDate, $file->hashName(), $this->config['disk']);
  138 + }
  139 +
  140 + if ($save_name) {
  141 + //保存文件hash
  142 + $this->setFileHash($file, $save_name);
  143 + // 返回地址
  144 + return $save_name;
  145 + }
  146 +
  147 + $this->fail( '上传失败');
  148 +
  149 + }
  150 + // 异常
  151 + $this->fail('请上传文件');
  152 + }
  153 +
  154 + /**
  155 + * 批量上传
  156 + * @param string $input_file
  157 + * @param string $config
  158 + * @return array
  159 + * @throws \Exception
  160 + * @author:dc
  161 + * @time 2021/12/30 16:36
  162 + */
  163 + public function puts($input_file = "file", $config = 'default'): array
  164 + {
  165 + $request = request();
  166 + $files = $request->post($input_file, []);
  167 + $files_ = $request->file($input_file);
  168 + if ($files_) {
  169 + $files = array_merge($files, $files_);
  170 + }
  171 + $filename = [];
  172 + if ($files instanceof UploadedFile) {
  173 + $filename[] = $this->put($files, $config);
  174 + } else if ($files) {
  175 + foreach ($files as $file) {
  176 + $filename[] = $this->put($file, $config);
  177 + }
  178 + } else {
  179 + // 异常
  180 + $this->fail('请上传文件');
  181 + }
  182 +
  183 + return $filename;
  184 + }
  185 +
  186 + /**
  187 + * 上传文件,文件内容
  188 + * @param string $filename
  189 + * @param string $content
  190 + * @param string $config
  191 + * @return string
  192 + * @throws \App\Exceptions\BsideGlobalException
  193 + * @author:dc
  194 + * @time 2022/1/7 14:45
  195 + */
  196 + public function filePut(string $filename, string $content, $config = 'default')
  197 + {
  198 + if ($content) {
  199 + $this->config($config);
  200 + $save_name = $this->config['path'] . '/' . $filename;
  201 + // 保存文件
  202 + $result = Storage::disk($this->config['disk'])->put($save_name, $content);
  203 +
  204 + if ($result) {
  205 + // 返回地址
  206 + return $save_name;
  207 + }
  208 +
  209 + $this->fail('上传失败');
  210 +
  211 + }
  212 + // 异常
  213 + $this->fail('请上传文件');
  214 + }
  215 +
  216 + /**
  217 + * 文件hash是否存在 存在返回文件路径
  218 + * @param $file
  219 + * @return mixed|string|null
  220 + */
  221 + protected function getFileHash($file)
  222 + {
  223 + $hash = FileEtag::sum($file)[0];
  224 + if (!$hash) {
  225 + return '';
  226 + }
  227 + return DB::table('gl_file_hash')->where('hash', $hash)->value('filename');
  228 + }
  229 +
  230 + /**
  231 + * 保存文件hash
  232 + * @param $file
  233 + * @param $save_path
  234 + * @return bool
  235 + */
  236 + protected function setFileHash($file, $save_path): bool
  237 + {
  238 + $hash = FileEtag::sum($file)[0];
  239 +
  240 + if (!$hash) {
  241 + return false;
  242 + }
  243 + $data = [
  244 + 'filename' => $save_path,
  245 + 'hash' => $hash,
  246 + ];
  247 + DB::table('gl_file_hash')->insert($data);
  248 + return true;
  249 + }
  250 +
  251 + /**
  252 + * 文件地址转本地路径
  253 + * @param $url
  254 + * @param string $disk
  255 + * @return array|string|string[]
  256 + * @author zbj
  257 + * @date 2023/4/20
  258 + */
  259 + public function url2path($url, $disk = 'upload'){
  260 + $upload_url = config('filesystems')['disks'][$disk]['url'];
  261 + return str_replace($upload_url . '/', '', $url);
  262 + }
  263 +}
@@ -53,6 +53,13 @@ return [ @@ -53,6 +53,13 @@ return [
53 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 53 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
54 ], 54 ],
55 55
  56 + 'upload' => [
  57 + 'driver' => 'local',
  58 + 'root' => env('UPLOAD_ROOT_PATH') ?: public_path('upload'),
  59 + 'url' => env('UPLOAD_LOCAL_URL') ?: env('APP_URL') . '/upload',
  60 + 'visibility' => 'public',
  61 + ]
  62 +
56 ], 63 ],
57 64
58 /* 65 /*
  1 +<?php
  2 +
  3 +/**
  4 + * 上传配置
  5 + * 如果其他配置没有设置某一项则使用default的
  6 + * 默认配置见下
  7 + * @see \App\Services\UploadService::$config
  8 + */
  9 +return [
  10 + // 产品图集
  11 + 'product' => [
  12 + 'size' => [
  13 + 'max' => 1024*1024*2, // 2M
  14 + ],
  15 + 'path' => '/product'
  16 + ],
  17 +];
@@ -133,9 +133,15 @@ Route::middleware(['bloginauth'])->group(function () { @@ -133,9 +133,15 @@ Route::middleware(['bloginauth'])->group(function () {
133 Route::post('/save', [\App\Http\Controllers\Bside\DeptController::class, 'save'])->name('dept_save'); 133 Route::post('/save', [\App\Http\Controllers\Bside\DeptController::class, 'save'])->name('dept_save');
134 Route::any('/delete', [\App\Http\Controllers\Bside\DeptController::class, 'delete'])->name('dept_delete'); 134 Route::any('/delete', [\App\Http\Controllers\Bside\DeptController::class, 'delete'])->name('dept_delete');
135 }); 135 });
  136 +
  137 + //文件操作
  138 + Route::prefix('file')->group(function () {
  139 + Route::post('/upload', [\App\Http\Controllers\Bside\FileController::class, 'upload'])->name('file_upload');
  140 + });
136 }); 141 });
137 142
138 //无需登录验证的路由组 143 //无需登录验证的路由组
139 Route::group([], function () { 144 Route::group([], function () {
140 Route::any('/login', [\App\Http\Controllers\Bside\ComController::class, 'login'])->name('login'); 145 Route::any('/login', [\App\Http\Controllers\Bside\ComController::class, 'login'])->name('login');
  146 + Route::get('/file/download', [\App\Http\Controllers\Bside\FileController::class, 'download'])->name('file_download');
141 }); 147 });