Importations de contenu multimédia avec reprise dans le protocole de données Google

Eric Bidelman, équipe des API G Suite
Février 2010

  1. Introduction
  2. Protocole de reprise
    1. Lancer une requête d'importation avec reprise
    2. Importer un fichier
    3. Reprendre une importation
    4. Annuler une importation
    5. Mettre à jour une ressource existante
  3. Exemples de bibliothèques clientes

Introduction

Les normes Web actuelles ne fournissent aucun mécanisme fiable pour faciliter l'importation de fichiers volumineux via HTTP. Par conséquent, la taille des fichiers importés sur Google et d'autres sites a toujours été limitée à une taille modérée (par exemple, 100 Mo). Cela représente un obstacle majeur pour les services tels que les API YouTube et Google Documents List, qui acceptent l'importation de fichiers volumineux.

Le protocole de reprise des données Google répond directement aux problèmes susmentionnés en acceptant les requêtes HTTP POST/PUT pouvant être reprises dans HTTP/1.0. Le protocole a été modélisé d'après la ResumableHttpRequestsProposal suggérée par l'équipe Google Gears.

Ce document explique comment intégrer la fonctionnalité d'importation réutilisable de Google Data dans vos applications. Les exemples ci-dessous utilisent l'API Google Documents List Data. Notez que les API Google supplémentaires qui implémentent ce protocole peuvent avoir des exigences, des codes de réponse, etc. légèrement différents. Veuillez consulter la documentation du service pour plus de détails.

Protocole avec reprise

Lancer une requête d'importation avec reprise

Pour lancer une session d'importation avec reprise, envoyez une requête HTTP POST au lien de publication avec reprise. Ce lien se trouve au niveau du flux. Le lien de publication pouvant être repris de l'API DocList se présente comme suit :

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

Le corps de votre requête POST doit être vide ou contenir une entrée XML Atom, et ne doit pas inclure le contenu réel du fichier. L'exemple ci-dessous crée une requête avec reprise pour importer un fichier PDF volumineux et inclut un titre pour le futur document à l'aide de l'en-tête 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

Les en-têtes X-Upload-Content-Type et X-Upload-Content-Length doivent être définis sur le type MIME et la taille du fichier que vous allez importer. Si la longueur du contenu est inconnue au moment de la création de la session d'importation, l'en-tête X-Upload-Content-Length peut être omis.

Voici un autre exemple de requête qui télécharge un document Word. Cette fois, les métadonnées Atom sont incluses et seront appliquées à l'entrée de document finale.

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>

La réponse du serveur à la requête POST initiale est un URI d'importation unique dans l'en-tête Location et un corps de réponse vide :

HTTP/1.1 200 OK
Location: <upload_uri>

L'URI d'importation unique sera utilisé pour importer les blocs de fichiers.

Remarque : La requête POST initiale ne crée pas d'entrée dans le flux. Cela ne se produit que lorsque l'opération d'importation est terminée.

Remarque : L'URI d'une session avec reprise expire après une semaine.

Importer un fichier

Le protocole de reprise autorise, mais n'exige pas, l'importation de contenu par "blocs", car HTTP n'impose aucune restriction inhérente à la taille des requêtes. Votre client est libre de choisir la taille des blocs ou d'importer le fichier dans son intégralité. Cet exemple utilise l'URI d'importation unique pour émettre un PUT avec reprise. L'exemple suivant envoie les 100 000 premiers octets d'un fichier PDF de 1 234 567 octets :

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

bytes 0-99999

Si la taille du fichier PDF était inconnue, cet exemple utiliserait Content-Range: bytes 0-99999/*. Pour en savoir plus sur l'en-tête Content-Range, cliquez ici.

Le serveur répond avec la plage d'octets actuelle qui a été stockée :

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

Votre client doit continuer à utiliser PUT pour chaque fragment du fichier jusqu'à l'importation du fichier entier. Tant que l'importation n'est pas terminée, le serveur répond avec un code HTTP 308 Resume Incomplete et la plage d'octets qu'il connaît dans l'en-tête Range. Les clients doivent utiliser l'en-tête Range pour déterminer où démarrer le prochain fragment. Par conséquent, ne partez pas du principe que le serveur a reçu tous les octets initialement envoyés dans la requête PUT.

Remarque : Le serveur peut émettre une nouvelle URI d'importation unique dans l'en-tête Location lors d'un bloc. Votre client doit rechercher un Location mis à jour et utiliser cet URI pour envoyer les blocs restants au serveur.

Une fois l'importation terminée, la réponse sera la même que si l'importation avait été effectuée à l'aide du mécanisme d'importation non reprise de l'API. En d'autres termes, un 201 Created sera renvoyé avec le <atom:entry>, tel que créé par le serveur. Les PUT ultérieurs à l'URI d'importation unique renverront la même réponse que celle renvoyée à la fin de l'importation. Au bout d'un certain temps, la réponse sera 410 Gone ou 404 Not Found.

Reprendre une importation

Si votre requête est interrompue avant de recevoir une réponse du serveur ou si vous recevez une réponse HTTP 503 du serveur, vous pouvez interroger l'état actuel de l'importation en envoyant une requête PUT vide sur l'URI d'importation unique.

Le client interroge le serveur pour déterminer les octets qu'il a reçus :

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

Utilisez * comme content_length si la longueur n'est pas connue.

Le serveur répond avec la plage d'octets actuelle :

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

Remarque : Si le serveur n'a validé aucun octet pour la session, il omettra l'en-tête Range.

Remarque : Le serveur peut émettre une nouvelle URI d'importation unique dans l'en-tête Location lors d'un bloc. Votre client doit rechercher un Location mis à jour et utiliser cet URI pour envoyer les blocs restants au serveur.

Enfin, le client reprend là où le serveur s'est arrêté :

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

<bytes 43-99>

Annulation d'une importation

Si vous souhaitez annuler l'importation et empêcher toute autre action, envoyez une requête DELETE à l'URI d'importation unique.

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

Si l'opération aboutit, le serveur répond que la session est annulée et renvoie le même code pour les futures PUT ou requêtes d'état :

HTTP/1.1 499 Client Closed Request

Remarque : Si une importation est abandonnée sans être annulée, elle expire naturellement une semaine après sa création.

Mettre à jour une ressource existante

Comme pour lancer une session d'importation avec reprise, vous pouvez utiliser le protocole d'importation avec reprise pour remplacer le contenu d'un fichier existant. Pour démarrer une requête de mise à jour avec reprise, envoyez une requête HTTP PUT au lien de l'entrée avec rel='...#resumable-edit-media'. Chaque entry de contenu multimédia contient un tel lien si l'API permet de mettre à jour le contenu de la ressource.

Par exemple, une entrée de document dans l'API DocList contiendra un lien semblable à celui-ci :

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

La requête initiale serait donc la suivante :

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

Pour mettre à jour les métadonnées et le contenu d'une ressource en même temps, incluez Atom XML au lieu d'un corps vide. Consultez l'exemple de la section Lancer une requête d'importation avec reprise.

Lorsque le serveur répond avec l'URI d'importation unique, envoyez un PUT avec votre charge utile. Une fois que vous avez l'URI d'importation unique, la procédure de mise à jour du contenu du fichier est la même que pour importer un fichier.

Cet exemple spécifique met à jour le contenu du document existant en une seule fois :

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

<bytes 0-999>

Haut de page

Exemples de bibliothèques clientes

Vous trouverez ci-dessous des exemples d'importation d'un fichier de film dans Google Docs (à l'aide du protocole d'importation avec reprise) dans les bibliothèques clientes Google Data. Notez que toutes les bibliothèques ne sont pas compatibles avec la fonctionnalité de reprise pour le moment.

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

Pour obtenir des exemples complets et des références de code source, consultez les ressources suivantes :

Haut de page