Эрик Бидельман, команда G Suite API
февраль 2010 г.
Введение
Текущие веб-стандарты не предоставляют надежного механизма для облегчения загрузки больших файлов по протоколу 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 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
Полные примеры и справочные материалы по исходному коду см. в следующих ресурсах:
- Пример приложения и исходный код библиотеки Java
- Пример приложения библиотеки Objective-C
- Источник библиотеки .NET