Google 資料通訊協定中的可續傳媒體上傳作業

Eric Bidelman,G Suite API 團隊
2010 年 2 月

  1. 簡介
  2. 可續傳通訊協定
    1. 啟動支援續傳的上傳要求
    2. 上傳檔案
    3. 繼續上傳
    4. 取消上傳
    5. 更新現有資源
  3. 用戶端程式庫範例

簡介

目前的網路標準沒有可靠的機制,可協助 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 要求傳送至可續傳的 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-TypeX-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 會在一週後失效。

上傳檔案

可恢復上傳的通訊協定允許 (但並非必要) 以「區塊」形式上傳內容,因為 HTTP 對要求大小沒有固有的限制。用戶端可以自由選擇區塊大小,或直接上傳整個檔案。 本範例使用專屬上傳 URI 發出續傳 PUT。以下範例會傳送 1234567 位元組 PDF 檔案的前 100000 個位元組:

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 Gone404 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

注意:如果放棄上傳但未取消,上傳 URI 會在建立後一週自然失效。

更新現有資源

啟動支援續傳的上傳工作階段類似,您可以使用支援續傳的上傳通訊協定,取代現有檔案的內容。如要啟動可續傳的更新要求,請將 HTTP PUT 傳送至項目的連結,並加上 rel='...#resumable-edit-media'。如果 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
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

如需完整範例和原始碼參考資料,請參閱下列資源:

返回頁首