Эрик Бидельман, команда API G Suite
Февраль 2010 г.
Введение
Современные веб-стандарты не предусматривают надежного механизма для 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 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