Возобновляемая загрузка медиафайлов в протоколе Google Data Protocol

Эрик Бидельман, команда API G Suite
Февраль 2010 г.

  1. Введение
  2. Возобновляемый протокол
    1. Инициирование запроса на возобновление загрузки.
    2. Загрузка файла
    3. Возобновление загрузки
    4. Отмена загрузки
    5. Обновление существующего ресурса
  3. Примеры клиентских библиотек

Введение

Современные веб-стандарты не предусматривают надежного механизма для HTTP-загрузки больших файлов. В результате загрузка файлов на Google и других сайтах традиционно ограничена умеренными размерами (например, 100 МБ). Для таких сервисов, как YouTube и API списков документов Google, которые поддерживают загрузку больших файлов, это представляет собой серьезную проблему.

Протокол Google Data с возможностью возобновления запросов напрямую решает вышеупомянутые проблемы, поддерживая возобновляемые HTTP-запросы POST/PUT в HTTP/1.0. Протокол был создан по образцу предложения ResumableHttpRequestsProposal, предложенного командой Google Gears.

В этом документе описывается, как интегрировать функцию возобновляемой загрузки данных Google Data в ваши приложения. В приведенных ниже примерах используется API Google Documents List Data . Обратите внимание, что другие API Google, реализующие этот протокол, могут иметь несколько иные требования/коды ответов и т. д. Для получения подробной информации обратитесь к документации соответствующего сервиса.

Возобновляемый протокол

Инициирование запроса на возобновление загрузки.

Для запуска сеанса возобновляемой загрузки отправьте HTTP POST запрос по ссылке resumable-post. Эта ссылка находится на уровне ленты. Ссылка resumable-post в API DocList выглядит следующим образом:

<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 сервер получает уникальный URI для загрузки файла в заголовке Location и пустое тело ответа:

HTTP/1.1 200 OK
Location: <upload_uri>

Для загрузки фрагментов файла будет использоваться уникальный URI загрузки.

Примечание : Первоначальный POST запрос не создает новую запись в ленте. Это происходит только после завершения всей операции загрузки.

Примечание : URI возобновляемой сессии истекает через одну неделю.

Загрузка файла

Протокол с возможностью возобновления загрузки позволяет, но не требует, загружать контент «по частям», поскольку в HTTP нет внутренних ограничений на размеры запросов. Ваш клиент может свободно выбирать размер части или просто загружать файл целиком. В этом примере используется уникальный URI загрузки для отправки PUT запроса с возможностью возобновления. В следующем примере отправляются первые 100000 байт из PDF-файла размером 1234567 байт:

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 запросе.

Примечание : Сервер может добавить новый уникальный URI для загрузки в заголовок Location во время передачи фрагмента данных. Ваш клиент должен проверить наличие обновленного Location и использовать этот URI для отправки оставшихся фрагментов на сервер.

После завершения загрузки ответ будет таким же, как если бы загрузка была выполнена с использованием механизма невозобновляемой загрузки API. То есть будет возвращен код 201 Created вместе с <atom:entry> , созданным сервером. Последующие PUT -запросы к уникальному URI загрузки будут возвращать тот же ответ, что и после завершения загрузки. Через некоторое время ответ будет 410 Gone или 404 Not Found .

Возобновление загрузки

Если ваш запрос прерывается до получения ответа от сервера или если вы получаете HTTP-ответ 503 от сервера, вы можете узнать текущий статус загрузки, отправив пустой PUT запрос по уникальному URI загрузки.

Клиент опрашивает сервер, чтобы определить, какие байты он получил:

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 .

Примечание : Сервер может добавить новый уникальный URI для загрузки в заголовок Location во время передачи фрагмента данных. Ваш клиент должен проверить наличие обновленного Location и использовать этот URI для отправки оставшихся фрагментов на сервер.

Наконец, клиент продолжает работу с того места, где остановился сервер:

PUT upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 57
Content-Range: 43-99/100

<bytes 43-99>

Отмена загрузки

Если вы хотите отменить загрузку и предотвратить дальнейшие действия с ней, отправьте запрос DELETE по уникальному URI загрузки.

DELETE upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 0

В случае успеха сервер сообщает об отмене сессии и отправляет тот же код для последующих запросов PUT или запросов статуса запроса :

HTTP/1.1 499 Client Closed Request

Примечание : Если загрузка не отменена, она автоматически аннулируется через неделю после создания.

Обновление существующего ресурса

Аналогично запуску сеанса возобновляемой загрузки , вы можете использовать протокол возобновляемой загрузки для замены содержимого существующего файла. Чтобы начать запрос на возобновляемое обновление, отправьте HTTP PUT по ссылке записи с параметром rel=' ...#resumable-edit-media '. Каждая entry медиафайлов будет содержать такую ​​ссылку, если API поддерживает обновление содержимого ресурса.

Например, запись документа в API DocList будет содержать ссылку, похожую на следующую:

<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 Docs (с использованием протокола возобновляемой загрузки) в клиентских библиотеках Google Data . Обратите внимание, что в настоящее время не все библиотеки поддерживают функцию возобновляемой загрузки.

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

Полные примеры и справочная информация по исходному коду доступны по следующим ссылкам:

Вернуться наверх