Eric Bidelman、G Suite APIs チーム
2010 年 2 月
はじめに
現在のウェブ標準では、大きなファイルの HTTP アップロードを容易にする信頼性の高いメカニズムが提供されていません。そのため、Google や他のサイトでのファイル アップロードは、従来、適度なサイズ(100 MB など)に制限されてきました。YouTube や Google ドキュメント リスト API などの大きなファイルのアップロードをサポートするサービスでは、これは大きな障害となります。
Google Data の再開可能なプロトコルは、HTTP/1.0 で再開可能な POST/PUT HTTP リクエストをサポートすることで、前述の問題に直接対処します。このプロトコルは、Google Gears チームが提案した ResumableHttpRequestsProposal をモデルとしています。
このドキュメントでは、Google Data の再開可能なアップロード機能をアプリケーションに組み込む方法について説明します。以下の例では、Google Documents List Data API を使用しています。このプロトコルを実装する追加の Google API では、要件やレスポンス コードなどが若干異なる場合があります。詳細については、サービスのドキュメントをご覧ください。
再開可能なプロトコル
再開可能なアップロード リクエストを開始する
再開可能なアップロード セッションを開始するには、再開可能な投稿リンクに HTTP POST リクエストを送信します。このリンクはフィードレベルにあります。DocList API の再開可能な投稿リンクは次のようになります。
<link rel="http://schemas.google.com/g/2005#resumable-create-media" type="application/atom+xml" href="https://docs.google.com/feeds/upload/create-session/default/private/full"/>
POST リクエストの本文は空にするか、Atom XML エントリを含める必要があります。実際のファイルの内容を含めることはできません。次の例では、大きな PDF をアップロードするための再開可能なリクエストを作成し、Slug ヘッダーを使用して将来のドキュメントのタイトルを含めています。
POST /feeds/upload/create-session/default/private/full HTTP/1.1 Host: docs.google.com GData-Version: version_number Authorization: authorization Content-Length: 0 Slug: MyTitle X-Upload-Content-Type: content_type X-Upload-Content-Length: content_length empty body
X-Upload-Content-Type ヘッダーと X-Upload-Content-Length ヘッダーは、最終的にアップロードするファイルの MIME タイプとサイズに設定する必要があります。アップロード セッションの作成時にコンテンツの長さが不明な場合は、X-Upload-Content-Length ヘッダーを省略できます。
次に、Word ドキュメントをアップロードする別のリクエストの例を示します。今回は Atom メタデータが含まれており、最終的なドキュメント エントリに適用されます。
POST /feeds/upload/create-session/default/private/full?convert=false HTTP/1.1
Host: docs.google.com
GData-Version: version_number
Authorization: authorization
Content-Length: atom_metadata_content_length
Content-Type: application/atom+xml
X-Upload-Content-Type: application/msword
X-Upload-Content-Length: 7654321
<?xml version='1.0' encoding='UTF-8'?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007">
<category scheme="http://schemas.google.com/g/2005#kind"
term="http://schemas.google.com/docs/2007#document"/>
<title>MyTitle</title>
<docs:writersCanInvite value="false"/>
</entry>
最初の POST からのサーバーのレスポンスは、Location ヘッダー内の一意のアップロード URI と空のレスポンス本文です。
HTTP/1.1 200 OK
Location: <upload_uri>
一意のアップロード URI は、ファイル チャンクのアップロードに使用されます。
注: 最初の POST リクエストでは、フィードに新しいエントリは作成されません。これは、アップロード オペレーション全体が完了した場合にのみ発生します。
注: 再開可能なセッションの URI は 1 週間後に期限切れになります。
ファイルのアップロード
再開可能なプロトコルでは、リクエスト サイズに HTTP の制限がないため、コンテンツを「チャンク」単位でアップロードできますが、必須ではありません。クライアントは、チャンクサイズを自由に選択するか、ファイルを全体としてアップロードできます。この例では、一意のアップロード URI を使用して再開可能な PUT を発行します。次の例では、1,234,567 バイトの PDF ファイルの最初の 100,000 バイトを送信します。
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 100000 Content-Range: bytes 0-99999/1234567 bytes 0-99999
PDF ファイルのサイズが不明な場合、この例では Content-Range: bytes
0-99999/* が使用されます。Content-Range ヘッダーの詳細については、こちらをご覧ください。
サーバーは、保存されている現在のバイト範囲をレスポンスとして返します。
HTTP/1.1 308 Resume Incomplete Content-Length: 0 Range: bytes=0-99999
ファイル全体のアップロードが完了するまで、クライアントはファイルのチャンクごとに PUT を続行する必要があります。アップロードが完了するまで、サーバーは HTTP 308 Resume Incomplete と、認識しているバイト範囲を Range ヘッダーで返します。クライアントは Range ヘッダーを使用して、次のチャンクの開始位置を決定する必要があります。したがって、PUT リクエストで最初に送信されたすべてのバイトがサーバーで受信されたとは限りません。
注: サーバーは、チャンク中に Location ヘッダーで新しい一意のアップロード URI を発行する場合があります。クライアントは、更新された Location を確認し、その URI を使用して残りのチャンクをサーバーに送信する必要があります。
アップロードが完了すると、API の再開不可能なアップロード メカニズムを使用してアップロードが行われた場合と同じレスポンスが返されます。つまり、サーバーで作成された <atom:entry> とともに 201 Created が返されます。一意のアップロード URI への後続の PUT は、アップロードが完了したときに返されたものと同じレスポンスを返します。一定期間が経過すると、レスポンスは 410 Gone または 404 Not Found になります。
アップロードを再開する
サーバーからレスポンスを受信する前にリクエストが終了した場合や、サーバーから HTTP 503 レスポンスが返された場合は、一意のアップロード URI で空の PUT リクエストを発行して、アップロードの現在のステータスをクエリできます。
クライアントはサーバーをポーリングして、受信したバイト数を特定します。
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 0 Content-Range: bytes */content_length
長さが不明な場合は、content_length として * を使用します。
サーバーは現在のバイト範囲を返します。
HTTP/1.1 308 Resume Incomplete Content-Length: 0 Range: bytes=0-42
注: サーバーがセッション用にバイトをコミットしていない場合、Range ヘッダーは省略されます。
注: サーバーは、チャンク中に Location ヘッダーで新しい一意のアップロード URI を発行する場合があります。クライアントは、更新された Location を確認し、その URI を使用して残りのチャンクをサーバーに送信する必要があります。
最後に、クライアントはサーバーが中断したところから再開します。
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 57 Content-Range: 43-99/100 <bytes 43-99>
アップロードのキャンセル
アップロードをキャンセルして、それ以降の処理が行われないようにするには、一意のアップロード URI で DELETE リクエストを発行します。
DELETE upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 0
成功すると、サーバーはセッションがキャンセルされたことを応答し、以降の PUT またはクエリ ステータス リクエストに対して同じコードで応答します。
HTTP/1.1 499 Client Closed Request
注: アップロードがキャンセルされずに破棄された場合、作成から 1 週間後に自動的に期限切れになります。
既存のリソースを更新する
再開可能なアップロード セッションを開始すると同様に、再開可能なアップロード プロトコルを使用して既存のファイルの内容を置き換えることができます。再開可能な更新リクエストを開始するには、rel='...#resumable-edit-media' のエントリのリンクに HTTP PUT を送信します。API がリソースのコンテンツの更新をサポートしている場合、各メディア entry にこのようなリンクが含まれます。
たとえば、DocList API のドキュメント エントリには、次のようなリンクが含まれます。
<link rel="http://schemas.google.com/g/2005#resumable-edit-media" type="application/atom+xml" href="https://docs.google.com/feeds/upload/create-session/default/private/full/document%3A12345"/>
したがって、最初のリクエストは次のようになります。
PUT /feeds/upload/create-session/default/private/full/document%3A12345 HTTP/1.1 Host: docs.google.com GData-Version: version_number Authorization: authorization If-Match: ETag | * Content-Length: 0 X-Upload-Content-Length: content_length X-Upload-Content-Type: content_type empty body
リソースのメタデータとコンテンツを同時に更新するには、空の本文ではなく Atom XML を含めます。再開可能なアップロード リクエストを開始するセクションの例をご覧ください。
サーバーから一意のアップロード URI が返されたら、ペイロードとともに PUT を送信します。一意のアップロード URI を取得したら、ファイルのコンテンツを更新するプロセスは、ファイルをアップロードする場合と同じです。
この例では、既存のドキュメントのコンテンツを一度に更新します。
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 1000 Content-Range: 0-999/1000 <bytes 0-999>
クライアント ライブラリの例
以下は、Google Data クライアント ライブラリで、再開可能なアップロード プロトコルを使用して Google ドキュメントに動画ファイルをアップロードする例です。現時点では、すべてのライブラリが再開可能機能をサポートしているわけではありません。
int MAX_CONCURRENT_UPLOADS = 10; int PROGRESS_UPDATE_INTERVAL = 1000; int DEFAULT_CHUNK_SIZE = 10485760; DocsService client = new DocsService("yourCompany-yourAppName-v1"); client.setUserCredentials("user@gmail.com", "pa$$word"); // Create a listener FileUploadProgressListener listener = new FileUploadProgressListener(); // See the sample for details on this class. // Pool for handling concurrent upload tasks ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_UPLOADS); // Create {@link ResumableGDataFileUploader} for each file to upload Listuploaders = Lists.newArrayList(); File file = new File("test.mpg"); String contentType = DocumentListEntry.MediaType.fromFileName(file.getName()).getMimeType(); MediaFileSource mediaFile = new MediaFileSource(file, contentType); URL createUploadUrl = new URL("https://docs.google.com/feeds/upload/create-session/default/private/full"); ResumableGDataFileUploader uploader = new ResumableGDataFileUploader(createUploadUrl, mediaFile, client, DEFAULT_CHUNK_SIZE, executor, listener, PROGRESS_UPDATE_INTERVAL); uploaders.add(uploader); listener.listenTo(uploaders); // attach the listener to list of uploaders // Start the upload(s) for (ResumableGDataFileUploader uploader : uploaders) { uploader.start(); } // wait for uploads to complete while(!listener.isDone()) { try { Thread.sleep(100); } catch (InterruptedException ie) { listener.printResults(); throw ie; // rethrow }
// Chunk size in MB int CHUNK_SIZE = 1; ClientLoginAuthenticator cla = new ClientLoginAuthenticator( "yourCompany-yourAppName-v1", ServiceNames.Documents, "user@gmail.com", "pa$$word"); // Set up resumable uploader and notifications ResumableUploader ru = new ResumableUploader(CHUNK_SIZE); ru.AsyncOperationCompleted += new AsyncOperationCompletedEventHandler(this.OnDone); ru.AsyncOperationProgress += new AsyncOperationProgressEventHandler(this.OnProgress); // Set metadata for our upload. Document entry = new Document() entry.Title = "My Video"; entry.MediaSource = new MediaFileSource("c:\\test.mpg", "video/mpeg"); // Add the upload uri to document entry. Uri createUploadUrl = new Uri("https://docs.google.com/feeds/upload/create-session/default/private/full"); AtomLink link = new AtomLink(createUploadUrl.AbsoluteUri); link.Rel = ResumableUploader.CreateMediaRelation; entry.DocumentEntry.Links.Add(link); ru.InsertAsync(cla, entry.DocumentEntry, userObject);
- (void)uploadAFile { NSString *filePath = @"~/test.mpg"; NSString *fileName = [filePath lastPathComponent]; // get the file's data NSData *data = [NSData dataWithContentsOfMappedFile:filePath]; // create an entry to upload GDataEntryDocBase *newEntry = [GDataEntryStandardDoc documentEntry]; [newEntry setTitleWithString:fileName]; [newEntry setUploadData:data]; [newEntry setUploadMIMEType:@"video/mpeg"]; [newEntry setUploadSlug:fileName]; // to upload, we need the entry, our service object, the upload URL, // and the callback for when upload has finished GDataServiceGoogleDocs *service = [self docsService]; NSURL *uploadURL = [GDataServiceGoogleDocs docsUploadURL]; SEL finishedSel = @selector(uploadTicket:finishedWithEntry:error:); // now start the upload GDataServiceTicket *ticket = [service fetchEntryByInsertingEntry:newEntry forFeedURL:uploadURL delegate:self didFinishSelector:finishedSel]; // progress monitoring is done by specifying a callback, like this SEL progressSel = @selector(ticket:hasDeliveredByteCount:ofTotalByteCount:); [ticket setUploadProgressSelector:progressSel]; } // callback for when uploading has finished - (void)uploadTicket:(GDataServiceTicket *)ticket finishedWithEntry:(GDataEntryDocBase *)entry error:(NSError *)error { if (error == nil) { // upload succeeded } } - (void)pauseOrResumeUploadForTicket:(GDataServiceTicket *)ticket { if ([ticket isUploadPaused]) { [ticket resumeUpload]; } else { [ticket pauseUpload]; } }
import os.path import atom.data import gdata.client import gdata.docs.client import gdata.docs.data CHUNK_SIZE = 10485760 client = gdata.docs.client.DocsClient(source='yourCompany-yourAppName-v1') client.ClientLogin('user@gmail.com', 'pa$$word', client.source); f = open('test.mpg') file_size = os.path.getsize(f.name) uploader = gdata.client.ResumableUploader( client, f, 'video/mpeg', file_size, chunk_size=CHUNK_SIZE, desired_class=gdata.docs.data.DocsEntry) # Set metadata for our upload. entry = gdata.docs.data.DocsEntry(title=atom.data.Title(text='My Video')) new_entry = uploader.UploadFile('/feeds/upload/create-session/default/private/full', entry=entry) print 'Document uploaded: ' + new_entry.title.text print 'Quota used: %s' % new_entry.quota_bytes_used.text
完全なサンプルとソースコードのリファレンスについては、次のリソースをご覧ください。