אריק בידלמן, צוות G Suite APIs
פברואר 2010
מבוא
תקני האינטרנט הנוכחיים לא מספקים מנגנון אמין להעלאת קבצים גדולים באמצעות 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 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