Przesyłanie możliwe do wznowienia w ramach protokołu danych Google

Eric Bidelman, zespół ds. interfejsów API G Suite
luty 2010 r.

  1. Wprowadzenie
  2. Protokół wznowienia
    1. Inicjowanie żądania wznowienia
    2. Przesyłanie pliku
    3. Wznawianie przesyłania
    4. Anulowanie przesyłania
    5. Aktualizowanie istniejącego zasobu
  3. Przykłady z biblioteki klienta

Wprowadzenie

Obecne standardy internetowe nie pozwalają na przesyłanie dużych plików za pomocą protokołu HTTP. W związku z tym rozmiary plików przesyłanych do Google i innych witryn zwykle były ograniczone do średniej wielkości (np. 100 MB). W przypadku usług takich jak YouTube czy interfejsy API listy Dokumentów Google, które obsługują przesyłanie dużych plików, stanowi to poważne przeszkody.

Protokół wznawiania danych Google bezpośrednio rozwiązuje wyżej wymienione problemy, obsługując wznawiane żądania HTTP POST/PUT w HTTP/1.0. Protokół został wymodelowany na podstawie elementu ResumableHttpRequestsOffer sugerowanego przez zespół Google wtyczki.

W tym dokumencie opisano, jak włączyć funkcję wznawiania przesyłania danych Google w swoich aplikacjach. W poniższych przykładach użyto interfejsu Google Documents List Data API. Pamiętaj, że dodatkowe interfejsy API Google, które wdrażają ten protokół, mogą mieć nieco inne wymagania/kody odpowiedzi itp. Szczegółowe informacje znajdziesz w dokumentacji usługi.

Protokół wznawiania

Rozpoczynanie żądania wznowienia

Aby zainicjować sesję przesyłania możliwego do wznowienia, wyślij żądanie HTTP POST do linku do wznawiania. Ten link znajduje się na poziomie pliku danych. Link do wznawiania przesyłania w interfejsie API DocList wygląda tak:

<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"/>

Treść żądania POST powinna być pusta lub zawierać pozycję XML Atom. Nie może zawierać rzeczywistej zawartości pliku. Przykład poniżej tworzy wznowione żądanie przesłania dużego pliku PDF z tytułem przyszłego dokumentu za pomocą nagłówka 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

Nagłówki X-Upload-Content-Type i X-Upload-Content-Length powinny mieć ustawiony typ MIME i rozmiar pliku, który chcesz przesłać. Jeśli długość treści nie jest znana podczas tworzenia sesji przesyłania, nagłówek X-Upload-Content-Length można pominąć.

Oto inne przykładowe żądanie przesłania dokumentu Word. Tym razem metadane Atom zostały dodane i zostaną zastosowane do ostatecznego wpisu dokumentu.

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>

Odpowiedź serwera z początkowego ciągu POST to unikalny identyfikator URI przesyłania w nagłówku Location i pusta treść odpowiedzi:

HTTP/1.1 200 OK
Location: <upload_uri>

Do przesyłania fragmentów plików używany będzie unikalny identyfikator URI przesyłania.

Uwaga: pierwsze żądanie POST nie powoduje utworzenia nowego wpisu w pliku danych. Dzieje się tak dopiero po zakończeniu przesyłania.

Uwaga: identyfikator URI możliwej do wznowienia sesji wygasa po tygodniu.

Przesyłanie pliku

Protokół wznawiania umożliwia przesyłanie treści za pomocą „fragmentów”, ale nie wiąże się to bezpośrednio z ograniczeniami dotyczącymi rozmiaru żądań HTTP. Klient może wybrać rozmiar fragmentu lub przesłać plik jako całość. W tym przykładzie użyto unikalnego identyfikatora URI przesyłania, aby można było wznowić proces PUT. Poniższy przykład wysyła pierwsze 100 000 bajtów z 1234567 bajtu pliku PDF:

PUT upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 100000
Content-Range: bytes 0-99999/1234567

bytes 0-99999

Jeśli rozmiar pliku PDF jest nieznany, w tym przykładzie używany będzie Content-Range: bytes 0-99999/*. Więcej informacji o nagłówku Content-Range znajdziesz tutaj.

Serwer odpowiada bieżącym zakresem bajtów, który jest przechowywany:

HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: bytes=0-99999

Klient powinien przechodzić dalej do PUT każdego fragmentu pliku, dopóki nie zostanie przesłany cały plik. Dopóki przesyłanie nie zostanie ukończone, serwer wyśle odpowiedź HTTP 308 Resume Incomplete i zakres bajtów znany w nagłówku Range. Klienci muszą użyć nagłówka Range, aby określić, gdzie zacząć nowy fragment. Dlatego nie zakładaj, że serwer otrzymał wszystkie bajty pierwotnie wysłane w żądaniu PUT.

Uwaga: podczas wyświetlania fragmentu kodu serwer może wygenerować nowy unikalny identyfikator URI przesyłania w nagłówku Location. Twój klient powinien sprawdzić, czy Location jest zaktualizowany, i użyć tego identyfikatora URI do wysłania pozostałych fragmentów na serwer.

Po zakończeniu przesyłania odpowiedź będzie taka sama, jak gdyby została przesłana za pomocą mechanizmu nieodwracania interfejsu API. Oznacza to, że wraz z <atom:entry> zostanie zwrócony ciąg 201 Created utworzony przez serwer. Kolejne PUT lub unikalny identyfikator URI przesyłania zwraca tę samą odpowiedź, która została zwrócona po zakończeniu przesyłania. Po pewnym czasie odpowiedź zmieni się z 410 Gone lub 404 Not Found.

Wznawiam przesyłanie

Jeśli żądanie zostanie zakończone przed otrzymaniem odpowiedzi z serwera lub otrzymasz odpowiedź HTTP 503 z serwera, możesz wysłać zapytanie o bieżący stan przesyłania, wysyłając puste żądanie PUT na unikalny identyfikator URI przesyłania.

Klient odpytuje serwer, aby określić, które bajtyk otrzymał:

PUT upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 0
Content-Range: bytes */content_length

Jeśli nie znasz długości, użyj content_length jako *.

Serwer odpowiada bieżącym zakresem bajtów:

HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: bytes=0-42

Uwaga: jeśli serwer nie zatwierdził żadnych bajtów na potrzeby sesji, nagłówek Range zostanie pominięty.

Uwaga: podczas wyświetlania fragmentu kodu serwer może wygenerować nowy unikalny identyfikator URI przesyłania w nagłówku Location. Twój klient powinien sprawdzić, czy Location jest zaktualizowany, i użyć tego identyfikatora URI do wysłania pozostałych fragmentów na serwer.

Na koniec klient wznawia działanie od miejsca, w którym serwer został przerwany:

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

<bytes 43-99>

Anulowanie przesyłania

Jeśli chcesz anulować przesyłanie i nie chcesz podejmować żadnych działań, wyślij żądanie DELETE dotyczące unikalnego identyfikatora URI przesyłania.

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

Jeśli operacja się powiedzie, serwer odpowie, że sesja zostanie anulowana, a na żądanie PUT lub stanu zapytania zostanie wysłany z tym samym kodem:

HTTP/1.1 499 Client Closed Request

Uwaga: jeśli przesłany plik porzuci się bez anulowania, utraci on ważność tydzień po utworzeniu.

Aktualizowanie istniejącego zasobu

Podobnie jak w przypadku inicjowania sesji przesyłania możliwego do wznowienia możesz użyć protokołu wznawiania, aby zastąpić zawartość istniejącego pliku. Aby rozpocząć żądanie wznowienia aktualizacji, wyślij link HTTP PUT do linku z atrybutem rel='...#resumable-edit-media'. Każdy plik multimedialny entry będzie zawierać taki link, jeśli interfejs API obsługuje aktualizowanie treści zasobu.

Na przykład wpis w dokumencie w interfejsie DocList API będzie zawierał link podobny do:

<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"/>

Początkowe żądanie będzie wyglądać tak:

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

Aby jednocześnie zaktualizować metadane i zawartość zasobu, zamiast pustego treści umieść Atom XML. Zobacz przykład w sekcji Inicjowanie wznawianego żądania przesyłania.

Gdy serwer wysyła unikalny identyfikator URI przesyłania, wyślij PUT z Twoim ładunkiem. Po przesłaniu niepowtarzalnego identyfikatora URI proces aktualizowania jego zawartości przebiega tak samo jak przesyłanie pliku.

Ten konkretny przykład zaktualizuje zawartość istniejącego dokumentu w jednym ujęciu:

PUT upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 1000
Content-Range: 0-999/1000

<bytes 0-999>

Powrót do góry

Przykłady biblioteki klienta

Poniżej znajdziesz przykłady przesłania pliku z filmem do Dokumentów Google (z użyciem protokołu wznawiania) w bibliotekach danych Google. Obecnie nie wszystkie biblioteki obsługują funkcję wznawiania odtwarzania.

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

Pełną listę przykładów i kodu źródłowego znajdziesz w tych zasobach:

Powrót do góry