リモートワーク環境のデスクツアー

PHPでAWSのS3のファイルを操作する方法まとめ【アップロード・DL他】

本サイトのコンテンツには、プロモーションが含まれている場合があります。

PHPでAWSのS3のファイルを操作する方法まとめ【アップロード・DL他】

こんな悩みに答えます
  • PHPでS3のファイルを操作したい
  • S3のファイルをアップロード・DLする方法が知りたい
  • アップロードした時間で絞り込んでリストを取得したい

以前S3のファイル操作をPHPでやったのですが、割と苦戦しました。

公式が使い方を出してくれてはいるのですが、それでもなかなか難しかった。

この記事で紹介するのは以下の操作方法。

  • ファイル・ディレクトリの中身を取得
  • コマンドを叩いてファイルの中身をアップロード時間などの条件付きで取得
  • ファイルのアップロード・ダウンロード
  • ファイル・ディレクトリのデリート
  • ファイル・ディレクトリのコピー
  • ファイルのリネーム方法

覚え書き程度に書いていけたらと思います。

S3の中身を取得する方法

まずはファイル(バケットの中身)の取得方法から。

PHP
$s3 = Aws\S3\S3Client::factory([
    'credentials' => [
        'key' => 'your access key',
        'secret' => 'your access secret',  
    ],
    'version' => 'latest',
    'region'  => 'ap-northeast-1',
]);

$s3_list = $s3->ListObjects([
    'Bucket' => 'xxxxxxxxxx',//バケット名
    'Prefix' => $s3_dir,//ファイル指定
]);

上側でS3Clientクラスのfactory()メソッドからクライアントオブジェクトを作成して、ListObjectsでファイルを取得します。

$s3_listにファイル名やら、(同一ファイルかどうか見分けるための)Etagや、アップロード時間などが入っています。

例えば$s3_listのContents内に取得したファイルの中身が書かれているので、それをいじってどうこうするようになるかな。

maipyon
maipyon

最大1,000件までしか取得できないので、ファイル数が多すぎる場合はループして取得する必要があるので注意。

以後、オブジェクト作成の部分(factoryメソッド)については紹介するコードに含めませんが、必ず必要なのでクライアントオブジェクトはs3のコマンド実行前に作成するようにしてください。

一方で、ディレクトリリストを取得しようと思ったら以下のコードを実行することになります。

PHP
$s3_dir_list = $s3->ListObjects([
    'Bucket' => 'xxxxxxxxxx',//バケット名
    'Prefix' => $s3_dir . '/',//ファイル指定
    'Delimiter' => '/',
]);

Prefixでディレクトリ配下のファイルが取得できるのですが、ディレクトリ「直下」ではなくて全体が取得できてしまって困ることになります。

そこで、Delimiterを指定して、ディレクトリ直下の要素のみ取得できるように条件を付けます。

また、ファイルとディレクトリとの判別条件がやっかいで、$s3_dir_list[‘Contents’]のSizeが0だとディレクトリだと判定できます。

s3にはフォルダという概念がないのがこの辺りの難しさで、Amazon S3は単純なKVS(Key-Value型データストア)です。

そのため、空のファイルfooを作って、その配下にファイルbazを置くと、fooがフォルダのように活用できるということになります。

フォルダのように見えているものは、サイズが0のファイルと同じです。フォルダなんて概念はAmazon S3にはないんだぜ、って話らしい。

詳しくはAmazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする(別サイト)を参考にしてください。

S3の中身を時間指定して取得する方法

S3にアップロードした時間で条件を付けてリストを取得したい場合、今までの方法では出来なさそうでした。

例えば、2020年9月中にアップロードされた全ファイルを取得したい場合など。

できるのかもですが、参考できるコマンドが見つからなくて断念。

やり方はPHPでコマンドを叩く以下の通り。「10分前以後」にアップロードされたファイルを取得します。

PHP
$input_time = date("Y-m-d H:i:s",strtotime("-10 minute"));//10分前

//AWSのCLI(コマンドラインからの実行)でフィルターを掛けつつ情報取得
$s3_list = shell_exec('aws s3api list-objects --bucket '
. $s3_bucket . ' --prefix "foo/" --query "Contents[?LastModified >= \`'
. $input_time . '\`]" --output=json');

$s3_list = json_decode($s3_list, true);

//LastModifiedは、アップロード開始時間
//時間を日本時間に戻す
foreach($s3_list as $key => $value){
    $time = new DateTime($s3_list[$key]['LastModified']);
    $s3_list[$key]['LastModified'] = $time->setTimeZone( new DateTimeZone('Asia/Tokyo'))->format('Y-m-d H:i:s');
}

query部分で、LastModifiedの時間に条件を付けて実行します。

環境にもよる気はしますが、LastModifiedの中身を使用したい場合はあとで日本時間に戻す必要がありそうなので戻しました。(自分の環境だと、9時間前になってました。)

S3でファイルのアップロード・ダウンロード

次に、アップロードとダウンロードの方法を紹介します。

まずはアップロードから。

PHP
use Aws\S3\Exception\S3Exception;
$files = count($_FILES['files']['name']);
if( !$_FILES['files']['name'][0] ){
    //エラーを返して終了
}

//s3へのアップロード
for( $i = 0; $i < $files; $i++ ){
    try{
        $s3_upload = $s3->putObject([
            'ACL' => 'public-read',
            'Bucket' => 'xxxxxxxxxxxx',
            'Key' => $s3_dir . '/' . $_FILES['files']['name'][$i],
            'SourceFile' => $_FILES['files']['tmp_name'][$i],
            'ContentType' => $_FILES['files']['type'][$i],
        ]);
    } catch (S3Exception $e ){
        //エラー処理
    }
}

ajaxでファイルを投げて、それをアップロードするイメージ

複数ファイルのアップロードでも、ループしつつアップロードします。

use Aws\S3\Exception\S3Exception; にて、S3でのエラーをcatchで処理できるようにできます。
(以後、説明は省きます。)

ただ、ajaxでファイルを投げる際に、ファイルが大きすぎたら無理。(フロントからバックエンドに投げた際に止まってしまう)

なので上記のやり方は非推奨。(DLもgetObjectコマンドでできるしファイルの大きさ関係なくうまく動作したけど、ちょっと不安があります)

ダウンロードの場合は、取得したファイルをbase_64でエンコードしてフロントに返して、フロントでデコードしてDL。
ややめんどくさいし、結構時間もかかります。

ただ念のため、DLについても書いておきます。

こちらはかなり大きなファイルでもDLできましたので、アップロードと違って大きな問題は起きずに動作するはず。

PHP
try{
    $s3_download = $s3->getObject(array(
        'Bucket' => 'xxxxxxxxxxxx',
        'Key' => $s3_dir . '/' . $file_name,
    ));

    $s3_content = $s3_download['Body'];
    $s3_content = base64_encode($s3_content);

    //エンコードした内容を返す
    echo json_encode($s3_content);
}
catch(S3Exception $e){
    //エラー処理
}

その後フロントでデコードしつつDLします。

JavaScript
var binary = atob(data['file']);//data['file']が返ってきたファイル内容のことです
var len = binary.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++){
    bytes[i] = binary.charCodeAt(i);
}
var blob = new Blob(
    [bytes],
);
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = file_name;
link.click();

ただ、色々と面倒なので、やり方を変えて、署名付きURLを発行してs3のファイルを操作するほうが良いかと思います。

署名付きURLを発行して、s3のファイルを操作する

署名付きURLを発行して、そこにアクセスすることでアップロード・DLをする方法もあり、こちらの方が軽くて速いので推奨。

PHP
$key = $s3_dir . '/' . $file_name;//ファイル名は半角英数字にすること

$cmd = $s3->getCommand( 'PutObject', [
    'Bucket' => 'xxxxxxxxx',
    'Key' => $key,
]);
$r = $s3->createPresignedRequest( $cmd, '+1 minute' );
//URLを返す
echo (string)$r->getUri();

これで、返ってきたURLにアクセスすることでファイルアップロードができます。

URLは期限付きで、上記だと1分。

期限が切れたときにURLにアクセスしても、エラーページが返ってきます。
最大7日間まで指定できるようです。

ダウンロードの場合でも、ほぼ同じで、上記のコードをgetObjectに変えればOK。

S3のファイルをデリートする

デリートのやり方は簡単。

PHP
$fp = $s3_dir . '/' . $filename;
//s3にファイルが存在するか
if( !$s3->doesObjectExist('bucket名', $fp) ){
    //エラー処理
} else {
    $s3_delete = $s3->deleteObject([
        'Bucket' => 'xxxxxxxx',
        'Key'    => $fp,
    ]);
}

指定した1ファイルを削除するだけなので、割と簡単。

一方で、ディレクトリごと(数ファイルを一気に)削除するのなら、以下のように実行します。

PHP
$file_list = $s3->listObjects([
    'Bucket' => 'xxxxxxx',
    'region' => 'ap-northeast-1',
]);

foreach( $file_list['Contents'] as $filename ){
    $files[] = ['Key' => $filename['Key']];
}
try{
    $s3_delete = $s3->deleteObjects(array(
        'Bucket' => 'xxxxxxxx',
        'Delete' => [
            'Objects' => $files,
        ]
    ));
} catch (s3Exception $e ){
     //エラーを返す
}

まずlistObjectsで消したいファイル以下を全て選択して、deleteObjectsで一気に削除です。

maipyon
maipyon

これも1,000件までが最大なので、超えるようなら1,000件ごとに$filesを分けてループ処理させる必要があります。

S3のファイルをコピーする

例えば、バケットAからバケットBにコピーしたい場合に使えます。

1ファイルをコピーするのであれば以下のようにします。

PHP
$path = $s3_dir . '/' . $filename;
$s3_copy = $s3->copyObject([
    'Bucket' => 'xxxxxxxxx',//バケットBを指定
    'CopySource' => 'xxxxxxx/' . $path,//xxxxxはバケットAを指定
    'Key' => $path,
]);

バケット間でコピペができるのでありがたいですね。

ファイルを本番環境に移したいときなどに使用するかな、という感じです。

また、ディレクトリごとコピペしたい場合は、以下のようにします。

PHP
$s3_list = $s3->listObjects([
    'Bucket' => 'xxxxxxx',バケットAを指定
    'Prefix' => $s3_dir . '/',//コピーするディレクトリを指定
    'Delimiter' => '/',
]);

$batch = array();
//ディレクトリ配下をコピー
foreach( $s3_list['Contents'] as $file ){
    $batch[] = $s3->getCommand('CopyObject', [
        'Bucket'  => 'xxxxxxxx',//バケットBを指定
        'Key'  => $file['Key'],
        'CopySource' => 'xxxxxx/' . $file['Key'],//xxxxxxはバケットAを指定
    ]);
}
$results = CommandPool::batch($s3, $batch);

ループしながらコピーをするのは(コピー回数が)多すぎると危ないので、最終行で一気に実行する形をとっています。

こんなやり方もあるんだなー、という感じ。

もちろん、別の場所でも応用できるので、色々試してみてください。

maipyon
maipyon

こちらもlistObjectsの時点で最大値が1,000件なので、そこを1,000件以上取得できるようにしてあげないと1,000件以上のファイルがあるディレクトリのコピペはできません。

S3のファイルをリネームする

厄介なのがリネームで、非常に面倒。

なぜかというと、リネームコマンドが(たぶん)ないため、名前を変えつつコピー→元を削除という形をとる必要があります。

ファイルを変更する場合は簡単なのですが、ディレクトリのリネームをするのであればディレクトリごと上記の処理を行う必要があるので、場合わけが必要になるので要注意。

その途中でS3のファイルリストを取得するのですが、例えばfoo1というディレクトリがあり、foo2に変える場合、削除の際にfoo1、foo2どちらも削除対象として含まれてしまいます。

工夫次第でどうにかなるかもしれませんが、ディレクトリの名称は(新規作成時、リネーム時は)「前方一致したらアウト」というルールで作るようにしました。

気を付けてください。

PHPでs3のファイル操作をする方法まとめ

この記事では、以下のファイル操作方法を紹介しました。

今回紹介したファイル操作方法
  • ファイル・ディレクトリの中身を取得
  • コマンドを叩いてファイルの中身をアップロード時間などの条件付きで取得
  • ファイルのアップロード・ダウンロード
  • ファイル・ディレクトリのデリート
  • ファイル・ディレクトリのコピー
  • ファイルのリネーム方法

他にも操作するコマンドはありますが、上記を網羅できれば大体のファイル操作はできるのではないかと思います。

説明のためにエラー処理の部分をコメントだけにしたり、factory()メソッドを含めなかったりしましたので、その点にだけ注意してください。

ややこしい部分もありますが、頑張りましょう!