Skip to content

存储服务

基于 app/System/Service/Storage.php 的文件存储服务,提供多存储后端支持、文件上传下载和 URL 签名功能。

🚀 快速开始

基础使用

php
use App\System\Service\Storage;

// 获取默认存储对象
$storage = Storage::getObject();

// 获取指定名称的存储对象
$storage = Storage::getObject('qiniu');

// 上传文件
$storage->writeStream('path/file.jpg', $fileStream);

// 获取文件 URL
$url = $storage->url('path/file.jpg');

📋 主要功能

1. 多存储后端支持

php
use App\System\Service\Storage;

// 本地存储
$localStorage = Storage::getObject('local');

// 七牛云存储
$qiniuStorage = Storage::getObject('qiniu');

// 阿里云 OSS
$ossStorage = Storage::getObject('oss');

// 腾讯云 COS
$cosStorage = Storage::getObject('cos');

2. 文件操作

php
use App\System\Service\Storage;

$storage = Storage::getObject();

// 写入文件流
$fileStream = fopen('/path/to/local/file.jpg', 'r');
$storage->writeStream('uploads/image.jpg', $fileStream);

// 读取文件
$content = $storage->read('uploads/image.jpg');

// 检查文件是否存在
$exists = $storage->fileExists('uploads/image.jpg');

// 获取文件大小
$size = $storage->fileSize('uploads/image.jpg');

// 删除文件
$storage->delete('uploads/image.jpg');

3. URL 签名

php
use App\System\Service\Storage;

// 生成本地文件签名 URL
$signedUrl = Storage::localSign('/uploads/private/document.pdf');

// 带过期时间的签名(3600秒后过期)
$signedUrl = Storage::localSign('/uploads/private/document.pdf', 3600);

🔧 实际应用示例

1. 文件上传服务

php
<?php

namespace App\System\Service;

use App\System\Service\Storage;

class FileUploadService
{
    /**
     * 上传文件到指定存储
     */
    public static function uploadFile($uploadedFile, array $options = []): array
    {
        try {
            // 获取存储对象
            $storageType = $options['storage'] ?? 'default';
            $storage = Storage::getObject($storageType);

            // 生成文件路径
            $category = $options['category'] ?? 'general';
            $extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
            $filename = ($options['filename'] ?? uniqid()) . '.' . $extension;
            $path = "{$category}/" . date('Y/m/d') . "/{$filename}";

            // 上传文件
            $stream = $uploadedFile->getStream();
            $storage->writeStream($path, $stream->detach());

            // 获取文件信息
            $url = $storage->url($path);
            $size = $uploadedFile->getSize();

            return [
                'success' => true,
                'data' => [
                    'url' => $url,
                    'path' => $path,
                    'size' => $size,
                    'name' => $uploadedFile->getClientFilename(),
                    'type' => $uploadedFile->getClientMediaType(),
                    'storage' => $storageType
                ]
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * 批量上传文件
     */
    public static function uploadMultipleFiles(array $files, array $options = []): array
    {
        $results = [];
        $successCount = 0;
        $failCount = 0;

        foreach ($files as $index => $file) {
            $result = self::uploadFile($file, $options);
            
            if ($result['success']) {
                $successCount++;
            } else {
                $failCount++;
            }

            $results[] = [
                'index' => $index,
                'filename' => $file->getClientFilename(),
                'result' => $result
            ];
        }

        return [
            'total' => count($files),
            'success' => $successCount,
            'failed' => $failCount,
            'results' => $results
        ];
    }

    /**
     * 复制文件到不同存储
     */
    public static function copyToStorage(string $sourcePath, string $targetStorage, string $targetPath = null): array
    {
        try {
            // 获取源存储和目标存储
            $sourceStorage = Storage::getObject();
            $targetStorageObj = Storage::getObject($targetStorage);

            // 使用原路径或指定新路径
            $targetPath = $targetPath ?? $sourcePath;

            // 读取源文件
            $content = $sourceStorage->read($sourcePath);

            // 写入目标存储
            $targetStorageObj->write($targetPath, $content);

            return [
                'success' => true,
                'source_path' => $sourcePath,
                'target_path' => $targetPath,
                'target_storage' => $targetStorage,
                'target_url' => $targetStorageObj->url($targetPath)
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }
}

2. 私有文件访问

php
<?php

namespace App\System\Admin;

use App\System\Service\Storage;
use Core\Resources\Action\Resources;

class PrivateFile extends Resources
{
    /**
     * 生成私有文件访问链接
     */
    public function generateAccessUrl(string $filePath, int $expireTime = 3600): array
    {
        try {
            // 检查用户权限
            $auth = request()->getAttribute('auth');
            if (!$this->checkFileAccess($auth['id'], $filePath)) {
                return error('无权限访问此文件');
            }

            // 生成签名 URL
            $signedUrl = Storage::localSign($filePath, $expireTime);

            return success('访问链接生成成功', [
                'url' => $signedUrl,
                'expire_time' => $expireTime,
                'expire_at' => date('Y-m-d H:i:s', time() + $expireTime)
            ]);

        } catch (\Exception $e) {
            return error('生成访问链接失败:' . $e->getMessage());
        }
    }

    /**
     * 下载私有文件
     */
    public function downloadFile(string $filePath): ResponseInterface
    {
        try {
            // 检查权限
            $auth = request()->getAttribute('auth');
            if (!$this->checkFileAccess($auth['id'], $filePath)) {
                throw new \Exception('无权限下载此文件');
            }

            // 获取文件
            $storage = Storage::getObject();
            
            if (!$storage->fileExists($filePath)) {
                throw new \Exception('文件不存在');
            }

            $content = $storage->read($filePath);
            $filename = basename($filePath);

            // 返回文件下载响应
            $response = response();
            $response->getBody()->write($content);
            
            return $response
                ->withHeader('Content-Type', 'application/octet-stream')
                ->withHeader('Content-Disposition', "attachment; filename=\"{$filename}\"")
                ->withHeader('Content-Length', strlen($content));

        } catch (\Exception $e) {
            return error('文件下载失败:' . $e->getMessage());
        }
    }

    /**
     * 检查文件访问权限
     */
    private function checkFileAccess(int $userId, string $filePath): bool
    {
        // 实现文件访问权限检查逻辑
        // 例如:检查文件所有者、部门权限、角色权限等
        return true;
    }
}

3. 文件管理服务

php
<?php

namespace App\System\Service;

use App\System\Service\Storage;

class FileManagerService
{
    /**
     * 获取目录文件列表
     */
    public static function listFiles(string $directory = '', string $storage = 'default'): array
    {
        try {
            $storageObj = Storage::getObject($storage);
            
            // 这里需要根据具体存储实现来获取文件列表
            // 不同存储后端的实现方式可能不同
            
            return [
                'success' => true,
                'directory' => $directory,
                'files' => []  // 实际实现中返回文件列表
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * 创建目录
     */
    public static function createDirectory(string $directory, string $storage = 'default'): array
    {
        try {
            $storageObj = Storage::getObject($storage);
            
            // 创建一个空的 .gitkeep 文件来创建目录
            $keepFile = rtrim($directory, '/') . '/.gitkeep';
            $storageObj->write($keepFile, '');

            return [
                'success' => true,
                'directory' => $directory,
                'message' => '目录创建成功'
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * 删除文件或目录
     */
    public static function deleteFile(string $path, string $storage = 'default'): array
    {
        try {
            $storageObj = Storage::getObject($storage);
            
            if (!$storageObj->fileExists($path)) {
                return [
                    'success' => false,
                    'message' => '文件不存在'
                ];
            }

            $storageObj->delete($path);

            return [
                'success' => true,
                'path' => $path,
                'message' => '文件删除成功'
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * 移动/重命名文件
     */
    public static function moveFile(string $sourcePath, string $targetPath, string $storage = 'default'): array
    {
        try {
            $storageObj = Storage::getObject($storage);
            
            if (!$storageObj->fileExists($sourcePath)) {
                return [
                    'success' => false,
                    'message' => '源文件不存在'
                ];
            }

            // 读取源文件内容
            $content = $storageObj->read($sourcePath);
            
            // 写入目标位置
            $storageObj->write($targetPath, $content);
            
            // 删除源文件
            $storageObj->delete($sourcePath);

            return [
                'success' => true,
                'source_path' => $sourcePath,
                'target_path' => $targetPath,
                'target_url' => $storageObj->url($targetPath),
                'message' => '文件移动成功'
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * 获取文件信息
     */
    public static function getFileInfo(string $path, string $storage = 'default'): array
    {
        try {
            $storageObj = Storage::getObject($storage);
            
            if (!$storageObj->fileExists($path)) {
                return [
                    'success' => false,
                    'message' => '文件不存在'
                ];
            }

            $info = [
                'path' => $path,
                'url' => $storageObj->url($path),
                'size' => $storageObj->fileSize($path),
                'last_modified' => $storageObj->lastModified($path),
                'extension' => pathinfo($path, PATHINFO_EXTENSION),
                'filename' => basename($path),
                'storage' => $storage
            ];

            return [
                'success' => true,
                'info' => $info
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }
}

💡 最佳实践

1. 存储选择策略

php
// ✅ 根据文件类型选择存储
public function getOptimalStorage(string $fileType, int $fileSize): string
{
    // 大文件使用云存储
    if ($fileSize > 10 * 1024 * 1024) { // 10MB
        return 'qiniu';
    }

    // 图片文件使用 CDN 存储
    if (in_array($fileType, ['jpg', 'png', 'gif', 'webp'])) {
        return 'oss';
    }

    // 私有文件使用本地存储
    if (in_array($fileType, ['pdf', 'doc', 'docx'])) {
        return 'local';
    }

    return 'default';
}

2. 错误处理

php
// ✅ 完善的错误处理
public function safeFileOperation(callable $operation): array
{
    try {
        return $operation();
    } catch (\League\Flysystem\FileNotFoundException $e) {
        return ['success' => false, 'message' => '文件不存在'];
    } catch (\League\Flysystem\FileExistsException $e) {
        return ['success' => false, 'message' => '文件已存在'];
    } catch (\Exception $e) {
        logger()->error('存储操作失败', [
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString()
        ]);
        return ['success' => false, 'message' => '存储操作失败'];
    }
}

3. 文件路径规范

php
// ✅ 标准化文件路径
public function normalizePath(string $path): string
{
    // 移除开头的斜杠
    $path = ltrim($path, '/');
    
    // 替换反斜杠为正斜杠
    $path = str_replace('\\', '/', $path);
    
    // 移除连续的斜杠
    $path = preg_replace('/\/+/', '/', $path);
    
    return $path;
}

// ✅ 生成安全的文件路径
public function generateSafePath(string $category, string $filename): string
{
    $extension = pathinfo($filename, PATHINFO_EXTENSION);
    $safeFilename = uniqid() . '.' . $extension;
    
    return $this->normalizePath("{$category}/" . date('Y/m/d') . "/{$safeFilename}");
}

4. 性能优化

php
// ✅ 批量操作优化
public function batchUpload(array $files): array
{
    $storage = Storage::getObject();
    $results = [];

    // 使用事务或批量操作(如果存储支持)
    foreach ($files as $file) {
        try {
            $result = $this->uploadSingleFile($storage, $file);
            $results[] = $result;
        } catch (\Exception $e) {
            $results[] = ['success' => false, 'error' => $e->getMessage()];
        }
    }

    return $results;
}

🎉 总结

存储服务的特点:

  • 🔧 多后端支持:支持本地、七牛、OSS、COS 等多种存储
  • 🛡️ 安全可靠:支持文件签名和权限控制
  • ⚡ 高性能:优化的文件操作和批量处理
  • 📊 功能完整:涵盖文件的增删改查操作
  • 🔗 易于集成:统一的 API 接口

通过合理使用存储服务,可以构建灵活可扩展的文件管理系统!