Laravel 13 文件存储高级特性深度解析

文件存储是 Laravel 处理文件上传、存储和管理的强大工具。本文将深入探讨 Laravel 13 中文件存储的高级特性。

文件存储基础回顾

基本操作

1
2
3
4
5
6
7
8
9
<?php

use Illuminate\Support\Facades\Storage;

Storage::put('file.txt', 'content');
Storage::get('file.txt');
Storage::exists('file.txt');
Storage::delete('file.txt');
Storage::url('file.txt');

多磁盘支持

配置磁盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
],

'ftp' => [
'driver' => 'ftp',
'host' => env('FTP_HOST'),
'username' => env('FTP_USERNAME'),
'password' => env('FTP_PASSWORD'),
],
],
];

使用不同磁盘

1
2
3
4
5
6
7
<?php

use Illuminate\Support\Facades\Storage;

Storage::disk('local')->put('file.txt', 'content');
Storage::disk('s3')->put('file.txt', 'content');
Storage::disk('public')->url('file.txt');

文件上传

基本上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
public function upload(Request $request)
{
$request->validate([
'file' => 'required|file|max:10240',
]);

$path = $request->file('file')->store('uploads');

return response()->json([
'path' => $path,
'url' => Storage::url($path),
]);
}

public function uploadToS3(Request $request)
{
$path = $request->file('file')->store('uploads', 's3');

return response()->json([
'path' => $path,
'url' => Storage::disk('s3')->url($path),
]);
}
}

自定义文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class FileController extends Controller
{
public function uploadWithCustomName(Request $request)
{
$file = $request->file('file');
$extension = $file->getClientOriginalExtension();
$filename = Str::uuid() . '.' . $extension;

$path = $file->storeAs('uploads', $filename);

return response()->json([
'path' => $path,
'filename' => $filename,
]);
}

public function uploadWithOriginalName(Request $request)
{
$file = $request->file('file');
$originalName = $file->getClientOriginalName();
$safeName = $this->sanitizeFilename($originalName);

$path = $file->storeAs('uploads', $safeName);

return response()->json(['path' => $path]);
}

protected function sanitizeFilename(string $filename): string
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$name = pathinfo($filename, PATHINFO_FILENAME);

$safeName = Str::slug($name);
$uniqueName = $safeName . '-' . Str::random(8);

return $uniqueName . '.' . $extension;
}
}

文件可见性

设置可见性

1
2
3
4
5
6
7
8
9
<?php

use Illuminate\Support\Facades\Storage;

Storage::put('file.txt', 'content', 'public');
Storage::put('file.txt', 'content', 'private');

Storage::setVisibility('file.txt', 'public');
Storage::getVisibility('file.txt');

预签名 URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

use Illuminate\Support\Facades\Storage;

$url = Storage::temporaryUrl('file.pdf', now()->addMinutes(5));

$url = Storage::disk('s3')->temporaryUrl(
'file.pdf',
now()->addMinutes(30),
[
'ResponseContentType' => 'application/pdf',
'ResponseContentDisposition' => 'attachment; filename="document.pdf"',
]
);

目录操作

创建目录

1
2
3
4
5
6
<?php

use Illuminate\Support\Facades\Storage;

Storage::makeDirectory('photos/2024');
Storage::makeDirectory('photos/2024', 0755, true);

列出文件

1
2
3
4
5
6
7
8
9
<?php

use Illuminate\Support\Facades\Storage;

$files = Storage::files('photos');
$allFiles = Storage::allFiles('photos');

$directories = Storage::directories('photos');
$allDirectories = Storage::allDirectories('photos');

删除目录

1
2
3
4
5
<?php

use Illuminate\Support\Facades\Storage;

Storage::deleteDirectory('photos/2024');

文件元数据

获取文件信息

1
2
3
4
5
6
7
8
9
<?php

use Illuminate\Support\Facades\Storage;

$size = Storage::size('file.txt');
$mimeType = Storage::mimeType('file.txt');
$lastModified = Storage::lastModified('file.txt');

$path = Storage::path('file.txt');

自定义元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

namespace App\Services;

use Illuminate\Support\Facades\Storage;

class FileMetadataService
{
public function getMetadata(string $path): array
{
return [
'path' => $path,
'size' => Storage::size($path),
'mime_type' => Storage::mimeType($path),
'last_modified' => Storage::lastModified($path),
'url' => Storage::url($path),
'exists' => Storage::exists($path),
];
}

public function getDetailedMetadata(string $path): array
{
$metadata = $this->getMetadata($path);

if (str_starts_with($metadata['mime_type'], 'image/')) {
$imageInfo = $this->getImageInfo($path);
$metadata = array_merge($metadata, $imageInfo);
}

return $metadata;
}

protected function getImageInfo(string $path): array
{
$fullPath = Storage::path($path);
$info = getimagesize($fullPath);

return [
'width' => $info[0] ?? null,
'height' => $info[1] ?? null,
'type' => $info[2] ?? null,
'aspect_ratio' => ($info[0] ?? 0) / ($info[1] ?? 1),
];
}
}

文件复制和移动

复制文件

1
2
3
4
5
6
7
8
9
<?php

use Illuminate\Support\Facades\Storage;

Storage::copy('old/file.txt', 'new/file.txt');

Storage::disk('local')->copy('file.txt', 'backup/file.txt');

Storage::disk('s3')->copy('file.txt', 'archive/file.txt');

移动文件

1
2
3
4
5
6
7
<?php

use Illuminate\Support\Facades\Storage;

Storage::move('old/file.txt', 'new/file.txt');

Storage::disk('local')->move('file.txt', 'archive/file.txt');

跨磁盘操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace App\Services;

use Illuminate\Support\Facades\Storage;

class FileTransferService
{
public function transferBetweenDisks(
string $sourceDisk,
string $sourcePath,
string $targetDisk,
string $targetPath
): bool {
$content = Storage::disk($sourceDisk)->get($sourcePath);

return Storage::disk($targetDisk)->put($targetPath, $content);
}

public function moveToS3(string $localPath, string $s3Path): bool
{
$content = Storage::disk('local')->get($localPath);

$success = Storage::disk('s3')->put($s3Path, $content);

if ($success) {
Storage::disk('local')->delete($localPath);
}

return $success;
}
}

流式处理

流式上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class StreamUploadController extends Controller
{
public function upload(Request $request)
{
$file = $request->file('file');

$stream = fopen($file->getRealPath(), 'r');

Storage::disk('s3')->put(
'uploads/' . $file->getClientOriginalName(),
$stream
);

fclose($stream);

return response()->json(['success' => true]);
}
}

流式下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Storage;

class StreamDownloadController extends Controller
{
public function download(string $path)
{
$stream = Storage::readStream($path);

return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => Storage::mimeType($path),
'Content-Length' => Storage::size($path),
]);
}

public function downloadFromS3(string $path)
{
return Storage::disk('s3')->download($path);
}
}

自定义文件系统

自定义驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Filesystem;

use Illuminate\Filesystem\FilesystemAdapter;
use League\Flysystem\FilesystemAdapter as FlysystemAdapter;

class CustomFilesystemAdapter extends FilesystemAdapter
{
public function __construct(FlysystemAdapter $adapter, array $config = [])
{
parent::__construct($adapter, $config);
}

public function getUrl(string $path): string
{
return $this->config['url'] . '/' . $path;
}

public function getTemporaryUrl(string $path, \DateTimeInterface $expiration, array $options = []): string
{
return $this->adapter->getTemporaryUrl($path, $expiration, $options);
}
}

注册自定义驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Storage;
use App\Filesystem\CustomFilesystemAdapter;

class FilesystemServiceProvider extends ServiceProvider
{
public function boot(): void
{
Storage::extend('custom', function ($app, $config) {
$adapter = new CustomFilesystemAdapter(
new \App\Filesystem\CustomAdapter($config),
$config
);

return $adapter;
});
}
}

文件处理服务

图片处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php

namespace App\Services;

use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;

class ImageProcessingService
{
public function resize(
string $path,
int $width,
int $height,
?string $destination = null
): string {
$image = Image::make(Storage::path($path));

$image->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});

$destinationPath = $destination ?? $this->getResizedPath($path, $width, $height);

Storage::put($destinationPath, $image->stream());

return $destinationPath;
}

public function createThumbnail(string $path, int $size = 200): string
{
return $this->resize($path, $size, $size, $this->getThumbnailPath($path));
}

public function watermark(string $path, string $watermarkPath): string
{
$image = Image::make(Storage::path($path));
$watermark = Image::make(Storage::path($watermarkPath));

$image->insert($watermark, 'bottom-right', 10, 10);

Storage::put($path, $image->stream());

return $path;
}

protected function getResizedPath(string $path, int $width, int $height): string
{
$info = pathinfo($path);

return sprintf(
'%s/%s_%dx%d.%s',
$info['dirname'],
$info['filename'],
$width,
$height,
$info['extension']
);
}

protected function getThumbnailPath(string $path): string
{
$info = pathinfo($path);

return sprintf(
'%s/thumbnails/%s.%s',
$info['dirname'],
$info['filename'],
$info['extension']
);
}
}

文件压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

namespace App\Services;

use Illuminate\Support\Facades\Storage;
use ZipArchive;

class FileCompressionService
{
public function compressDirectory(string $directory, string $zipPath): bool
{
$zip = new ZipArchive();
$fullZipPath = Storage::path($zipPath);

if ($zip->open($fullZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
return false;
}

$files = Storage::allFiles($directory);

foreach ($files as $file) {
$zip->addFile(
Storage::path($file),
str_replace($directory . '/', '', $file)
);
}

$zip->close();

return true;
}

public function extractZip(string $zipPath, string $destination): bool
{
$zip = new ZipArchive();
$fullZipPath = Storage::path($zipPath);

if ($zip->open($fullZipPath) !== true) {
return false;
}

$fullDestination = Storage::path($destination);

$zip->extractTo($fullDestination);
$zip->close();

return true;
}
}

测试文件存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

namespace Tests\Unit\Services;

use Tests\TestCase;
use Illuminate\Support\Facades\Storage;
use App\Services\FileMetadataService;

class StorageTest extends TestCase
{
public function test_file_upload(): void
{
Storage::fake('local');

Storage::disk('local')->put('file.txt', 'content');

Storage::disk('local')->assertExists('file.txt');
}

public function test_file_deletion(): void
{
Storage::fake('local');

Storage::disk('local')->put('file.txt', 'content');
Storage::disk('local')->delete('file.txt');

Storage::disk('local')->assertMissing('file.txt');
}

public function test_file_metadata(): void
{
Storage::fake('local');

Storage::disk('local')->put('file.txt', 'content');

$service = new FileMetadataService();
$metadata = $service->getMetadata('file.txt');

$this->assertEquals(7, $metadata['size']);
$this->assertTrue($metadata['exists']);
}
}

最佳实践

1. 使用适当的磁盘

1
2
3
4
5
<?php

Storage::disk('local')->put('temp/file.txt', $content);
Storage::disk('s3')->put('public/file.txt', $content);
Storage::disk('public')->put('avatar.jpg', $image);

2. 清理临时文件

1
2
3
<?php

Storage::deleteDirectory('temp/' . $sessionId);

3. 验证文件类型

1
2
3
4
5
<?php

$validated = $request->validate([
'file' => 'required|file|mimes:pdf,doc,docx|max:10240',
]);

总结

Laravel 13 的文件存储系统提供了强大的文件管理功能。通过合理使用多磁盘、流式处理、自定义驱动等高级特性,可以构建灵活、可扩展的文件管理系统。