文件存储

简介

Laravel 借助 Frank de Jonge 开发的 Flysystem PHP 包,提供强大的文件系统抽象。其 Flysystem 集成提供操作本地文件系统、SFTP 和 Amazon S3 的简洁驱动。各存储系统使用相同 API,因此在本地开发环境与生产服务器之间切换存储方式也很方便。

配置

文件系统配置位于 config/filesystems.php。在这里可以配置所有文件系统“磁盘”,每个磁盘代表一种存储驱动及存储位置。配置文件包含各受支持驱动的示例,你可以按存储需求和凭据修改。 local 驱动操作运行 Laravel 应用的服务器上的本地文件;sftp 驱动用于基于 SSH 密钥的文件传输;s3 驱动用于向 Amazon S3 云存储服务写入文件。

Note

可以配置任意数量的磁盘,也可以让多个磁盘使用同一种驱动。

本地驱动

使用 local 驱动时,所有文件操作都相对于 filesystems 配置中的 root 目录。默认目录是 storage/app/private,所以下例会写入 storage/app/private/example.txt:

use Illuminate\Support\Facades\Storage;

Storage::disk('local')->put('example.txt', 'Contents');

公开磁盘

应用 filesystems 配置中的 public 磁盘用于存储可公开访问的文件。默认情况下,它使用 local 驱动,文件保存在 storage/app/public。

如果 public 磁盘使用 local 驱动,并且需要通过网页访问文件,应建立从 public/storage 指向源目录 storage/app/public 的符号链接: 可以使用 Artisan 的 storage:link 命令创建链接:

php artisan storage:link

保存文件并创建符号链接后,可以用 asset 辅助函数生成文件 URL:

echo asset('storage/file.txt');

也可以在 filesystems 配置中定义其他符号链接。运行 storage:link 时会创建所有配置的链接:

'links' => [
    public_path('storage') => storage_path('app/public'),
    public_path('images') => storage_path('app/images'),
],

storage:unlink 命令可以移除已配置的符号链接:

php artisan storage:unlink

驱动的前置依赖

配置 S3 驱动

使用 S3 驱动前,需要通过 Composer 安装 Flysystem S3 包:

composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies

config/filesystems.php 已包含 S3 磁盘配置数组。通常通过下面这些由配置文件引用的环境变量设置 S3 信息和凭据:

AWS_ACCESS_KEY_ID=<your-key-id>
AWS_SECRET_ACCESS_KEY=<your-secret-access-key>
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=<your-bucket-name>
AWS_USE_PATH_STYLE_ENDPOINT=false

这些环境变量采用与 AWS CLI 相同的命名方式,便于使用。

配置 FTP 驱动

使用 FTP 驱动前,需要通过 Composer 安装 Flysystem FTP 包:

composer require league/flysystem-ftp "^3.0"

Laravel 的 Flysystem 集成支持 FTP,但框架默认的 config/filesystems.php 没有包含 FTP 示例配置。需要时可以使用下例:

'ftp' => [
    'driver' => 'ftp',
    'host' => env('FTP_HOST'),
    'username' => env('FTP_USERNAME'),
    'password' => env('FTP_PASSWORD'),
    // Optional FTP Settings...
    // 'port' => env('FTP_PORT', 21),
    // 'root' => env('FTP_ROOT'),
    // 'passive' => true,
    // 'ssl' => true,
    // 'timeout' => 30,
],

配置 SFTP 驱动

使用 SFTP 驱动前,需要通过 Composer 安装 Flysystem SFTP 包:

composer require league/flysystem-sftp-v3 "^3.0"

Laravel 的 Flysystem 集成支持 SFTP,但默认的 config/filesystems.php 没有 SFTP 示例配置。需要时可以参考下例:

'sftp' => [
    'driver' => 'sftp',
    'host' => env('SFTP_HOST'),

    // Settings for basic authentication...
    'username' => env('SFTP_USERNAME'),
    'password' => env('SFTP_PASSWORD'),
    // Settings for SSH key-based authentication with encryption password...
    'privateKey' => env('SFTP_PRIVATE_KEY'),
    'passphrase' => env('SFTP_PASSPHRASE'),

    // Settings for file / directory permissions...
    'visibility' => 'private', // `private` = 0600, `public` = 0644
    'directory_visibility' => 'private', // `private` = 0700, `public` = 0755
    // Optional SFTP Settings...
    // 'hostFingerprint' => env('SFTP_HOST_FINGERPRINT'),
    // 'maxTries' => 4,
    // 'passphrase' => env('SFTP_PASSPHRASE'),
    // 'port' => env('SFTP_PORT', 22),
    // 'root' => env('SFTP_ROOT', ''),
    // 'timeout' => 30,
    // 'useAgent' => true,
],

限定路径、只读及读穿文件系统

限定路径磁盘会自动为所有路径加上指定前缀。创建这类磁盘前,需要通过 Composer 安装额外的 Flysystem 包:

composer require league/flysystem-path-prefixing "^3.0"

通过定义使用 scoped 驱动的磁盘,可以为已有磁盘创建限定路径的实例。例如,为现有 s3 磁盘限定一个路径前缀后,所有文件操作都会使用这个前缀:

's3-videos' => [
    'driver' => 'scoped',
    'disk' => 's3',
    'prefix' => 'path/to/videos',
],

只读磁盘不允许执行写入操作。使用 read-only 配置项前,需要通过 Composer 安装额外的 Flysystem 包:

composer require league/flysystem-read-only "^3.0"

然后在一个或多个磁盘的配置数组中加入 read-only:

's3-videos' => [
    'driver' => 's3',
    // ...
    'read-only' => true,
],

读穿磁盘支持不停机迁移文件。读取时,Laravel 先检查主磁盘;如果文件只存在于后备磁盘,则从后备磁盘读取,并复制到主磁盘,供后续请求使用:

'assets' => [
    'driver' => 'read-through',
    'primary' => 's3',
    'fallback' => 'legacy-s3',
],

写入操作和目录列表针对主磁盘。存在性及元数据检查可使用任一磁盘,不会触发复制。复制后备文件到主磁盘失败时,默认仍能完成读取;如需抛出异常,可将 throw_on_promotion_failure 设为 true。

兼容 Amazon S3 的文件系统

默认的 filesystems 配置包含一个 s3 磁盘。 除了 Amazon S3,它也能用于兼容 S3 的服务,例如 RustFS、DigitalOcean Spaces、Vultr Object Storage、Cloudflare R2 和 Hetzner Cloud Storage。 通常将凭据修改为目标服务的凭据后,只需更新 endpoint 配置项。该值一般由 AWS_ENDPOINT 环境变量定义:

'endpoint' => env('AWS_ENDPOINT', 'https://rustfs:9000'),

获取磁盘实例

可以使用 Storage 门面操作任意已配置磁盘。例如,通过 put 在默认磁盘保存头像。如果调用门面方法前没有调用 disk,该操作会自动交给默认磁盘:

use Illuminate\Support\Facades\Storage;

Storage::put('avatars/1', $content);

如果应用使用多个磁盘,可以通过 Storage 门面的 disk 方法操作指定磁盘上的文件:

Storage::disk('s3')->put('avatars/1', $content);

按需创建磁盘

有时需要在运行时根据指定配置创建磁盘,而不将其写入 config/filesystems.php。为此可以向 Storage 的 build 方法传入配置数组:

use Illuminate\Support\Facades\Storage;

$disk = Storage::build([
    'driver' => 'local',
    'root' => '/path/to/root',
]);

$disk->put('image.jpg', $content);

读取文件

get 方法读取文件内容,返回原始字符串。所有文件路径都应相对于磁盘的 root:

$contents = Storage::get('file.jpg');

如果文件内容是 JSON,可以使用 json 读取并解码:

$orders = Storage::json('orders.json');

exists 判断磁盘上是否存在某个文件:

if (Storage::disk('s3')->exists('file.jpg')) {
    // ...
}

missing 判断磁盘上是否缺少某个文件:

if (Storage::disk('s3')->missing('file.jpg')) {
    // ...
}

下载文件

download 生成响应,强制浏览器下载指定路径的文件。第二个参数是下载时向用户展示的文件名,第三个参数可传入 HTTP 标头数组:

return Storage::download('file.jpg');

return Storage::download('file.jpg', $name, $headers);

文件 URL

url 获取指定文件的 URL。使用 local 驱动时,通常在路径前加上 /storage,返回相对 URL;使用 s3 驱动时,返回完整的远程 URL:

use Illuminate\Support\Facades\Storage;

$url = Storage::url('file.jpg');

使用 local 驱动时,需要公开访问的文件应放在 storage/app/public;同时在 public/storage 创建指向该目录的符号链接。

Warning

local 驱动的 url 返回值不会进行 URL 编码。因此建议始终使用能构成有效 URL 的文件名。

自定义 URL 主机

要修改 Storage 门面生成的 URL 主机,可以在磁盘配置数组中增加或修改 url:

'public' => [
    'driver' => 'local',
    'root' => storage_path('app/public'),
    'url' => env('APP_URL').'/storage',
    'visibility' => 'public',
    'throw' => false,
],

临时 URL

temporaryUrl 可以为 local 和 s3 驱动存储的文件创建临时 URL。参数为文件路径,以及表示到期时间的 DateTime 实例:

use Illuminate\Support\Facades\Storage;

$url = Storage::temporaryUrl(
    'file.jpg', now()->plus(minutes: 5)
);

启用本地临时 URL

如果应用开发开始于 local 驱动支持临时 URL 之前,可能需要手动启用。请在 config/filesystems.php 的 local 磁盘配置中加入 serve:

'local' => [
    'driver' => 'local',
    'root' => storage_path('app/private'),
    'serve' => true, // [tl! add]
    'throw' => false,
],

S3 请求参数

如需指定额外的 S3 请求参数,可以将参数数组作为 temporaryUrl 的第三个参数:

$url = Storage::temporaryUrl(
    'file.jpg',
    now()->plus(minutes: 5),
    [
        'ResponseContentType' => 'application/octet-stream',
        'ResponseContentDisposition' => 'attachment; filename=file2.jpg',
    ]
);

自定义临时 URL

通过 buildTemporaryUrlsUsing 可以自定义指定磁盘的临时 URL 生成方式。例如,某个控制器允许下载通常不支持临时 URL 的磁盘上的文件时,这个方法就很有用。通常在服务提供者的 boot 方法中调用:

<?php

namespace App\Providers;
use DateTime;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Storage::disk('local')->buildTemporaryUrlsUsing(
            function (string $path, DateTime $expiration, array $options) {
                return URL::temporarySignedRoute(
                    'files.download',
                    $expiration,
                    array_merge($options, ['path' => $path])
                );
            }
        );
    }
}

临时上传 URL

Warning

临时上传 URL 仅受 s3 和 local 驱动支持。 如需让客户端直接上传文件,可以用 temporaryUploadUrl 创建临时上传 URL。它接收路径和表示到期时间的 DateTime,返回关联数组,可以解构得到上传 URL 和上传请求应携带的标头:

use Illuminate\Support\Facades\Storage;
['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl(
    'file.jpg', now()->plus(minutes: 5)
);

这个方法主要适合无服务器环境,让客户端应用直接向 Amazon S3 等云存储系统上传文件。

文件元数据

除了读写内容,Laravel 也能提供文件本身的信息。例如,size 返回文件大小,单位为字节:

use Illuminate\Support\Facades\Storage;

$size = Storage::size('file.jpg');

lastModified 返回文件最后修改时间的 UNIX 时间戳:

$time = Storage::lastModified('file.jpg');

mimeType 返回文件的 MIME 类型:

$mime = Storage::mimeType('file.jpg');

文件路径

path 获取指定文件的路径。使用 local 驱动时返回绝对路径;使用 s3 驱动时返回相对于 S3 存储桶的路径:

use Illuminate\Support\Facades\Storage;

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

保存文件

put 可以向磁盘保存文件内容。也可以传入 PHP resource,利用 Flysystem 的底层流支持。所有路径都应相对于磁盘配置的 root:

use Illuminate\Support\Facades\Storage;

Storage::put('file.jpg', $contents);

Storage::put('file.jpg', $resource);

写入失败

如果 put 或其他写操作无法将文件写入磁盘,会返回 false:

if (! Storage::put('file.jpg', $contents)) {
    // The file could not be written to disk...
}

可以在磁盘配置数组中定义 throw。将其设为 true 后,put 等写方法在写入失败时抛出 League\Flysystem\UnableToWriteFile:

'public' => [
    'driver' => 'local',
    // ...
    'throw' => true,
],

另一种选择是在磁盘配置中定义 report。将其设为 true 后,写入失败时,Laravel 会通过应用的异常处理器记录底层异常,但不抛出异常,也不改变写操作的返回值:

'public' => [
    'driver' => 'local',
    // ...
    'report' => true,
],

如果没有定义 throw 或 report,写入失败时磁盘会静默返回 false,底层异常既不会抛出,也不会记录。

在文件开头或末尾写入

prepend 和 append 分别在文件开头和末尾写入内容:

Storage::prepend('file.log', 'Prepended Text');
Storage::append('file.log', 'Appended Text');

复制与移动文件

copy 将已有文件复制到磁盘上的新位置;move 用于重命名文件或将其移到新位置:

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

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

copyToDisk 和 moveToDisk 可以将文件复制或移动到另一磁盘。默认沿用源路径;第三个参数可指定目标路径:

Storage::disk('local')->copyToDisk('s3', 'reports/report.csv');

Storage::disk('local')->moveToDisk(
    's3', 'reports/report.csv', 'archive/report.csv'
);

自动流式传输

将文件以流的形式写入存储可以明显减少内存占用。若希望 Laravel 自动管理传输,可以使用 putFile 或 putFileAs。它们接受 Illuminate\Http\File 或 Illuminate\Http\UploadedFile 实例,将文件流式写入目标位置:

use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
// Automatically generate a unique ID for filename...
$path = Storage::putFile('photos', new File('/path/to/photo'));

// Manually specify a filename...
$path = Storage::putFileAs('photos', new File('/path/to/photo'), 'photo.jpg');

putFile 的几个要点:这里仅指定目录,没有指定文件名。默认会生成唯一 ID 作为文件名,根据 MIME 类型确定扩展名,并返回包含生成文件名的路径,方便保存到数据库。 putFile 和 putFileAs 还可接收参数来指定文件“可见性”。当文件保存在 Amazon S3 等云磁盘,并且需要通过生成的 URL 公开访问时,这很有用:

Storage::putFile('photos', new File('/path/to/photo'), 'public');

文件上传

网页应用中,文件存储最常见的用途之一是保存用户上传的照片和文档。Laravel 在上传文件实例上提供 store 方法,调用时传入希望保存文件的路径即可:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
class UserAvatarController extends Controller
{
    /**
     * Update the avatar for the user.
     */
    public function update(Request $request): string
    {
        $path = $request->file('avatar')->store('avatars');

        return $path;
    }
}

这里仅指定目录,没有指定文件名。默认的 store 会生成唯一 ID 作为文件名,根据 MIME 类型确定扩展名,并返回包含文件名的路径,方便保存到数据库。 也可以调用 Storage 门面的 putFile,完成与上例相同的存储操作:

$path = Storage::putFile('avatars', $request->file('avatar'));

指定文件名

如果不希望自动分配文件名,可以使用 storeAs;它依次接受路径、文件名和可选磁盘参数:

$path = $request->file('avatar')->storeAs(
    'avatars', $request->user()->id
);

也可以使用 Storage 门面的 putFileAs 完成同样的操作:

$path = Storage::putFileAs(
    'avatars', $request->file('avatar'), $request->user()->id
);

Warning

文件路径中的不可打印字符与无效 Unicode 字符会自动移除。因此,传入 Laravel 文件存储方法前,可以先清理路径。路径通过 League\Flysystem\WhitespacePathNormalizer::normalizePath 进行规范化。

指定磁盘

默认情况下,上传文件的 store 使用默认磁盘。要指定其他磁盘,将其名称作为第二个参数传入:

$path = $request->file('avatar')->store(
    'avatars/'.$request->user()->id, 's3'
);

使用 storeAs 时,磁盘名称可以作为第三个参数传入:

$path = $request->file('avatar')->storeAs(
    'avatars',
    $request->user()->id,
    's3'
);

上传文件的其他信息

通过 getClientOriginalName 和 getClientOriginalExtension 可以获取上传文件的原始名称和扩展名:

$file = $request->file('avatar');

$name = $file->getClientOriginalName();
$extension = $file->getClientOriginalExtension();

但这两个方法不安全:恶意用户可能篡改文件名和扩展名。因此通常应优先使用 hashName 和 extension 获取上传文件的名称及扩展名:

$file = $request->file('avatar');
$name = $file->hashName(); // Generate a unique, random name...
$extension = $file->extension(); // Determine the file's extension based on the file's MIME type...

文件可见性

在 Laravel 的 Flysystem 集成中,可见性是对不同平台文件权限的抽象。文件可声明为 public 或 private;public 表示通常允许其他人访问。例如,使用 S3 驱动时,可以获取公开文件的 URL。

通过 put 写入时可以设置可见性:

use Illuminate\Support\Facades\Storage;
Storage::put('file.jpg', $contents, 'public');

文件保存后,可以使用 getVisibility 和 setVisibility 获取或修改可见性:

$visibility = Storage::getVisibility('file.jpg');

Storage::setVisibility('file.jpg', 'public');

对于上传文件,可以通过 storePublicly 和 storePubliclyAs 以 public 可见性保存:

$path = $request->file('avatar')->storePublicly('avatars', 's3');

$path = $request->file('avatar')->storePubliclyAs(
    'avatars',
    $request->user()->id,
    's3'
);

图像处理

如需在保存之前调整上传图像大小、裁剪或转换格式,可以使用 Laravel 的图像处理功能:

$path = $request->image('avatar')
    ->cover(400, 400)
    ->toWebp()
    ->storePublicly('avatars', 'public');

也可以从已有磁盘文件创建图像实例:

$image = Storage::disk('public')->image('avatars/photo.jpg');

本地文件与可见性

使用 local 驱动时,public 可见性对应目录的 0755 权限与文件的 0644 权限。可以在 filesystems 配置中修改映射:

'local' => [
    'driver' => 'local',
    'root' => storage_path('app'),
    'permissions' => [
        'file' => [
            'public' => 0644,
            'private' => 0600,
        ],
        'dir' => [
            'public' => 0755,
            'private' => 0700,
        ],
    ],
    'throw' => false,
],

删除文件

delete 接受一个文件名或文件数组:

use Illuminate\Support\Facades\Storage;
Storage::delete('file.jpg');

Storage::delete(['file.jpg', 'file2.jpg']);

必要时可以指定删除操作使用的磁盘:

use Illuminate\Support\Facades\Storage;

Storage::disk('s3')->delete('path/file.jpg');

目录

获取目录内的所有文件

files 返回指定目录内的文件数组。要包括子目录中的文件,使用 allFiles:

use Illuminate\Support\Facades\Storage;

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

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

获取目录内的所有子目录

directories 返回指定目录内的子目录数组。要递归包含更深层的目录,使用 allDirectories:

$directories = Storage::directories($directory);

$directories = Storage::allDirectories($directory);

创建目录

makeDirectory 创建指定目录及所需的父目录:

Storage::makeDirectory($directory);

删除目录

deleteDirectory 移除指定目录及其中全部文件:

Storage::deleteDirectory($directory);

测试

Storage 门面的 fake 可以轻松生成虚拟磁盘。结合 Illuminate\Http\UploadedFile 的文件生成工具,可以大幅简化上传测试。例如:

<?php

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

test('albums can be uploaded', function () {
    Storage::fake('photos');
    $response = $this->json('POST', '/photos', [
        UploadedFile::fake()->image('photo1.jpg'),
        UploadedFile::fake()->image('photo2.jpg')
    ]);

    // Assert one or more files were stored...
    Storage::disk('photos')->assertExists('photo1.jpg');
    Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);
    // Assert one or more files were not stored...
    Storage::disk('photos')->assertMissing('missing.jpg');
    Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);

    // Assert that the number of files in a given directory matches the expected count...
    Storage::disk('photos')->assertCount('/wallpapers', 2);

    // Assert that a given directory is empty...
    Storage::disk('photos')->assertDirectoryEmpty('/wallpapers');
    // Assert that the disk contains no files...
    Storage::disk('photos')->assertEmpty();
});
<?php

namespace Tests\Feature;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_albums_can_be_uploaded(): void
    {
        Storage::fake('photos');
        $response = $this->json('POST', '/photos', [
            UploadedFile::fake()->image('photo1.jpg'),
            UploadedFile::fake()->image('photo2.jpg')
        ]);

        // Assert one or more files were stored...
        Storage::disk('photos')->assertExists('photo1.jpg');
        Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);
        // Assert one or more files were not stored...
        Storage::disk('photos')->assertMissing('missing.jpg');
        Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);

        // Assert that the number of files in a given directory matches the expected count...
        Storage::disk('photos')->assertCount('/wallpapers', 2);

        // Assert that a given directory is empty...
        Storage::disk('photos')->assertDirectoryEmpty('/wallpapers');
        // Assert that the disk contains no files...
        Storage::disk('photos')->assertEmpty();
    }
}

默认的 fake 会删除临时目录中的所有文件。如需保留,可改用 persistentFake。更多上传测试信息见 HTTP 测试文档。

Warning

image 方法需要 GD 扩展。

自定义文件系统

Laravel 的 Flysystem 集成内置了多种驱动,但 Flysystem 还为其他存储系统提供适配器。要在 Laravel 中使用这些适配器,可以创建自定义驱动。 定义自定义文件系统需要一个 Flysystem 适配器。先向项目加入社区维护的 Dropbox 适配器:

composer require spatie/flysystem-dropbox

接着,在应用服务提供者的 boot 方法中使用 Storage 门面的 extend 注册驱动:

<?php

namespace App\Providers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use League\Flysystem\Filesystem;
use Spatie\Dropbox\Client as DropboxClient;
use Spatie\FlysystemDropbox\DropboxAdapter;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Storage::extend('dropbox', function (Application $app, array $config) {
            $adapter = new DropboxAdapter(new DropboxClient(
                $config['authorization_token']
            ));

            return new FilesystemAdapter(
                new Filesystem($adapter, $config),
                $adapter,
                $config
            );
        });
    }
}

extend 的第一个参数是驱动名称,第二个参数是接收 $app 和 $config 的闭包。闭包必须返回 Illuminate\Filesystem\FilesystemAdapter 实例;$config 包含 config/filesystems.php 中该磁盘的配置。

创建并注册该扩展的服务提供者后,就可以在 config/filesystems.php 中使用 dropbox 驱动。


原文:File Storage,Laravel 文档贡献者。文档源自 laravel/docs 13.x,依据该文档仓库的 MIT 许可证将正文译为中文,保留代码。Copyright (c) Taylor Otwell。完整版权及许可声明见随附 LICENSE-source.txt。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容