Filesystem:本地、S3 与 Cloudflare R2
High Contrast
Dark Mode
Light Mode
Sepia
Forest
1 min read149 words

Filesystem:本地、S3 与 Cloudflare R2

Laravel 的文件系统抽象层(Filesystem)让代码对存储后端无感——本地开发用本地磁盘,生产环境用 S3 或 R2,切换只需改一行配置。


配置 Disk

// config/filesystems.php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root'   => storage_path('app'),
],
'public' => [
'driver'     => 'local',
'root'       => storage_path('app/public'),
'url'        => env('APP_URL').'/storage',
'visibility' => 'public',
],
's3' => [
'driver'   => 's3',
'key'      => env('AWS_ACCESS_KEY_ID'),
'secret'   => env('AWS_SECRET_ACCESS_KEY'),
'region'   => env('AWS_DEFAULT_REGION', 'ap-southeast-1'),
'bucket'   => env('AWS_BUCKET'),
'url'      => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),  // R2 用这个设置自定义端点
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
],
// Cloudflare R2(兼容 S3 API,但更便宜)
'r2' => [
'driver'                  => 's3',
'key'                     => env('R2_ACCESS_KEY_ID'),
'secret'                  => env('R2_SECRET_ACCESS_KEY'),
'region'                  => 'auto',
'bucket'                  => env('R2_BUCKET'),
'endpoint'                => env('R2_ENDPOINT'),  // https://xxx.r2.cloudflarestorage.com
'use_path_style_endpoint' => true,
],
],
];
# .env(生产 S3)
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx
AWS_DEFAULT_REGION=ap-southeast-1
AWS_BUCKET=taskflow-uploads
# .env(Cloudflare R2)
FILESYSTEM_DISK=r2
R2_ACCESS_KEY_ID=xxx
R2_SECRET_ACCESS_KEY=xxx
R2_BUCKET=taskflow-uploads
R2_ENDPOINT=https://accountid.r2.cloudflarestorage.com

文件操作速查

use Illuminate\Support\Facades\Storage;
// 存储文件
Storage::put('tasks/42/plan.txt', '内容文本');
Storage::put('tasks/42/photo.jpg', file_get_contents($path));
$path = Storage::putFile('tasks/42', $uploadedFile);  // 自动生成文件名
$path = Storage::putFileAs('tasks/42', $uploadedFile, 'photo.jpg');  // 自定义文件名
// 读取
$content = Storage::get('tasks/42/plan.txt');
$exists  = Storage::exists('tasks/42/plan.txt');
$size    = Storage::size('tasks/42/plan.txt');
// URL(公共文件)
$url = Storage::url('tasks/42/photo.jpg');
// 预签名 URL(私有文件,临时访问)
$temporaryUrl = Storage::temporaryUrl('tasks/42/private.pdf', now()->addMinutes(30));
// 删除
Storage::delete('tasks/42/plan.txt');
Storage::delete(['tasks/42/a.txt', 'tasks/42/b.txt']);
// 切换 Disk
Storage::disk('local')->put(...);
Storage::disk('s3')->get(...);

文件上传完整流程

// app/Http/Controllers/Api/V1/TaskAttachmentController.php
class TaskAttachmentController extends Controller
{
public function store(Request $request, Task $task)
{
$this->authorize('update', $task);
$request->validate([
'file' => [
'required',
'file',
'max:10240',  // 10MB
'mimes:pdf,docx,xlsx,jpg,jpeg,png,webp,gif',
],
]);
$file = $request->file('file');
// 文件路径:tasks/{task_id}/{uuid}.{ext}
$path = $file->store(
"tasks/{$task->id}",
['disk' => 's3', 'visibility' => 'private']
);
// 记录到数据库
$attachment = $task->attachments()->create([
'filename'      => $file->getClientOriginalName(),
'disk'          => 's3',
'path'          => $path,
'mime_type'     => $file->getMimeType(),
'size_bytes'    => $file->getSize(),
'uploaded_by'   => auth()->id(),
]);
return response()->json([
'data' => [
'id'           => $attachment->id,
'filename'     => $attachment->filename,
'size'         => $attachment->size_bytes,
'download_url' => Storage::disk('s3')
->temporaryUrl($path, now()->addMinutes(60)),
],
], 201);
}
public function download(TaskAttachment $attachment)
{
$this->authorize('view', $attachment->task);
// 生成 30 分钟有效的临时下载 URL
$url = Storage::disk($attachment->disk)
->temporaryUrl($attachment->path, now()->addMinutes(30));
return redirect($url);
}
public function destroy(TaskAttachment $attachment)
{
$this->authorize('delete', $attachment->task);
Storage::disk($attachment->disk)->delete($attachment->path);
$attachment->delete();
return response()->noContent();
}
}

图片处理(Intervention Image)

composer require intervention/image-laravel
use Intervention\Image\Laravel\Facades\Image;
// 用户头像:上传时压缩 + 生成缩略图
class UploadAvatarController extends Controller
{
public function __invoke(Request $request)
{
$request->validate(['avatar' => ['required', 'image', 'max:5120']]);
$file = $request->file('avatar');
// 处理图片:裁剪为正方形 + 压缩
$image = Image::read($file)
->cover(400, 400)   // 裁剪为 400x400 正方形
->toWebp(quality: 85);  // 转 WebP,质量 85%
$path = "avatars/{$request->user()->id}.webp";
Storage::disk('s3')->put($path, $image, 'public');
$request->user()->update([
'avatar' => Storage::disk('s3')->url($path),
]);
return response()->json(['avatar_url' => Storage::disk('s3')->url($path)]);
}
}

下一节Mailable 与 Markdown 邮件模板——文件上传好了,下一步是邮件系统:任务截止提醒、团队邀请、每周摘要——如何用 Mailable 和 Markdown 模板优雅地发送这些邮件。