Google データ プロトコルでの再開可能なメディア アップロード

G Suite API チーム Eric Bidelman
2010 年 2 月

  1. はじめに
  2. 再開可能なプロトコル
    1. 再開可能なアップロード リクエストの開始
    2. ファイルのアップロード
    3. アップロードの再開
    4. アップロードをキャンセルする
    5. 既存のリソースを更新する
  3. クライアント ライブラリの例

はじめに

現在のウェブ標準では、大きなファイルの HTTP アップロードを促進する信頼性の高いメカニズムは提供されていません。そのため、Google と他のサイトにおけるファイルのアップロードは従来、中程度のサイズ(100 MB など)に制限されていました。大規模なファイルのアップロードをサポートする YouTube や Google Documents List API のようなサービスには大きなハードルがあります。

Google Data の再開可能プロトコルは、HTTP/1.0 で再開可能な POST/PUT HTTP リクエストをサポートすることで、前述の問題に直接対応します。 プロトコルは、Google Gears チームによって提案された ResumableHttpRequestsProposal の後にモデル化されています。

このドキュメントでは、Google データの再開可能なアップロード機能をアプリケーションに組み込む方法について説明します。以下の例では、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 を発行しています。次の例では、1234,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 の再開できないアップロード メカニズムを使用してアップロードした場合と同じレスポンスが返されます。つまり、201 Created は、サーバーによって作成された <atom:entry> とともに返されます。一意のアップロード 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

: サーバーがセッション中にバイトを commit しない場合、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 データ クライアント ライブラリで、映画ファイルを 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
List uploaders = 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

完全なサンプルとソースコード リファレンスについては、次のリソースをご覧ください。

トップへ戻る