העלאות מדיה מתחדשות ב-Google Data Protocol

אריק ביילמן, צוות G Suite APIs
פברואר 2010

  1. מבוא
  2. הפרוטוקול לחידוש
    1. יצירת בקשת העלאה שניתן להמשיך
    2. העלאת קובץ
    3. המשך ההעלאה
    4. ביטול העלאה
    5. עדכון משאב קיים
  3. דוגמאות לספריות לקוח

מבוא

תקני האינטרנט הנוכחיים לא מספקים מנגנון אמין להעלאת HTTP של קבצים גדולים. כתוצאה מכך, באופן מסורתי ניתן היה להעלות קבצים ב-Google ובאתרים אחרים לגדלים מתונה (למשל, 100 MB). בשירותים כמו YouTube וממשקי ה-API של רשימת המסמכים של Google שתומכים בהעלאת קבצים גדולים, יש בעיה משמעותית.

הפרוטוקול של Google לחידוש נתונים מתייחס באופן ישיר לבעיות שצוינו לעיל על ידי תמיכה בבקשות POST/PUT HTTP שניתן להמשיך ב-HTTP/1.0. הפרוטוקול נוצר לאחר מודל של ResumableHttpRequestsProposal שהוצע על ידי צוות Google גלגלים.

מסמך זה מתאר כיצד לשלב באפליקציות שלך את תכונת ההעלאה העדכנית של נתוני Google. בדוגמאות הבאות אנחנו משתמשים ב-Google Documents List Data API. לתשומת ליבך: ייתכן שלממשקי ה-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.

זוהי בקשה נוספת לדוגמה שבה מעלים מסמך 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 שניתן להמשיך. הדוגמה הבאה שולחת את 10,000 הבייטים הראשונים של קובץ 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

כדי לעדכן את המטא-נתונים והתוכן של המשאב בו-זמנית, יש לכלול XML של Atom במקום גוף ריק. עיינו בדוגמה שבקטע יצירת בקשת העלאה שניתן להמשיך.

כאשר השרת מגיב עם ה-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. שימו לב: לא כל הספריות תומכות בשלב הזה בפיצ'רים שאפשר להפעיל מחדש.

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

דוגמאות מלאות והפניות לקוד מקור זמינות במשאבים הבאים:

חזרה למעלה