Google 데이터 프로토콜에서 재개 가능한 미디어 업로드

에릭 비델만, G Suite API팀
2010년 2월

  1. 소개
  2. 재개 가능한 프로토콜
    1. 재개 가능한 업로드 요청 시작
    2. 파일 업로드
    3. 업로드 재개
    4. 업로드 취소
    5. 기존 리소스 업데이트
  3. 클라이언트 라이브러리 예시

소개

현재 웹 표준은 대용량 파일의 HTTP 업로드를 용이하게 하는 안정적인 메커니즘을 제공하지 않습니다. 그 결과 Google 및 기타 사이트에서 파일 업로드는 일반적으로 크기가 보통 (예: 100MB)으로 제한되었습니다. 대용량 파일 업로드를 지원하는 YouTube 및 Google Documents List API와 같은 서비스의 경우 이는 심각한 문제가 됩니다.

Google 데이터 재개 가능 프로토콜은 HTTP/1.0에서 재개 가능한 POST/PUT HTTP 요청을 지원하여 앞서 언급한 문제를 직접적으로 해결합니다. 이 프로토콜은 Google Gear팀에서 제안한 ResumableHttpRequestsSuggestion를 따라 모델링되었습니다.

이 문서에서는 Google 데이터의 재개 가능한 업로드 기능을 애플리케이션에 통합하는 방법을 설명합니다. 아래는 Google Documents List Data API를 사용하는 예입니다. 이 프로토콜을 구현하는 추가 Google API에는 요구사항/응답 코드 등이 약간 다를 수 있습니다. 구체적인 내용은 서비스 문서를 참조하세요.

재개 가능한 프로토콜

재개 가능한 업로드 요청 시작

재개 가능한 업로드 세션을 시작하려면 재개 가능한 게시물 링크로 HTTP POST 요청을 보냅니다. 이 링크는 피드 수준에서 확인할 수 있습니다. DocList API의 재개 가능한 게시물 링크는 다음과 같습니다.

<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-TypeX-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의 서버 응답은 Location 헤더의 고유한 업로드 URI이자 빈 응답 본문입니다.

HTTP/1.1 200 OK
Location: <upload_uri>

고유한 업로드 URI는 파일 청크를 업로드하는 데 사용됩니다.

참고: 초기 POST 요청은 피드에 새 항목을 만들지 않습니다. 이 작업은 전체 업로드 작업이 완료된 경우에만 발생합니다.

참고: 재개 가능한 세션 URI는 일주일 후에 만료됩니다.

파일 업로드

재개 가능한 프로토콜은 요청 크기에 관한 HTTP의 고유한 제한사항이 없으므로 콘텐츠를 '청크' 단위로 업로드할 수 있도록 허용하지만 필수는 아닙니다. 클라이언트는 단위 크기를 선택하거나 파일 전체를 업로드할 수 있습니다. 이 예에서는 고유한 업로드 URI를 사용하여 재개 가능한 PUT를 실행합니다. 다음 예는 1234567바이트의 PDF 파일 중 처음 100,000바이트를 전송합니다.

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 요청에 전송된 모든 바이트를 수신했다고 가정하지 마세요.

참고: 서버는 단위별로 Location 헤더에 새로운 고유 업로드 URI를 발급할 수 있습니다. 클라이언트는 업데이트된 Location를 확인하고 이 URI를 사용하여 나머지 단위를 서버에 전송해야 합니다.

업로드가 완료되면 API의 재개 불가능한 업로드 메커니즘을 사용한 업로드와 응답이 동일합니다. 즉, 서버에서 만든 대로 201 Created<atom:entry>와 함께 반환됩니다. 순 업로드 URI에 이어 PUT를 사용하면 업로드가 완료될 때 반환된 것과 동일한 응답이 반환됩니다. 일정 기간이 지나면 응답은 410 Gone 또는 404 Not Found입니다.

업로드 재개

서버로부터 응답을 받기 전에 요청이 종료되거나 서버로부터 HTTP 503 응답을 받은 경우, 고유한 업로드 URI에 대해 빈 PUT 요청을 실행하여 현재 업로드 상태를 쿼리할 수 있습니다.

클라이언트는 서버를 폴링하여 수신된 바이트를 확인합니다.

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 헤더가 생략됩니다.

참고: 서버는 단위별로 Location 헤더에 새로운 고유 업로드 URI를 발급할 수 있습니다. 클라이언트는 업데이트된 Location를 확인하고 이 URI를 사용하여 나머지 단위를 서버에 전송해야 합니다.

마지막으로 클라이언트는 서버가 중단된 부분부터 다시 시작합니다.

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

<bytes 43-99>

업로드 취소

업로드를 취소하고 추가 작업을 하지 않으려면 고유한 업로드 URI에서 DELETE 요청을 실행합니다.

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

성공하면 서버는 세션이 취소되었다고 응답하고 추가 PUT 또는 쿼리 상태 요청에 동일한 코드로 응답합니다.

HTTP/1.1 499 Client Closed Request

참고: 업로드를 취소하지 않고 폐기하면 생성 후 일주일이 지나면 자동으로 만료됩니다.

기존 리소스 업데이트

재개 가능한 업로드 세션 시작과 마찬가지로 재개 가능한 업로드 프로토콜을 활용하여 기존 파일의 콘텐츠를 대체할 수 있습니다. 재개 가능한 업데이트 요청을 시작하려면 rel='...#resumable-edit-media'을 사용하여 항목의 링크에 HTTP PUT를 전송합니다. API가 리소스의 콘텐츠 업데이트를 지원하는 경우 각 미디어 entry에 이러한 링크가 포함됩니다.

예를 들어 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를 전송합니다. 고유한 업로드 URI가 있으면 파일 콘텐츠를 업데이트하는 프로세스는 파일 업로드와 동일합니다.

이 예제는 기존 문서의 내용을 한 번에 업데이트합니다.

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

<bytes 0-999>

맨 위로

클라이언트 라이브러리 예시

다음은 Google 데이터 클라이언트 라이브러리에서 재개 가능한 업로드 프로토콜을 사용하여 Google Docs에 영화 파일을 업로드하는 샘플입니다. 현재 모든 라이브러리가 재개 가능한 기능을 지원하지는 않습니다.

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

전체 샘플 및 소스 코드 참조는 다음 리소스를 참고하세요.

맨 위로