> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 文件存储

> 介绍如何使用 Laravel 集成的 Flysystem 进行文件读写、上传以及对接云存储。

## 什么是文件存储

Laravel 基于 [Flysystem](https://github.com/thephpleague/flysystem) 这一 PHP 扩展，提供了一层强大的文件系统抽象。
本地磁盘、SFTP、Amazon S3 等不同的存储后端都可以通过相同的 API 操作，因此在不同环境间切换时**无需修改代码**。

<Info>
  即便更换驱动，代码也不用改。开发用本地磁盘、生产用 S3 的组合可以轻松实现。
</Info>

## 配置

文件系统的配置集中在 `config/filesystems.php` 中。
你可以在这里定义“磁盘（disk）”，也就是一组“驱动 + 存储位置”的组合。

主要驱动如下：

| 驱动       | 用途                    |
| -------- | --------------------- |
| `local`  | 服务器本地文件系统             |
| `public` | 面向公网访问的本地磁盘           |
| `s3`     | Amazon S3（或兼容 S3 的服务） |
| `sftp`   | SFTP 服务器              |
| `ftp`    | FTP 服务器               |

### local 驱动

使用 `local` 驱动时，操作会以 `filesystems` 中配置的 `root` 目录为基准。
默认情况下 `root` 是 `storage/app/private`。

```php theme={null}
use Illuminate\Support\Facades\Storage;

Storage::disk('local')->put('example.txt', 'Contents');
// 会写入到 storage/app/private/example.txt
```

### 修改默认磁盘

可以通过 `FILESYSTEM_DISK` 环境变量切换默认磁盘。

```ini theme={null}
FILESYSTEM_DISK=s3
```

## public 磁盘与符号链接

`public` 磁盘用于存放需要通过 Web 访问的文件。
默认会保存到 `storage/app/public` 目录。

要让 Web 服务器可以访问，需要从 `public/storage` 建立到 `storage/app/public` 的符号链接。

<Steps>
  <Step title="创建符号链接">
    ```shell theme={null}
    php artisan storage:link
    ```
  </Step>

  <Step title="获取文件 URL">
    创建符号链接后，可以用 `asset` 辅助函数生成 URL。

    ```php theme={null}
    echo asset('storage/file.txt');
    ```
  </Step>
</Steps>

<Tip>
  如果需要更多符号链接，可以在 `config/filesystems.php` 的 `links` 数组中配置。

  ```php theme={null}
  'links' => [
      public_path('storage') => storage_path('app/public'),
      public_path('images') => storage_path('app/images'),
  ],
  ```
</Tip>

要删除符号链接，可以使用 `storage:unlink`。

```shell theme={null}
php artisan storage:unlink
```

## Storage Facade 的基本操作

### 读取文件

```php theme={null}
use Illuminate\Support\Facades\Storage;

// 以字符串形式获取文件内容
$contents = Storage::get('file.jpg');

// 读取并解析 JSON 文件
$data = Storage::json('orders.json');

// 判断文件是否存在
if (Storage::exists('file.jpg')) {
    // 文件存在
}

// 判断文件是否不存在
if (Storage::missing('file.jpg')) {
    // 文件不存在
}
```

### 写入文件

```php theme={null}
// 写入文件
Storage::put('file.jpg', $contents);

// 以流的方式写入资源
Storage::put('file.jpg', $resource);

// 在开头或末尾追加
Storage::prepend('file.log', 'Prepended Text');
Storage::append('file.log', 'Appended Text');

// 复制与移动
Storage::copy('old/file.jpg', 'new/file.jpg');
Storage::move('old/file.jpg', 'new/file.jpg');
```

<Info>
  `put` 失败时默认返回 `false`。
  可以在磁盘配置中设置 `'throw' => true` 来改为抛出异常。
</Info>

### 删除文件

```php theme={null}
// 删除单个文件
Storage::delete('file.jpg');

// 删除多个文件
Storage::delete(['file.jpg', 'file2.jpg']);

// 从指定磁盘删除文件
Storage::disk('s3')->delete('path/file.jpg');
```

### 文件下载响应

```php theme={null}
// 返回一个促使浏览器下载文件的响应
return Storage::download('file.jpg');

// 指定文件名与响应头
return Storage::download('file.jpg', 'my-file.jpg', $headers);
```

## 生成 URL

### 普通 URL

```php theme={null}
use Illuminate\Support\Facades\Storage;

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

`local` 驱动会返回类似 `/storage/file.jpg` 的相对 URL。
`s3` 驱动会返回完整的远程 URL。

### 临时 URL（Temporary URL）

要生成带有有效期的 URL，可以使用 `temporaryUrl` 方法。
`local` 与 `s3` 驱动都支持。

```php theme={null}
use Illuminate\Support\Facades\Storage;

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

对 S3，还可以附加额外的请求参数。

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

<Tip>
  需要临时上传 URL 时，可以使用 `temporaryUploadUrl`。
  在客户端直接向 S3 上传的无服务器架构中非常有用。

  ```php theme={null}
  ['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl(
      'file.jpg', now()->plus(minutes: 5)
  );
  ```
</Tip>

## 文件上传

以下是保存用户表单上传文件的常见模式。

### store 方法（自动生成文件名）

```php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserAvatarController extends Controller
{
    public function update(Request $request): string
    {
        // 会根据 MIME 类型自动推断扩展名并生成文件名
        $path = $request->file('avatar')->store('avatars');

        return $path;
    }
}
```

### storeAs 方法（指定文件名）

```php theme={null}
$path = $request->file('avatar')->storeAs(
    'avatars',
    $request->user()->id
);
```

### 指定磁盘上传

```php theme={null}
// 保存到 s3 磁盘
$path = $request->file('avatar')->store(
    'avatars/' . $request->user()->id,
    's3'
);
```

### 通过 Storage Facade 上传

```php theme={null}
use Illuminate\Support\Facades\Storage;

// 自动生成文件名
$path = Storage::putFile('avatars', $request->file('avatar'));

// 指定文件名
$path = Storage::putFileAs(
    'avatars',
    $request->file('avatar'),
    $request->user()->id
);
```

<Warning>
  `getClientOriginalName()` 与 `getClientOriginalExtension()` 依赖用户提供的信息，可能被伪造，因此并不安全。
  建议用 `hashName()` 生成文件名，用 `extension()` 判断扩展名。

  ```php theme={null}
  $file = $request->file('avatar');

  $name = $file->hashName();   // 生成唯一的随机文件名
  $extension = $file->extension(); // 根据 MIME 类型推断扩展名
  ```
</Warning>

## 文件可见性（Visibility）

Flysystem 通过 `visibility` 管理文件的公开/私有状态。

```php theme={null}
use Illuminate\Support\Facades\Storage;

// 写入时指定可见性
Storage::put('file.jpg', $contents, 'public');

// 获取和修改可见性
$visibility = Storage::getVisibility('file.jpg');
Storage::setVisibility('file.jpg', 'public');
```

上传的文件如果需要以公开状态保存，可以使用 `storePublicly`。

```php theme={null}
$path = $request->file('avatar')->storePublicly('avatars', 's3');
```

## 使用多个磁盘

通过 `disk` 方法可以切换要操作的磁盘。

```php theme={null}
// 保存到默认磁盘
Storage::put('avatars/1', $content);

// 保存到 s3 磁盘
Storage::disk('s3')->put('avatars/1', $content);

// 按需构建一个磁盘
$disk = Storage::build([
    'driver' => 'local',
    'root' => '/path/to/root',
]);
$disk->put('image.jpg', $content);
```

## 云存储（S3）配置

### 安装扩展包

```shell theme={null}
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
```

### 环境变量配置

在 `.env` 中配置 S3 的凭证信息。

```ini theme={null}
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
```

<Info>
  DigitalOcean Spaces、Cloudflare R2、Vultr Object Storage 等兼容 S3 的服务也可以使用 `s3` 驱动。
  在 `endpoint` 选项中指定服务的 endpoint URL 即可。

  ```php theme={null}
  'endpoint' => env('AWS_ENDPOINT', 'https://your-endpoint.example.com'),
  ```
</Info>

## 获取文件元数据

```php theme={null}
use Illuminate\Support\Facades\Storage;

// 文件大小（字节）
$size = Storage::size('file.jpg');

// 最后修改时间（UNIX 时间戳）
$time = Storage::lastModified('file.jpg');

// MIME 类型
$mime = Storage::mimeType('file.jpg');

// 文件的绝对路径（local 驱动）
$path = Storage::path('file.jpg');
```

## 目录操作

```php theme={null}
use Illuminate\Support\Facades\Storage;

// 目录下的文件列表
$files = Storage::files($directory);

// 包含子目录的文件列表
$files = Storage::allFiles($directory);

// 目录列表
$directories = Storage::directories($directory);

// 创建目录
Storage::makeDirectory($directory);

// 删除目录及其中的所有文件
Storage::deleteDirectory($directory);
```

## 测试

使用 `Storage::fake()` 可以在不触碰真实磁盘的情况下编写文件操作的测试。

```php theme={null}
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

test('可以上传头像', function () {
    Storage::fake('avatars');

    $file = UploadedFile::fake()->image('avatar.jpg');

    $this->post('/user/avatar', ['avatar' => $file]);

    // 确认文件已保存
    Storage::disk('avatars')->assertExists('avatar.jpg');

    // 确认文件未被保存
    Storage::disk('avatars')->assertMissing('other.jpg');
});
```

<Info>
  使用 `UploadedFile::fake()->image()` 需要 PHP 的 [GD 扩展](https://www.php.net/manual/zh/book.image.php)。
</Info>

## 实战示例：头像上传

下面是一个综合了校验、保存、路径写入数据库的实战控制器示例。

```php theme={null}
<?php

namespace App\Http\Controllers;

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

class ProfileController extends Controller
{
    public function updateAvatar(Request $request): RedirectResponse
    {
        $request->validate([
            'avatar' => ['required', 'image', 'max:2048'],
        ]);

        $user = $request->user();

        // 删除已有的头像
        if ($user->avatar_path) {
            Storage::disk('public')->delete($user->avatar_path);
        }

        // 保存新头像
        $path = $request->file('avatar')->store('avatars', 'public');

        // 将路径写入数据库
        $user->update(['avatar_path' => $path]);

        return back()->with('status', 'avatar-updated');
    }
}
```

在模板中通过 `Storage::url()` 获取 URL。

```blade theme={null}
<img src="{{ Storage::disk('public')->url($user->avatar_path) }}" alt="头像">
```


## Related topics

- [Laravel AI SDK](/zh/ai-sdk.md)
- [MongoDB](/zh/mongodb.md)
- [Laravel 8 升级到 9](/zh/blog/upgrade-8-to-9.md)
- [2026 年 5 月 Laravel 更新](/zh/blog/changelog/202605.md)
- [HTTP 请求](/zh/requests.md)
