478 lines
14 KiB
PHP
478 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Modules\SystemTool\Services;
|
|
|
|
use App\Exceptions\HttpResponseException;
|
|
use Illuminate\Filesystem\FilesystemAdapter;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Modules\SystemTool\Enum\FileType;
|
|
use Modules\SystemTool\Models\SysFileModel;
|
|
use Modules\SystemTool\Settings\StorageSettings;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
/**
|
|
* 文件服务类
|
|
*/
|
|
class SysFileService
|
|
{
|
|
|
|
/**
|
|
* 获取回收站列表
|
|
* @param array $params
|
|
* @return array
|
|
*/
|
|
public function getTrashedList(array $params = []): array
|
|
{
|
|
$pageSize = $params['pageSize'] ?? 10;
|
|
return SysFileModel::onlyTrashed()
|
|
->paginate($pageSize)
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* 获取存储磁盘实例
|
|
*/
|
|
protected function disk($disk = null): FilesystemAdapter
|
|
{
|
|
if(!$disk) {
|
|
$disk = config('filesystems.default');
|
|
}
|
|
if ($disk === 's3' && !self::isS3Configured()) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.storage.s3_not_configured')]);
|
|
}
|
|
/** @var FilesystemAdapter */
|
|
return Storage::disk($disk);
|
|
}
|
|
|
|
/**
|
|
* 生成存储路径
|
|
*/
|
|
protected function generateStoragePath(string $extension): string
|
|
{
|
|
return date('Ymd') . '/' . uniqid() . '.' . $extension;
|
|
}
|
|
|
|
/**
|
|
* 上传文件
|
|
* @param UploadedFile $file 文件
|
|
* @param int $groupId 分组 ID
|
|
* @param int $channel 上传来源 0:匿名用户,10:后台用户,20:APP用户
|
|
* @param int|null $user_id 上传用户 ID
|
|
* @return array
|
|
*/
|
|
public function upload(UploadedFile $file, int $groupId = 0, int $channel = 0, ?int $user_id = null): array
|
|
{
|
|
// 文件扩展名
|
|
$fileExt = strtolower($file->getClientOriginalExtension() ?: $file->extension());
|
|
|
|
if (empty($fileExt)) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => '无法识别的文件扩展名']);
|
|
}
|
|
// 推断文件类型
|
|
$fileType = FileType::guessFromExtension($fileExt);
|
|
// 获取储存路径
|
|
$storagePath = $this->generateStoragePath($fileExt);
|
|
// 获取磁盘
|
|
$disk = StorageSettings::get('filesystems.default', 'local');
|
|
// 存储文件并设置可见性
|
|
$stored = $this->disk($disk)->put($storagePath, $file->getContent(), 'public');
|
|
if (!$stored) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.upload_failed')]);
|
|
}
|
|
// 保存到数据库
|
|
$model = new SysFileModel();
|
|
$model->disk = $disk;
|
|
$model->group_id = $groupId;
|
|
$model->channel = $channel;
|
|
$model->file_type = $fileType->value;
|
|
$model->file_path = $storagePath;
|
|
$model->file_name = $file->getClientOriginalName();
|
|
$model->file_size = $file->getSize();
|
|
$model->file_ext = $fileExt;
|
|
$model->uploader_id = $user_id;
|
|
$model->save();
|
|
return $model->toArray();
|
|
}
|
|
|
|
/**
|
|
* 软删除文件(移入回收站)
|
|
*/
|
|
public function delete(int $id): bool
|
|
{
|
|
$file = SysFileModel::find($id);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
return (bool) $file->delete();
|
|
}
|
|
|
|
/**
|
|
* 批量软删除文件
|
|
*/
|
|
public function batchDelete(array $fileIds): int
|
|
{
|
|
return SysFileModel::whereIn('id', $fileIds)->delete();
|
|
}
|
|
|
|
/**
|
|
* 恢复已删除的文件
|
|
*/
|
|
public function restore(int $fileId): bool
|
|
{
|
|
$file = SysFileModel::withTrashed()->find($fileId);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
return $file->restore();
|
|
}
|
|
|
|
/**
|
|
* 批量恢复文件
|
|
*/
|
|
public function batchRestore(array $fileIds): int
|
|
{
|
|
return SysFileModel::withTrashed()->whereIn('id', $fileIds)->restore();
|
|
}
|
|
|
|
/**
|
|
* 彻底删除文件(含物理删除)
|
|
*/
|
|
public function forceDelete(int $fileId): bool
|
|
{
|
|
$file = SysFileModel::withTrashed()->find($fileId);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
// 删除物理文件
|
|
$this->disk($file->disk)->delete($file->file_path);
|
|
|
|
return (bool) $file->forceDelete();
|
|
}
|
|
|
|
/**
|
|
* 批量彻底删除文件
|
|
*/
|
|
public function batchForceDelete(array $fileIds): int
|
|
{
|
|
$files = SysFileModel::withTrashed()->whereIn('id', $fileIds)->get();
|
|
$count = 0;
|
|
|
|
foreach ($files as $file) {
|
|
$this->disk($file->disk)->delete($file->file_path);
|
|
$file->forceDelete();
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* 下载文件
|
|
*/
|
|
public function download(int $fileId, ?string $filename = null): StreamedResponse
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
$downloadName = $filename ?? $file->file_name;
|
|
return $this->disk($file->disk)->download($file->file_path, $downloadName);
|
|
}
|
|
|
|
/**
|
|
* 获取文件流式响应(用于在线预览等场景)
|
|
*/
|
|
public function stream(int $fileId): StreamedResponse
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
$mimeType = $this->getMimeType($file->file_ext);
|
|
|
|
return response()->stream(
|
|
function () use ($file) {
|
|
$stream = $this->disk($file->disk)->readStream($file->file_path);
|
|
fpassthru($stream);
|
|
if (is_resource($stream)) {
|
|
fclose($stream);
|
|
}
|
|
},
|
|
200,
|
|
[
|
|
'Content-Type' => $mimeType,
|
|
'Content-Disposition' => 'inline; filename="' . $file->file_name . '"',
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 获取文件内容
|
|
*/
|
|
public function getContent(int $fileId): string
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
return $this->disk($file->disk)->get($file->file_path);
|
|
}
|
|
|
|
/**
|
|
* 获取文件访问URL
|
|
*/
|
|
public function getUrl(int $fileId): ?string
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
return null;
|
|
}
|
|
|
|
return $this->disk($file->disk)->url($file->file_path);
|
|
}
|
|
|
|
/**
|
|
* 根据路径获取访问URL
|
|
*/
|
|
public function getUrlByPath(string $path, ?string $disk = null): string
|
|
{
|
|
return $this->disk($disk)->url($path);
|
|
}
|
|
|
|
/**
|
|
* 获取文件元数据信息
|
|
*/
|
|
public function getMetadata(int $fileId): ?array
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
return null;
|
|
}
|
|
|
|
$disk = $this->disk($file->disk);
|
|
|
|
return [
|
|
'id' => $file->id,
|
|
'name' => $file->file_name,
|
|
'path' => $file->file_path,
|
|
'disk' => $file->disk,
|
|
'size' => $file->file_size,
|
|
'extension' => $file->file_ext,
|
|
'mime_type' => $this->getMimeType($file->file_ext),
|
|
'last_modified' => $disk->exists($file->file_path)
|
|
? date('Y-m-d H:i:s', $disk->lastModified($file->file_path))
|
|
: null,
|
|
'url' => $this->getUrl($file->id),
|
|
'group_id' => $file->group_id,
|
|
'uploader_id' => $file->uploader_id,
|
|
'created_at' => $file->created_at?->format('Y-m-d H:i:s'),
|
|
'updated_at' => $file->updated_at?->format('Y-m-d H:i:s'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 复制文件
|
|
*/
|
|
public function copy(int $fileId, int $targetGroupId = 0): array
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
$newPath = $this->generateStoragePath($file->file_ext);
|
|
|
|
$this->disk($file->disk)->copy($file->file_path, $newPath);
|
|
|
|
// 创建新的文件记录
|
|
$newFile = SysFileModel::create([
|
|
'disk' => $file->disk,
|
|
'group_id' => $targetGroupId,
|
|
'channel' => $file->channel,
|
|
'file_name' => $file->file_name,
|
|
'file_type' => $file->file_type,
|
|
'file_path' => $newPath,
|
|
'file_size' => $file->file_size,
|
|
'file_ext' => $file->file_ext,
|
|
'uploader_id' => Auth::id(),
|
|
]);
|
|
|
|
return $newFile->toArray();
|
|
}
|
|
|
|
/**
|
|
* 批量复制文件
|
|
*/
|
|
public function batchCopy(array $fileIds, int $targetGroupId = 0): bool
|
|
{
|
|
$files = SysFileModel::whereIn('id', $fileIds)->get();
|
|
$fileArray = [];
|
|
|
|
foreach ($files as $file) {
|
|
|
|
$newPath = $this->generateStoragePath($file->file_ext);
|
|
|
|
$this->disk($file->disk)->copy($file->file_path, $newPath);
|
|
|
|
$fileArray[] =[
|
|
'disk' => $file->disk,
|
|
'group_id' => $targetGroupId,
|
|
'channel' => $file->channel,
|
|
'file_name' => $file->file_name,
|
|
'file_type' => $file->file_type,
|
|
'file_path' => $newPath,
|
|
'file_size' => $file->file_size,
|
|
'file_ext' => $file->file_ext,
|
|
'uploader_id' => Auth::id(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
];
|
|
}
|
|
return SysFileModel::insert($fileArray);
|
|
}
|
|
|
|
/**
|
|
* 移动文件
|
|
*/
|
|
public function move(int $fileId, int $groupId): bool
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
return $file->update(['group_id' => $groupId]);
|
|
}
|
|
|
|
/**
|
|
* 批量移动
|
|
*/
|
|
public function batchMove(array $fileIds, int $groupId): int
|
|
{
|
|
return SysFileModel::whereIn('id', $fileIds)->update(['group_id' => $groupId]);
|
|
}
|
|
|
|
/**
|
|
* 重命名文件
|
|
*/
|
|
public function rename(int $fileId, string $newName): bool
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
|
}
|
|
|
|
return $file->update(['file_name' => $newName]);
|
|
}
|
|
|
|
/**
|
|
* 检查文件是否存在
|
|
*/
|
|
public function exists(int $fileId): bool
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
return false;
|
|
}
|
|
|
|
return $this->disk($file->disk)->exists($file->file_path);
|
|
}
|
|
|
|
/**
|
|
* 获取文件大小(字节)
|
|
*/
|
|
public function getSize(int $fileId): ?int
|
|
{
|
|
$file = SysFileModel::find($fileId);
|
|
if (!$file) {
|
|
return null;
|
|
}
|
|
|
|
return $this->disk($file->disk)->size($file->file_path);
|
|
}
|
|
|
|
/**
|
|
* 获取MIME类型
|
|
*/
|
|
protected function getMimeType(string $extension): string
|
|
{
|
|
$mimeTypes = [
|
|
// 图片
|
|
'jpg' => 'image/jpeg',
|
|
'jpeg' => 'image/jpeg',
|
|
'png' => 'image/png',
|
|
'gif' => 'image/gif',
|
|
'webp' => 'image/webp',
|
|
'avif' => 'image/avif',
|
|
'bmp' => 'image/bmp',
|
|
'svg' => 'image/svg+xml',
|
|
// 音频
|
|
'mp3' => 'audio/mpeg',
|
|
'wav' => 'audio/wav',
|
|
'ogg' => 'audio/ogg',
|
|
'flac' => 'audio/flac',
|
|
'aac' => 'audio/aac',
|
|
// 视频
|
|
'mp4' => 'video/mp4',
|
|
'webm' => 'video/webm',
|
|
'mkv' => 'video/x-matroska',
|
|
'mov' => 'video/quicktime',
|
|
'avi' => 'video/x-msvideo',
|
|
// 文档
|
|
'pdf' => 'application/pdf',
|
|
'doc' => 'application/msword',
|
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'xls' => 'application/vnd.ms-excel',
|
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
// 压缩包
|
|
'zip' => 'application/zip',
|
|
'rar' => 'application/vnd.rar',
|
|
'7z' => 'application/x-7z-compressed',
|
|
// 其他
|
|
'json' => 'application/json',
|
|
'xml' => 'application/xml',
|
|
'txt' => 'text/plain',
|
|
];
|
|
|
|
return $mimeTypes[strtolower($extension)] ?? 'application/octet-stream';
|
|
}
|
|
|
|
/**
|
|
* 清空回收站文件
|
|
*/
|
|
public function cleanTrashed(): int
|
|
{
|
|
$expiredFiles = SysFileModel::onlyTrashed()->get();
|
|
$count = 0;
|
|
foreach ($expiredFiles as $file) {
|
|
$this->disk($file->disk)->delete($file->file_path);
|
|
$file->forceDelete();
|
|
$count++;
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
|
|
/**
|
|
* 检查 S3 配置是否有效
|
|
* @return bool
|
|
*/
|
|
public static function isS3Configured(): bool
|
|
{
|
|
$storageConfig = config('filesystems.disks.s3');
|
|
|
|
if (empty($storageConfig)) {
|
|
return false;
|
|
}
|
|
|
|
return !empty($storageConfig['key'])
|
|
&& !empty($storageConfig['secret'])
|
|
&& !empty($storageConfig['bucket'])
|
|
&& !empty($storageConfig['region']);
|
|
}
|
|
}
|