# S3

<a id="s3-stream-wrapper"></a>

## S3 ストリームラッパー

S3 ストリームラッパーを使うと、PHP のファイル関数 (`file_get_contents()`、`file_put_contents()`、`fopen()`、
`is_file()`、`scandir()`、...) で `s3://bucket/key` パスを使えます。これは pieceofcake2/aws が登録する
AWS SDK for PHP のストリームラッパー (`Aws\S3\StreamWrapper`) を AsyncAws に移植したもので、
対応範囲と制限も同じです。`async-aws/simple-s3` が必要です。

`'bootstrap' => true` でプラグインを読み込むと、`AsyncAws` 設定 (他の機能と同様に `Aws` 設定も使えます) で
ストリームラッパーが登録されます。自分で登録することもできます。

```php
App::uses('S3StreamWrapper', 'AsyncAws.Lib');

// With the AsyncAws configuration
S3StreamWrapper::bootstrap();

// Or with a client
S3StreamWrapper::register($this->AsyncAws->createSimpleS3(['region' => 'us-east-1']));
```

`async-aws/simple-s3` がインストールされていない場合、ブートストラップは警告をログに記録し、
ストリームラッパーを登録しません。登録したら、次のように使えます。

```php
file_put_contents('s3://my-bucket/reports/today.csv', $csv);
echo file_get_contents('s3://my-bucket/reports/today.csv');

if (is_file('s3://my-bucket/reports/today.csv')) {
    echo filesize('s3://my-bucket/reports/today.csv');
}

// "today.csv", ...
foreach (scandir('s3://my-bucket/reports') as $name) {
    echo $name;
}
```

S3 オペレーションのオプションは、ストリームコンテキストで渡せます。

```php
$context = stream_context_create([
    's3' => [
        'ACL' => 'private',
        'ContentType' => 'text/csv',
        'CacheControl' => 'max-age=3600',
        'Metadata' => ['source' => 'reports'],
        'ServerSideEncryption' => 'AES256',
    ],
]);
file_put_contents('s3://my-bucket/reports/today.csv', $csv, 0, $context);
```

オプションは AsyncAws のオペレーション (PutObject、GetObject、CopyObject、...) に渡され、
オペレーションは知っているオプションを使います。たとえば、読み込みでは `VersionId` や `Range`、
`unlink()` では `VersionId` です。次のオプションはストリームラッパー自身が使います。

| オプション | 説明 |
| --- | --- |
| `seekable` | `true` にすると、読み込みストリームをシーク可能にします (下記を参照)。 |
| `delimiter` | `opendir()`/`scandir()` の区切り文字。デフォルト: `/`。空文字列にすると、プレフィックス以下のすべてのキーを一覧にします。 |
| `listFilter` | `opendir()` 用の callable。各エントリー (`Key`、`Size`、`LastModified`、... または `Prefix`) を受け取り、一覧に含めるかどうかを返します。 |
| `PartSize` | マルチパートアップロードのパートサイズ (MB 単位、デフォルト: 64)。`async-aws/simple-s3` 3.x では、1 パートより大きなボディはパートに分けてアップロードされます。2.x (PHP 8.1 でインストールされます) は、パートサイズにかかわらず 64 MB までのボディを PutObject でアップロードします。 |

次の関数をサポートしています。

| 関数 | 方法 |
| --- | --- |
| `r` での `fopen()` | GetObject。ボディはストリーミングされます。 |
| `w`、`x` での `fopen()` | データは `php://temp` バッファーに書き込まれ、ストリームがフラッシュ (`fflush()`) されるか閉じられたときにアップロードされます。64 MB より大きなボディはパートに分けてアップロードされます。`x` はオブジェクトが存在する場合 (HeadObject) に失敗します。 |
| `a` での `fopen()` | オブジェクトがバッファーにダウンロードされ (オブジェクトが存在しない場合、バッファーは空で始まります)、その結果がアップロードされます。 |
| `fread()`、`fwrite()`、`feof()`、`ftell()`、`fseek()`、`fflush()`、`fclose()`、`fstat()` | ストリームまたはそのバッファーに対して動作します。 |
| `file_exists()`、`is_file()`、`is_dir()`、`filesize()`、`filemtime()`、`stat()`、... | HeadObject。キーがオブジェクトでない場合、オブジェクトを持つプレフィックス (ListObjectsV2) はディレクトリです。バケットと `s3://` はディレクトリです。ファイルとディレクトリのモードは `0777` です。 |
| `unlink()` | DeleteObject。 |
| `rename()` | CopyObject (メタデータ付き) と DeleteObject。バケット内でもバケット間でも使えます。 |
| `copy()` | オブジェクトを読み込んで書き込みます。 |
| `mkdir()` | `s3://bucket` はクライアントのリージョンにバケットを作成します。`s3://bucket/dir` は空の `dir/` オブジェクトを作成します。 |
| `rmdir()` | `s3://bucket` はバケットを削除します。`s3://bucket/dir` は、そのプレフィックスを持つものが他にない場合に `dir/` オブジェクトを削除します。 |
| `opendir()`、`readdir()`、`rewinddir()`、`closedir()`、`scandir()` | 区切り文字を指定した ListObjectsV2 で、ページごとに取得します。`dir/` オブジェクトはスキップされます。 |

<a id="limitations"></a>

### 制限

次の制限は AWS SDK for PHP のストリームラッパーのもので、このストリームラッパーもそれを引き継いでいます。

- サポートされるモードは `r`、`w`、`a`、`x` だけです (`r+`、`w+`、`c`、... はサポートされません)。
  そのため、ストリームは読み込みか書き込みのどちらかだけを行います。
- 読み込みストリーム (`r`) は、`seekable` コンテキストオプションが `true` でない限りシークできません。
  `true` の場合は、読み込んだバイトが `php://temp` バッファーに保持され、ストリームを前後にシークできます。
- 書き込みは、書き込んでいる間ではなく、ストリームがフラッシュされるか閉じられたときにアップロードされます。
  `fflush()` のたびにバッファー全体が再度アップロードされます。アップロードのエラーは警告を発生させますが、
  `fclose()` と `file_put_contents()` はそれを報告できません。重要な場合は `fflush()` を呼び出し、その結果を確認してください。
- `a` はオブジェクト全体をダウンロードし、ストリームが閉じられたときに再度アップロードします。
  2 つのプロセスが同時に追記すると、互いの変更を上書きします。
- `touch()`、`chmod()`、`chown()`、`flock()` (および `LOCK_EX` を指定した `file_put_contents()`)、
  `ftruncate()` はサポートされません。
- ディレクトリはキーのプレフィックスでエミュレートされます。`mkdir()` は `recursive` を無視します。
  プレフィックスに対する `rmdir()` はその `dir/` オブジェクトだけを削除し、プレフィックスに他のオブジェクトがある場合は失敗します。
  プレフィックスにオブジェクトがまったくない場合は `true` を返します。
- `ACL` コンテキストオプションのない `mkdir()` は、AWS SDK と同様にモードに応じた ACL を送信します。
  `07xx` (デフォルトの `0777` を含む) では `public-read`、`06xx` では `authenticated-read`、それ以外では `private` です。
  オブジェクト所有権の設定が "Bucket owner enforced" のバケット (新しいバケットのデフォルト) は
  `bucket-owner-full-control` ACL しか受け付けないため、そのようなバケットではこれを `ACL` コンテキストオプションで渡してください。
- `rename()` は 1 回の CopyObject リクエストでオブジェクトをコピーするため、5 GB までのオブジェクトに限られます
  (AWS SDK は 5 GB を超えるとマルチパートコピーに切り替えます)。アトミックではなく、コピーの後にコピー元が削除されます。
- `url_stat()` の結果と `readdir()` が一覧にしたエントリーはプロセス内にキャッシュされ (最大 1000 パス)、
  ストリームラッパーがパスを変更するとクリアされます。他のプロセスがオブジェクトを変更する場合は
  `S3StreamWrapper::clearCache()` を呼び出してください。PHP の `clearstatcache()` ではこのキャッシュはクリアされません。
- 読み込みストリームは AsyncAws を通じてボディを受け取ります。AsyncAws は受け取ったバイトのコピーを
  `php://temp` ストリームに保持します (2 MB まではメモリ、それを超えると一時ファイル)。
- エラーは `E_USER_WARNING` を発生させ、関数は `false` を返します。`file_exists()`、`is_file()` などは、
  存在しないパスを警告なしで報告します。
- ストリームラッパーは `STREAM_IS_URL` で登録されるため、`allow_url_fopen` が必要で、
  パスをインクルードすること (`include 's3://...'`) はできません。

次の点は AWS SDK と異なります。

- `opendir()` はディレクトリを開いたときに最初の ListObjectsV2 リクエストを送信するため、バケットが存在しないと
  `opendir()` と `scandir()` は警告とともに失敗します。後のページでエラーが起きると、`readdir()` は例外を投げる代わりに
  警告とともに `false` を返します。
- プレフィックスに対する `rmdir()` は、プレフィックスに `dir/` オブジェクトと他のオブジェクトがある場合にも失敗します
  (その場合、AWS SDK は `dir/` オブジェクトを削除します)。
- `rename()` は、`ACL` (または `acl`) コンテキストオプションが設定されている場合にだけ ACL を送信します。
  AWS SDK は常に ACL (デフォルトでは `private`) を送信しますが、ACL が無効なバケットは
  `bucket-owner-full-control` しか受け付けません。
- `a` は、存在しないオブジェクト (404) だけを新しいオブジェクトとして扱います。その他のエラーでは `fopen()` が失敗します。
- `fstat()` はファイルモード `0100777` を返します。
- `async-aws/s3` のクライアント (`S3StreamWrapper::register(new S3Client(...))`) でのアップロードは、
  AWS SDK と同様に PutObject を使い、5 GB までです。`async-aws/simple-s3` のクライアント (デフォルト) では、
  64 MB (`PartSize`) より大きなボディはパートに分けてアップロードされます。`ContentType` が設定されていない場合、
  キーのコンテンツタイプは CakePHP の MIME タイプの一覧を使って拡張子から推測されます。

<a id="cakephps-file-and-folder-classes"></a>

### CakePHP の File クラスと Folder クラス

CakePHP 2 の `File` クラスと `Folder` クラスは、このストリームラッパーでも AWS SDK のものでも、
`s3://` パスでは動作しません。`Folder::isAbsolute()` は、ストリームラッパーのプロトコルが英字だけの場合にしか
それを認識しないため、`s3://bucket/dir` は相対パスとして扱われます。
英字だけのプロトコル、たとえば `S3StreamWrapper::register($client, 'awss')` を使えば、`Folder::read()`、`find()`、
`new Folder($path, true)` は動作し、`File` は既存のオブジェクトの読み込み、書き込み、追記、削除ができます。
`File::create()` が `touch()` を呼び出すため、`File::write()` と `File::create()` ではオブジェクトを作成できません。
オブジェクトの作成には `file_put_contents()` を使ってください。

<a id="s3-storage"></a>

## S3 ストレージ

`S3Storage` は同じ `s3://bucket/key` パスを受け取り、
[`async-aws/simple-s3`](https://async-aws.com/integration/simple-s3.html) を使って一般的なファイル操作を提供します。

```php
App::uses('S3Storage', 'AsyncAws.Lib');

$storage = new S3Storage();

$storage->write('s3://my-bucket/reports/today.csv', $csv, ['ContentType' => 'text/csv']);
file_put_contents(TMP . 'today.csv', $storage->read('s3://my-bucket/reports/today.csv'));
```

| メソッド | 説明 |
| --- | --- |
| `write($path, $contents, $options = [])` | 文字列またはストリームをアップロードします。大きなオブジェクトはパートに分けてアップロードされます。`$options` は `SimpleS3Client::upload()` のもので、たとえば `ContentType`、`CacheControl`、`ACL`、`Metadata` です。 |
| `read($path)` | 内容を返します。オブジェクトが存在しない場合は `AsyncAws\S3\Exception\NoSuchKeyException` を投げます。 |
| `readStream($path)` | 内容のストリーム (`resource`) を返します。 |
| `exists($path)` | オブジェクトが存在するかどうかを返します。 |
| `delete($path)` | オブジェクトを削除します。 |
| `url($path)` | オブジェクトの URL を返します。 |
| `presignedUrl($path, $expires = null)` | 署名付きのダウンロード URL を返します。`$expires` は `DateTimeImmutable` です。 |
| `client()` | その他の操作のために `SimpleS3Client` を返します。 |

`new S3Storage()` は `AsyncAws` 設定からクライアントを作成します。別のクライアントを使うには
`SimpleS3Client` を渡します: `new S3Storage($this->AsyncAws->createSimpleS3($args))`。
