העלאות מדיה שניתן להמשיך בפרוטוקול הנתונים של Google

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

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

מבוא

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

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

במאמר הזה מוסבר איך לשלב את התכונה של העלאה שניתן להמשיך אותה של Google Data באפליקציות שלכם. בדוגמאות שלמטה נעשה שימוש ב-Google Documents List Data API. שימו לב: יכול להיות שלממשקי Google API נוספים שמיישמים את הפרוטוקול הזה יהיו דרישות, קודי תגובה וכו' שונים במקצת. לכן, מומלץ לעיין בתיעוד של השירות כדי לקבל את הפרטים הספציפיים.

פרוטוקול ההעלאה שניתן להמשיך

הפעלת בקשה להעלאה שניתן להמשיך

כדי להתחיל סשן העלאה שניתן להמשיך, שולחים בקשת HTTP POST לקישור resumable-post. הקישור הזה נמצא ברמת הפיד. הקישור של DocList API ל-resumable-post נראה כך:

<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 להעלאה שניתן להמשיך. בדוגמה הבאה נשלחים 100,000 הבייטים הראשונים של קובץ PDF בגודל 1,234,567 בייטים:

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 או query status:

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 עם מטען הייעודי (payload). אחרי שמקבלים את ה-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
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

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

חזרה למעלה