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