S-JIS[2013-06-27/2014-03-09] 変更履歴

Amazon S3 Java API

AWS S3のJava APIのメモ。


概要

S3をJava APIで操作するには、AmazonS3Clientクラスを使う。

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
	String accessKey = "アクセスキー";
	String secretKey = "プライベートキー";
	AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

	AmazonS3Client s3 = new AmazonS3Client(awsCredentials);
	s3.setEndpoint("https://s3-ap-northeast-1.amazonaws.com");
	try {
		〜
	} finally {
		s3.shutdown();
	}

なお、AmazonS3Clientの各メソッドは、スレッドセーフかどうか不明。[2013-12-12]
setRegion()だけは「スレッドセーフでない」とJavadocに書かれているが、他のメソッドには何も書かれていない。
書かれていない以上、スレッドセーフとして扱うのは適切ではないだろうなぁ。

まぁ、AmazonS3Clientのインスタンスを作る個数に制限は無さそうだから、マルチスレッドで扱いたい場合はスレッド毎にインスタンスを作ればいいか。


ファイル一覧の取得

バケット内のファイルの一覧を取得する例。[2013-08-21]

駄目な例

たいていの場合は以下のコーディングでも動くが、良くない。[2013-08-22]

	ObjectListing list = s3.listObjects("バケット名" /*, "キーの先頭" */);
	for (S3ObjectSummary s : list.getObjectSummaries()) {
		System.out.printf("bucket=%s, key=%s, size=%d\n", s.getBucketName(), s.getKey(), s.getSize());
	}

ListObjectsRequestは、1回のリクエストでは1000件までしか返さない。
1001個以上のファイルがあるかどうかをチェックして再度リクエストを実行する必要がある。

※1回のリクエストで返すキー数は、ListObjectsRequestのmaxKeysで指定できる。(デフォルト値が1000)[2014-03-09]


ListObjectsRequestを使う方法

import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.S3ObjectSummary;
	ListObjectsRequest request = new ListObjectsRequest();
	request.setBucketName("バケット名");
//	request.setPrefix("キーの先頭");

	ObjectListing list;
	do {
		list = s3.listObjects(request);
		for (S3ObjectSummary s : list.getObjectSummaries()) {
			System.out.printf("bucket=%s, key=%s, size=%d\n", s.getBucketName(), s.getKey(), s.getSize());
		}
		request.setMarker(list.getNextMarker());
	} while (list.isTruncated());

ListObjectsRequestを使わない方法

表面上はListObjectsRequestが出てこないが、内部ではListObjectsRequestを生成している。[2013-08-22]

	ObjectListing list = s3.listObjects("バケット名" /*, "キーの先頭" */);
	do {
		for (S3ObjectSummary s : list.getObjectSummaries()) {
			System.out.printf("bucket=%s, key=%s, size=%d\n", s.getBucketName(), s.getKey(), s.getSize());
		}
		list = s3.listNextBatchOfObjects(list);
	} while (list.getMarker() != null);

フォルダー直下のファイル一覧の取得

フォルダー名を指定して、その直下にあるファイルの一覧だけを取得する例。[2014-03-09]

S3上の保存方法には「フォルダー」という概念は無く、ただファイル名(キー)にスラッシュ「/」が含まれているだけだが、そのスラッシュに区切られた部分をフォルダーと呼んでいる。(AWSコンソール上はフォルダーとして表示される)

ファイル一覧を取得するフォルダーを指定するには、prefixにフォルダー名(末尾にスラッシュを付ける)を指定する。
そして、delimiterにスラッシュを指定する。(delimiterを指定しないと、prefixに合致する全ファイルが取得される)

import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.S3ObjectSummary;
	String bucketName = "バケット名";
	String prefix = "フォルダー/";
	String delimiter = "/";
	ListObjectsRequest request = new ListObjectsRequest(bucketName, prefix, null, delimiter, null);

	ObjectListing list;
	do {
		list = s3.listObjects(request);
		for (S3ObjectSummary s : list.getObjectSummaries()) {
			System.out.println(s.getKey()); //ファイル(キー)
		}
		request.setMarker(list.getNextMarker());
	} while (list.isTruncated());

※この方法では、フォルダー直下のファイルの一覧だけが取れる(サブフォルダーは取れない)。サブフォルダーの一覧が欲しい場合はgetCommonPrefixes()を使う。


フォルダー一覧の取得

フォルダー名を指定して、その直下にあるフォルダーの一覧だけを取得する例。[2014-03-09]

import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
	ListObjectsRequest request = new ListObjectsRequest(bucketName, prefix, null, delimiter, null);

	ObjectListing list = s3.listObjects(request);
	do {
		List<String> folders = list.getCommonPrefixes()); //フォルダー(共通のprefix)一覧
		System.out.println(folders);

		list = s3.listNextBatchOfObjects(list);
	} while (list.getMarker() != null);

※この方法では、フォルダー直下のサブフォルダー一覧だけが取れる(ファイルは取れない)。ファイル一覧も欲しい場合はgetObjectSummaries()を併用する。


ファイルのアップロード

1ファイルずつアップロードする例。[2013-08-22]

import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.Upload;
	String s3key = 〜;
	File file = 〜;
	PutObjectRequest request = new PutObjectRequest(bucketName, s3key, file);

	TransferManager tm = new TransferManager(s3);
	try {
		Upload upload = tm.upload(request);

		long lastTransferred = 0;
		while (!upload.isDone()) {
			long transferred = upload.getProgress().getBytesTransferred();
			System.out.printf("worked: %d%n", (int) (transferred - lastTransferred));
			lastTransferred = transferred;

			System.out.printf("worked: %f%n", upload.getProgress().getPercentTransferred());

			Thread.sleep(100);
		}
		upload.waitForCompletion();
	} finally {
		tm.shutdownNow();
	}

複数ファイルをまとめてアップロードする例。[2013-09-10]

複数ファイルを並列アップロードすると、1ファイルずつの転送を複数回行うよりも転送速度が速い。
ただし、中断したりエラーが起きたりした場合に、どのファイルまで終わったか・どのファイルでエラーになったのか等は分からない。

import com.amazonaws.services.s3.transfer.MultipleFileUpload;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.TransferProgress;
	String bucket   = 〜;
	String s3key    = 〜; //アップロード先のオブジェクトキー
	File directory  = 〜; //アップロード元のディレクトリー
	List<File> list = 〜; //アップロードするファイル一覧

	TransferManager tm = new TransferManager(s3);
	try {
		MultipleFileUpload upload = tm.uploadFileList(bucket, s3key, directory, list);
		TransferProgress progress = upload.getProgress();
		long lastTransferred = 0;
		while (!upload.isDone()) {
			long transferred = progress.getBytesTransferred();
			System.out.printf("worked: %d%n", (int) (transferred - lastTransferred));
			lastTransferred = transferred;

			Thread.sleep(100);
		}
		upload.waitForCompletion();
	} finally {
		tm.shutdownNow();
	}

TransferManager#uploadFileList()では、アップロードするファイル一覧を指定することが出来る。
ディレクトリーを指定してその中のファイル全てをアップロードするuploadDirectory()というメソッドもある。
(この方法では、1000件の制限は無いようだ)

アップロード先のオブジェクトキーは、ルートのオブジェクトキーに、ファイルの“directoryからの相対パス”を加えたものになる。
例えばs3keyが「root/foo/」、directoryが「D:\temp」、転送するファイルが「D:\temp\hoge\zzz.txt」のとき、
アップロードされたファイルのオブジェクトキーは「root/foo/hoge/zzz.txt」になる。


ファイルのダウンロード

1ファイルずつダウンロードする例。[2013-08-21]

import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.transfer.Download;
import com.amazonaws.services.s3.transfer.TransferManager;
		TransferManager tm = new TransferManager(s3);

		for (String s3key: list) {
			File file = new File(dir, s3key);
			downloadFile(s3, tm, s3key, file);
		}
	private void downloadFile(AmazonS3 s3, TransferManager tm, String s3key, File file) {
		GetObjectRequest req = new GetObjectRequest("bucket", s3key);

		S3Object object = s3.getObject(req);
		try {
			int totalWork = (int) object.getObjectMetadata().getContentLength();
			Download download = tm.download(req, file);

			long lastTransferred = 0;
			while (!download.isDone()) {
				long transferred = download.getProgress().getBytesTransferred();
				System.out.printf("worked: %d%n", (int) (transferred - lastTransferred));
				lastTransferred = transferred;

				System.out.printf("progress %f%n",download.getProgress().getPercentTransferred());

				Thread.sleep(100);
			}
			download.waitForCompletion();
		} finally {
			try {
				object.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

進捗状況を確認する為に、downloadインスタンスからTransferProgressを取得している。
転送バイト数を取得するメソッドは、 SDK1.3まではgetBytesTransfered()だったが、SDK1.5ではgetBytesTransferred()になっている。(綴りのミスの修正。「r」が1個増えている^^;)

取得したS3Objectは、使い終わったらクローズする必要がある。
クローズしないと、GetObjectRequestを50回実行した後でコネクションプールの50個制限に引っかかってConnectionPoolTimeoutExceptionが発生する。
しかし、S3ObjectのcloseメソッドはSDK1.5にはあるが、SDK1.3には無い。…1.3ではどうするのかね?(爆)


複数ファイルをまとめてダウンロードする例。[2013-09-10]

import com.amazonaws.services.s3.transfer.MultipleFileDownload;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.TransferProgress;
	String bucket  = 〜;
	String s3key   = 〜; //ダウンロード元オブジェクトキーの先頭部分
	File directory = 〜; //ダウンロード先ディレクトリー

	TransferManager tm = new TransferManager(s3);
	try {
		MultipleFileDownload download = tm.downloadDirectory(bucket, s3key, directory);
		TransferProgress progress = download.getProgress();
		long lastTransferred = 0;
		while (!download.isDone()) {
			long transferred = progress.getBytesTransferred();
			System.out.printf("worked: %d%n", (int) (transferred - lastTransferred));
			lastTransferred = transferred;

			Thread.sleep(100);
		}
		download.waitForCompletion();
	} finally {
		tm.shutdownNow();
	}

複数ファイルの並列ダウンロードは、並列アップロードと違って、ファイル一覧を指定するメソッドが無い。(SDK1.5)

また、オブジェクトキーの相対パス部分を指定することも出来ない。
ダウンロード先のdirectoryの下に、ダウンロードファイルのオブジェクトキーの階層のディレクトリーが作られる。
例えばs3keyが「root/foo/」でダウンロードされるファイルが「root/foo/hoge/zzz.txt」、directoryが「D:\download」のとき、
ダウンロードされるファイルが作られるのは「D:\download\root\foo\hoge\zzz.txt」になる。


ファイルのコピー

S3上のディレクトリー内の全ファイルをS3上の別の場所にコピーする例。[2013-12-12]

import com.amazonaws.services.s3.model.CopyObjectRequest;
import com.amazonaws.services.s3.model.CopyObjectResult;
import com.amazonaws.services.s3.model.ObjectMetadata;
		String bucketName = "hishidama";
		String srcPrefix = "example/from/";
		String dstPrefix = "example/to/";

		List<String> srcList = listFiles(s3, bucketName, srcPrefix);
		copyFiles(s3, bucketName, srcPrefix, srcList, dstPrefix);
	// 先頭が一致する(つまりディレクトリー配下の)ファイル一覧を取得
	private static List<String> listFiles(AmazonS3Client s3, String bucketName, String srcPrefix) {
		List<String> result = new ArrayList<String>();

		ObjectListing list = s3.listObjects(bucketName, srcPrefix);
		do {
			List<S3ObjectSummary> summaries = list.getObjectSummaries();
			for (S3ObjectSummary s : summaries) {
				String key = s.getKey();
				if (!key.endsWith("/")) {
					result.add(key);
				}
			}
			list = s3.listNextBatchOfObjects(list);
		} while (list.getMarker() != null);

		return result;
	}
	private static void copyFiles(AmazonS3Client s3, String bucketName, String srcPrefix, List<String> srcList, String dstPrefix) {
		int offset = srcPrefix.length();

		for (String srcKey : srcList) {
			String dstKey = dstPrefix + srcKey.substring(offset);

			ObjectMetadata metadata = s3.getObjectMetadata(bucketName, srcKey);

			CopyObjectRequest request = new CopyObjectRequest(bucketName, srcKey, bucketName, dstKey);
			request.setNewObjectMetadata(metadata);
			CopyObjectResult result = s3.copyObject(request);
			System.out.println(result);
		}
	}

この例ではバケットは転送元・転送先で同一だが、別々に指定することも出来る。

copyObject()メソッドは、コピーが完了するまで制御が戻ってこない。(同期的なメソッドである)

また、このメソッドでは、ディレクトリー相当のパスを指定しても“配下の全ファイルをコピーする”という動作はしない
したがって、自分でファイル一覧を取得し、個別にコピーする必要がある。


ファイルの削除

ファイルを削除する例。

S3上のファイルは「s3n://バケット名/キー」という形式で表される。
バケット名をDeleteObjectsRequestのコンストラクターに指定し、キー(複数可)をKeyVersionのListで指定する。

import com.amazonaws.services.s3.model.DeleteObjectsRequest;
import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion;
	List<KeyVersion> keys = new ArrayList<KeyVersion>();
	for (String key : 〜) {
		KeyVersion kv = new KeyVersion(key);
		keys.add(kv);
	}

	DeleteObjectsRequest request = new DeleteObjectsRequest("バケット名");
	request.setKeys(keys);
	s3.deleteObjects(request);

ただし、一度のリクエストで削除できるのは1000件まで。[2013-08-21]
1000件を超えるとAmazonS3Exceptionが発生する。(件数制限のエラーなのに、「XMLが不正」とかいうエラーメッセージなのは何とかして欲しいところ)

Status Code: 400, AWS Service: Amazon S3, AWS Request ID: 765CAB5794610405, AWS Error Code: MalformedXML, AWS Error Message: The XML you provided was not well-formed or did not validate against our published schema, S3 Extended Request ID: 〜
	at com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:614)
	at com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:312)
	at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:165)
	at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:2949)
	at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:2921)
	at com.amazonaws.services.s3.AmazonS3Client.deleteObjects(AmazonS3Client.java:1467)
	〜

DeleteObjectsRequestのJavadocには1000件の制限について何も書かれていないが、.NET 4.5のドキュメントには書かれている(苦笑)

したがって、削除対象が1000件を超える場合は1000件ずつDeleteObjectsRequestを実行するようにしなければならない。


ところで、S3の命名ルールでは、キー名にダブルクォーテーションを使うことが出来る。(参考: 横田 あかりさんのAmazon S3 の Bucket 命名ルールについて
のだが、ダブルクォーテーション入りのキーをDeleteObjectsRequestに指定すると例外が発生する。(1.3.27で出たが、1.4.7も同様)

Status Code: 400, AWS Service: Amazon S3, AWS Request ID: 0D89BE329212660B, AWS Error Code: MalformedXML, AWS Error Message: The XML you provided was not well-formed or did not validate against our published schema, S3 Extended Request ID: 〜
	at com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:614)
	at com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:312)
	at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:165)
	at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:2949)
	at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:2921)
	at com.amazonaws.services.s3.AmazonS3Client.deleteObjects(AmazonS3Client.java:1467)
	〜

調べてみると、XmlWriterクラスにバグがあった。

	private void appendEscapedString(String s, StringBuilder builder) {
〜
		switch (ch) {
〜
		case '"':
			escape = "&quote;";
			break;
〜
		}
〜
	}

ダブルクォーテーションのHTML(XML)エスケープは、「&quote;」じゃなくて「&quot;(eは無い!)なんだよね(苦笑)
この所為で“不正なXML”というエラーになったようだ。

GitHubでプルリクエストを送ってみたら、1.5.7で反映された^^[/2013-09-13]
2013-07-20に送ったから、反映されるまで2ヶ月弱だった。


S3へ戻る / AWSへ戻る / 技術メモへ戻る
メールの送信先:ひしだま