> ## 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 提供強大的檔案系統抽象層，底層基於 PHP 套件 [Flysystem](https://github.com/thephpleague/flysystem)。
它讓你能以相同的 API 操作本機磁碟、SFTP、Amazon S3 等各種不同的儲存後端，因此可依環境切換而**不必修改程式碼**。

<Info>
  即使切換驅動，程式碼也無須改變。開發環境用本機、正式環境用 S3 這種配置能輕鬆實現。
</Info>

## 設定

檔案系統的設定集中在 `config/filesystems.php`。
你可以在此定義「磁碟」。磁碟是特定驅動與儲存位置的組合。

主要驅動如下：

| 驅動       | 用途                   |
| -------- | -------------------- |
| `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);

// 以串流方式寫入 resource
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` 選項指定服務的端點 URL。

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

## 取得檔案 meta

```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/en/book.image.php)。
</Info>

## 實戰範例：上傳個人頭像

以下整合驗證、儲存與寫回 DB 的實戰控制器範例。

```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');

        // 將路徑寫入 DB
        $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

- [HTTP Request](/zh-TW/requests.md)
- [Laravel AI SDK](/zh-TW/ai-sdk.md)
- [MongoDB](/zh-TW/mongodb.md)
- [圖片加工](/zh-TW/images.md)
- [Laravel Boost Custom Agent for PhpStorm with GitHub Copilot](/zh-TW/packages/laravel-boost-phpstorm-copilot.md)
