Возобновляемая загрузка мультимедиа в протоколе данных Google

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

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

Введение

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

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

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

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

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

Чтобы инициировать сеанс возобновляемой загрузки, отправьте HTTP-запрос 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 можно опустить.

Вот еще один пример запроса, который вместо этого загружает текстовый документ. На этот раз метаданные 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 поддерживает обновление содержимого ресурса.

Например, запись документа в 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 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

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

Вернуться к вершине