diff options
-rw-r--r-- | chrome/browser/google_apis/drive_api_operations.cc | 64 | ||||
-rw-r--r-- | chrome/browser/google_apis/drive_api_operations.h | 38 | ||||
-rw-r--r-- | chrome/browser/google_apis/drive_api_operations_unittest.cc | 582 | ||||
-rw-r--r-- | chrome/browser/google_apis/drive_api_service.cc | 43 | ||||
-rw-r--r-- | chrome/browser/google_apis/gdata_wapi_operations_unittest.cc | 42 | ||||
-rw-r--r-- | chrome/browser/google_apis/test_util.cc | 32 | ||||
-rw-r--r-- | chrome/browser/google_apis/test_util.h | 8 | ||||
-rw-r--r-- | chrome/test/data/chromeos/drive/file_entry.json | 49 |
8 files changed, 785 insertions, 73 deletions
diff --git a/chrome/browser/google_apis/drive_api_operations.cc b/chrome/browser/google_apis/drive_api_operations.cc index 56044c1..492c3ec 100644 --- a/chrome/browser/google_apis/drive_api_operations.cc +++ b/chrome/browser/google_apis/drive_api_operations.cc @@ -32,7 +32,7 @@ void ParseJsonAndRun( DCHECK(!callback.is_null()); scoped_ptr<T> resource; - if (value.get()) { + if (value) { resource = T::CreateFrom(*value); if (!resource) { // Failed to parse the JSON value, although the JSON value is available, @@ -44,6 +44,33 @@ void ParseJsonAndRun( callback.Run(error, resource.Pass()); } +// Parses the JSON value to FileResource instance and runs |callback| on the +// UI thread once parsing is done. +// This is customized version of ParseJsonAndRun defined above to adapt the +// remaining response type. +void ParseFileResourceWithUploadRangeAndRun( + const drive::UploadRangeCallback& callback, + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK(!callback.is_null()); + + scoped_ptr<FileResource> file_resource; + if (value) { + file_resource = FileResource::CreateFrom(*value); + if (!file_resource) { + callback.Run( + UploadRangeResponse(GDATA_PARSE_ERROR, + response.start_position_received, + response.end_position_received), + scoped_ptr<FileResource>()); + return; + } + } + + callback.Run(response, file_resource.Pass()); +} + } // namespace //============================== GetAboutOperation ============================= @@ -441,5 +468,40 @@ InitiateUploadExistingFileOperation::GetExtraRequestHeaders() const { return headers; } +//============================ ResumeUploadOperation =========================== + +ResumeUploadOperation::ResumeUploadOperation( + OperationRegistry* registry, + net::URLRequestContextGetter* url_request_context_getter, + UploadMode upload_mode, + const base::FilePath& drive_file_path, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const scoped_refptr<net::IOBuffer>& buf, + const UploadRangeCallback& callback) + : ResumeUploadOperationBase(registry, + url_request_context_getter, + upload_mode, + drive_file_path, + upload_location, + start_position, + end_position, + content_length, + content_type, + buf), + callback_(callback) { + DCHECK(!callback_.is_null()); +} + +ResumeUploadOperation::~ResumeUploadOperation() {} + +void ResumeUploadOperation::OnRangeOperationComplete( + const UploadRangeResponse& response, scoped_ptr<base::Value> value) { + ParseFileResourceWithUploadRangeAndRun(callback_, response, value.Pass()); +} + } // namespace drive } // namespace google_apis diff --git a/chrome/browser/google_apis/drive_api_operations.h b/chrome/browser/google_apis/drive_api_operations.h index 504371c..2b7abb1 100644 --- a/chrome/browser/google_apis/drive_api_operations.h +++ b/chrome/browser/google_apis/drive_api_operations.h @@ -392,6 +392,44 @@ class InitiateUploadExistingFileOperation DISALLOW_COPY_AND_ASSIGN(InitiateUploadExistingFileOperation); }; +// Callback used for ResumeUpload() (and will be used for GetUploadStatus()). +typedef base::Callback<void( + const UploadRangeResponse& response, + scoped_ptr<FileResource> new_resource)> UploadRangeCallback; + +//============================ ResumeUploadOperation =========================== + +// Performs the operation for resuming the upload of a file. +class ResumeUploadOperation : public ResumeUploadOperationBase { + public: + // See also ResumeUploadOperationBase's comment for parameters meaining. + // |callback| must not be null. + ResumeUploadOperation( + OperationRegistry* registry, + net::URLRequestContextGetter* url_request_context_getter, + UploadMode upload_mode, + const base::FilePath& drive_file_path, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const scoped_refptr<net::IOBuffer>& buf, + const UploadRangeCallback& callback); + virtual ~ResumeUploadOperation(); + + protected: + // UploadRangeOperationBase overrides. + virtual void OnRangeOperationComplete( + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) OVERRIDE; + + private: + const UploadRangeCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(ResumeUploadOperation); +}; + } // namespace drive } // namespace google_apis diff --git a/chrome/browser/google_apis/drive_api_operations_unittest.cc b/chrome/browser/google_apis/drive_api_operations_unittest.cc index 4f0ab52..b6b95c6 100644 --- a/chrome/browser/google_apis/drive_api_operations_unittest.cc +++ b/chrome/browser/google_apis/drive_api_operations_unittest.cc @@ -24,6 +24,7 @@ namespace google_apis { namespace { const char kTestDriveApiAuthToken[] = "testtoken"; +const char kTestETag[] = "test_etag"; const char kTestUserAgent[] = "test-user-agent"; const char kTestChildrenResponse[] = @@ -34,7 +35,8 @@ const char kTestChildrenResponse[] = "\"childLink\": \"child_link\",\n" "}\n"; -const char kTestUploadUrl[] = "https://server/upload/path"; +const char kTestUploadExistingFilePath[] = "/upload/existingfile/path"; +const char kTestUploadNewFilePath[] = "/upload/newfile/path"; void CopyResultsFromGetAboutResourceCallbackAndQuit( GDataErrorCode* error_out, @@ -56,6 +58,16 @@ void CopyResultsFromFileResourceCallbackAndQuit( MessageLoop::current()->Quit(); } +void CopyResultFromUploadRangeCallbackAndQuit( + UploadRangeResponse* response_out, + scoped_ptr<FileResource>* file_resource_out, + const UploadRangeResponse& response_in, + scoped_ptr<FileResource> file_resource_in) { + *response_out = response_in; + *file_resource_out = file_resource_in.Pass(); + MessageLoop::current()->Quit(); +} + } // namespace class DriveApiOperationsTest : public testing::Test { @@ -82,6 +94,9 @@ class DriveApiOperationsTest : public testing::Test { base::Bind(&DriveApiOperationsTest::HandleDataFileRequest, base::Unretained(this))); test_server_.RegisterRequestHandler( + base::Bind(&DriveApiOperationsTest::HandleResumeUploadRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( base::Bind(&DriveApiOperationsTest::HandleInitiateUploadRequest, base::Unretained(this))); test_server_.RegisterRequestHandler( @@ -93,6 +108,8 @@ class DriveApiOperationsTest : public testing::Test { // Reset the server's expected behavior just in case. ResetExpectedResponse(); + received_bytes_ = 0; + content_length_ = 0; } virtual void TearDown() OVERRIDE { @@ -114,9 +131,9 @@ class DriveApiOperationsTest : public testing::Test { // the server. See also HandleDataFileRequest below. base::FilePath expected_data_file_path_; - // This is a url string in the expected response header from the server + // This is a path string in the expected response header from the server // for initiating file uploading. - std::string expected_upload_url_; + std::string expected_upload_path_; // These are content and its type in the expected response from the server. // See also HandleContentResponse below. @@ -131,7 +148,7 @@ class DriveApiOperationsTest : public testing::Test { private: void ResetExpectedResponse() { expected_data_file_path_.clear(); - expected_upload_url_.clear(); + expected_upload_path_.clear(); expected_content_type_.clear(); expected_content_.clear(); } @@ -177,13 +194,14 @@ class DriveApiOperationsTest : public testing::Test { // Returns the response based on set expected upload url. // The response contains the url in its "Location: " header. Also, it doesn't // have any content. - // To use this method, it is necessary to set |expected_upload_url_| + // To use this method, it is necessary to set |expected_upload_path_| // to the string representation of the url to be returned. scoped_ptr<test_server::HttpResponse> HandleInitiateUploadRequest( const test_server::HttpRequest& request) { - if (expected_upload_url_.empty()) { - // Expected upload url is not set. Delegate the processing to the next - // handler. + if (request.relative_url == expected_upload_path_ || + expected_upload_path_.empty()) { + // The request is for resume uploading or the expected upload url is not + // set. Delegate the processing to the next handler. return scoped_ptr<test_server::HttpResponse>(); } @@ -191,8 +209,95 @@ class DriveApiOperationsTest : public testing::Test { scoped_ptr<test_server::HttpResponse> response( new test_server::HttpResponse); + + // Check an ETag. + std::map<std::string, std::string>::const_iterator found = + request.headers.find("If-Match"); + if (found != request.headers.end() && + found->second != "*" && + found->second != kTestETag) { + response->set_code(test_server::PRECONDITION); + return response.Pass(); + } + + // Check if the X-Upload-Content-Length is present. If yes, store the + // length of the file. + found = request.headers.find("X-Upload-Content-Length"); + if (found == request.headers.end() || + !base::StringToInt64(found->second, &content_length_)) { + return scoped_ptr<test_server::HttpResponse>(); + } + received_bytes_ = 0; + response->set_code(test_server::SUCCESS); - response->AddCustomHeader("Location", expected_upload_url_); + response->AddCustomHeader( + "Location", + test_server_.base_url().Resolve(expected_upload_path_).spec()); + return response.Pass(); + } + + scoped_ptr<test_server::HttpResponse> HandleResumeUploadRequest( + const test_server::HttpRequest& request) { + if (request.relative_url != expected_upload_path_) { + // The request path is different from the expected path for uploading. + // Delegate the processing to the next handler. + return scoped_ptr<test_server::HttpResponse>(); + } + + http_request_ = request; + + if (!request.content.empty()) { + std::map<std::string, std::string>::const_iterator iter = + request.headers.find("Content-Range"); + if (iter == request.headers.end()) { + // The range must be set. + return scoped_ptr<test_server::HttpResponse>(); + } + + int64 length = 0; + int64 start_position = 0; + int64 end_position = 0; + if (!test_util::ParseContentRangeHeader( + iter->second, &start_position, &end_position, &length)) { + // Invalid "Content-Range" value. + return scoped_ptr<test_server::HttpResponse>(); + } + + EXPECT_EQ(start_position, received_bytes_); + EXPECT_EQ(length, content_length_); + + // end_position is inclusive, but so +1 to change the range to byte size. + received_bytes_ = end_position + 1; + } + + if (received_bytes_ < content_length_) { + scoped_ptr<test_server::HttpResponse> response( + new test_server::HttpResponse); + // Set RESUME INCOMPLETE (308) status code. + response->set_code(test_server::RESUME_INCOMPLETE); + + // Add Range header to the response, based on the values of + // Content-Range header in the request. + // The header is annotated only when at least one byte is received. + if (received_bytes_ > 0) { + response->AddCustomHeader( + "Range", "bytes=0-" + base::Int64ToString(received_bytes_ - 1)); + } + + return response.Pass(); + } + + // All bytes are received. Return the "success" response with the file's + // (dummy) metadata. + scoped_ptr<test_server::HttpResponse> response = + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("drive/file_entry.json")); + + // The response code is CREATED if it is new file uploading. + if (http_request_.relative_url == kTestUploadNewFilePath) { + response->set_code(test_server::CREATED); + } + return response.Pass(); } @@ -216,6 +321,10 @@ class DriveApiOperationsTest : public testing::Test { response->set_content(expected_content_); return response.Pass(); } + + // These are for the current upload file status. + int64 received_bytes_; + int64 content_length_; }; TEST_F(DriveApiOperationsTest, GetAboutOperation_ValidFeed) { @@ -430,17 +539,18 @@ TEST_F(DriveApiOperationsTest, DeleteResourceOperation) { EXPECT_FALSE(http_request_.has_content); } -TEST_F(DriveApiOperationsTest, InitiateUploadNewFileOperation) { +TEST_F(DriveApiOperationsTest, UploadNewFileOperation) { // Set an expected url for uploading. - expected_upload_url_ = kTestUploadUrl; + expected_upload_path_ = kTestUploadNewFilePath; const char kTestContentType[] = "text/plain"; - const int64 kTestContentLength = 100; + const std::string kTestContent(100, 'a'); GDataErrorCode error = GDATA_OTHER_ERROR; - GURL url; + GURL upload_url; - // Initiate uploading a new file to the directory with "parent_resource_id". + // 1) Initiate uploading a new file to the directory with + // "parent_resource_id". drive::InitiateUploadNewFileOperation* operation = new drive::InitiateUploadNewFileOperation( &operation_registry_, @@ -448,19 +558,19 @@ TEST_F(DriveApiOperationsTest, InitiateUploadNewFileOperation) { *url_generator_, base::FilePath(FILE_PATH_LITERAL("drive/file/path")), kTestContentType, - kTestContentLength, + kTestContent.size(), "parent_resource_id", // The resource id of the parent directory. "new file title", // The title of the file being uploaded. base::Bind(&test_util::CopyResultsFromInitiateUploadCallbackAndQuit, - &error, &url)); + &error, &upload_url)); operation->Start(kTestDriveApiAuthToken, kTestUserAgent, base::Bind(&test_util::DoNothingForReAuthenticateCallback)); MessageLoop::current()->Run(); EXPECT_EQ(HTTP_SUCCESS, error); - EXPECT_EQ(kTestUploadUrl, url.spec()); + EXPECT_EQ(kTestUploadNewFilePath, upload_url.path()); EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ(base::Int64ToString(kTestContentLength), + EXPECT_EQ(base::Int64ToString(kTestContent.size()), http_request_.headers["X-Upload-Content-Length"]); EXPECT_EQ(test_server::METHOD_POST, http_request_.method); @@ -475,19 +585,259 @@ TEST_F(DriveApiOperationsTest, InitiateUploadNewFileOperation) { "\"title\":\"new file title\"}", http_request_.content); - // Clean the operation remaining in |operation_registry_|. - operation_registry_.CancelAll(); + // 2) Upload the content to the upload URL. + scoped_refptr<net::IOBuffer> buffer = new net::StringIOBuffer(kTestContent); + + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + drive::ResumeUploadOperation* resume_operation = + new drive::ResumeUploadOperation( + &operation_registry_, + request_context_getter_.get(), + UPLOAD_NEW_FILE, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + upload_url, + 0, // start_position + kTestContent.size(), // end_position (exclusive) + kTestContent.size(), // content_length, + kTestContentType, + buffer, + base::Bind(&CopyResultFromUploadRangeCallbackAndQuit, + &response, &new_entry)); + + resume_operation->Start( + kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kTestContent.size() - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +TEST_F(DriveApiOperationsTest, UploadNewEmptyFileOperation) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadNewFilePath; + + const char kTestContentType[] = "text/plain"; + const char kTestContent[] = ""; + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // 1) Initiate uploading a new file to the directory with + // "parent_resource_id". + drive::InitiateUploadNewFileOperation* operation = + new drive::InitiateUploadNewFileOperation( + &operation_registry_, + request_context_getter_.get(), + *url_generator_, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + kTestContentType, + 0, + "parent_resource_id", // The resource id of the parent directory. + "new file title", // The title of the file being uploaded. + base::Bind(&test_util::CopyResultsFromInitiateUploadCallbackAndQuit, + &error, &upload_url)); + operation->Start(kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadNewFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ("0", http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_EQ(test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"parents\":[{" + "\"id\":\"parent_resource_id\"," + "\"kind\":\"drive#fileLink\"" + "}]," + "\"title\":\"new file title\"}", + http_request_.content); + + // 2) Upload the content to the upload URL. + scoped_refptr<net::IOBuffer> buffer = new net::StringIOBuffer(kTestContent); + + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + drive::ResumeUploadOperation* resume_operation = + new drive::ResumeUploadOperation( + &operation_registry_, + request_context_getter_.get(), + UPLOAD_NEW_FILE, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + upload_url, + 0, // start_position + 0, // end_position (exclusive) + 0, // content_length, + kTestContentType, + buffer, + base::Bind(&CopyResultFromUploadRangeCallbackAndQuit, + &response, &new_entry)); + + resume_operation->Start( + kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should NOT be added. + EXPECT_EQ(0U, http_request_.headers.count("Content-Range")); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); } -TEST_F(DriveApiOperationsTest, InitiateUploadExistingFileOperation) { +TEST_F(DriveApiOperationsTest, UploadNewLargeFileOperation) { // Set an expected url for uploading. - expected_upload_url_ = kTestUploadUrl; + expected_upload_path_ = kTestUploadNewFilePath; const char kTestContentType[] = "text/plain"; - const int64 kTestContentLength = 100; + const size_t kNumChunkBytes = 10; // Num bytes in a chunk. + const std::string kTestContent(100, 'a'); GDataErrorCode error = GDATA_OTHER_ERROR; - GURL url; + GURL upload_url; + + // 1) Initiate uploading a new file to the directory with + // "parent_resource_id". + drive::InitiateUploadNewFileOperation* operation = + new drive::InitiateUploadNewFileOperation( + &operation_registry_, + request_context_getter_.get(), + *url_generator_, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + kTestContentType, + kTestContent.size(), + "parent_resource_id", // The resource id of the parent directory. + "new file title", // The title of the file being uploaded. + base::Bind(&test_util::CopyResultsFromInitiateUploadCallbackAndQuit, + &error, &upload_url)); + operation->Start(kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadNewFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_EQ(test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"parents\":[{" + "\"id\":\"parent_resource_id\"," + "\"kind\":\"drive#fileLink\"" + "}]," + "\"title\":\"new file title\"}", + http_request_.content); + + // 2) Upload the content to the upload URL. + for (size_t start_position = 0; start_position < kTestContent.size(); + start_position += kNumChunkBytes) { + const std::string payload = kTestContent.substr( + start_position, + std::min(kNumChunkBytes, kTestContent.size() - start_position)); + const size_t end_position = start_position + payload.size(); + scoped_refptr<net::IOBuffer> buffer = new net::StringIOBuffer(payload); + + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + drive::ResumeUploadOperation* resume_operation = + new drive::ResumeUploadOperation( + &operation_registry_, + request_context_getter_.get(), + UPLOAD_NEW_FILE, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + upload_url, + start_position, + end_position, + kTestContent.size(), // content_length, + kTestContentType, + buffer, + base::Bind(&CopyResultFromUploadRangeCallbackAndQuit, + &response, &new_entry)); + + resume_operation->Start( + kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes " + + base::Int64ToString(start_position) + "-" + + base::Int64ToString(end_position - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(payload, http_request_.content); + + if (end_position == kTestContent.size()) { + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file + // The start and end positions should be set to -1, if an upload is + // complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); + } else { + // Check the response. + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_EQ(0, response.start_position_received); + EXPECT_EQ(static_cast<int64>(end_position), + response.end_position_received); + } + } +} + +TEST_F(DriveApiOperationsTest, UploadExistingFileOperation) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; // Initiate uploading a new file to the directory with "parent_resource_id". drive::InitiateUploadExistingFileOperation* operation = @@ -497,21 +847,21 @@ TEST_F(DriveApiOperationsTest, InitiateUploadExistingFileOperation) { *url_generator_, base::FilePath(FILE_PATH_LITERAL("drive/file/path")), kTestContentType, - kTestContentLength, + kTestContent.size(), "resource_id", // The resource id of the file to be overwritten. - "dummy-etag", + "", // No etag. base::Bind(&test_util::CopyResultsFromInitiateUploadCallbackAndQuit, - &error, &url)); + &error, &upload_url)); operation->Start(kTestDriveApiAuthToken, kTestUserAgent, base::Bind(&test_util::DoNothingForReAuthenticateCallback)); MessageLoop::current()->Run(); EXPECT_EQ(HTTP_SUCCESS, error); - EXPECT_EQ(kTestUploadUrl, url.spec()); + EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path()); EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ(base::Int64ToString(kTestContentLength), + EXPECT_EQ(base::Int64ToString(kTestContent.size()), http_request_.headers["X-Upload-Content-Length"]); - EXPECT_EQ("dummy-etag", http_request_.headers["If-Match"]); + EXPECT_EQ("*", http_request_.headers["If-Match"]); EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable", @@ -519,8 +869,176 @@ TEST_F(DriveApiOperationsTest, InitiateUploadExistingFileOperation) { EXPECT_TRUE(http_request_.has_content); EXPECT_TRUE(http_request_.content.empty()); - // Clean the operation remaining in |operation_registry_|. - operation_registry_.CancelAll(); + // 2) Upload the content to the upload URL. + scoped_refptr<net::IOBuffer> buffer = new net::StringIOBuffer(kTestContent); + + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + drive::ResumeUploadOperation* resume_operation = + new drive::ResumeUploadOperation( + &operation_registry_, + request_context_getter_.get(), + UPLOAD_EXISTING_FILE, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + upload_url, + 0, // start_position + kTestContent.size(), // end_position (exclusive) + kTestContent.size(), // content_length, + kTestContentType, + buffer, + base::Bind(&CopyResultFromUploadRangeCallbackAndQuit, + &response, &new_entry)); + + resume_operation->Start( + kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kTestContent.size() - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +TEST_F(DriveApiOperationsTest, UploadExistingFileOperationWithETag) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + drive::InitiateUploadExistingFileOperation* operation = + new drive::InitiateUploadExistingFileOperation( + &operation_registry_, + request_context_getter_.get(), + *url_generator_, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + kTestContentType, + kTestContent.size(), + "resource_id", // The resource id of the file to be overwritten. + kTestETag, + base::Bind(&test_util::CopyResultsFromInitiateUploadCallbackAndQuit, + &error, &upload_url)); + operation->Start(kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]); + + EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable", + http_request_.relative_url); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // 2) Upload the content to the upload URL. + scoped_refptr<net::IOBuffer> buffer = new net::StringIOBuffer(kTestContent); + + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + drive::ResumeUploadOperation* resume_operation = + new drive::ResumeUploadOperation( + &operation_registry_, + request_context_getter_.get(), + UPLOAD_EXISTING_FILE, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + upload_url, + 0, // start_position + kTestContent.size(), // end_position (exclusive) + kTestContent.size(), // content_length, + kTestContentType, + buffer, + base::Bind(&CopyResultFromUploadRangeCallbackAndQuit, + &response, &new_entry)); + + resume_operation->Start( + kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kTestContent.size() - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +TEST_F(DriveApiOperationsTest, UploadExistingFileOperationWithETagConflicting) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + drive::InitiateUploadExistingFileOperation* operation = + new drive::InitiateUploadExistingFileOperation( + &operation_registry_, + request_context_getter_.get(), + *url_generator_, + base::FilePath(FILE_PATH_LITERAL("drive/file/path")), + kTestContentType, + kTestContent.size(), + "resource_id", // The resource id of the file to be overwritten. + "Conflicting-etag", + base::Bind(&test_util::CopyResultsFromInitiateUploadCallbackAndQuit, + &error, &upload_url)); + operation->Start(kTestDriveApiAuthToken, kTestUserAgent, + base::Bind(&test_util::DoNothingForReAuthenticateCallback)); + MessageLoop::current()->Run(); + + EXPECT_EQ(HTTP_PRECONDITION, error); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + EXPECT_EQ("Conflicting-etag", http_request_.headers["If-Match"]); + + EXPECT_EQ(test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable", + http_request_.relative_url); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); } } // namespace google_apis diff --git a/chrome/browser/google_apis/drive_api_service.cc b/chrome/browser/google_apis/drive_api_service.cc index 1e058e5..c6a25d6 100644 --- a/chrome/browser/google_apis/drive_api_service.cc +++ b/chrome/browser/google_apis/drive_api_service.cc @@ -155,6 +155,34 @@ void ParseAppListAndRun(const google_apis::GetAppListCallback& callback, callback.Run(error, app_list.Pass()); } +// Parses the FileResource value to ResourceEntry for upload range operation, +// and runs |callback| on the UI thread. +void ParseResourceEntryForUploadRangeAndRun( + const UploadRangeCallback& callback, + const UploadRangeResponse& response, + scoped_ptr<FileResource> value) { + DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK(!callback.is_null()); + + if (!value) { + callback.Run(response, scoped_ptr<ResourceEntry>()); + return; + } + + // Converting to ResourceEntry is cheap enough to do on UI thread. + scoped_ptr<ResourceEntry> entry = + ResourceEntry::CreateFromFileResource(*value); + if (!entry) { + callback.Run(UploadRangeResponse(GDATA_PARSE_ERROR, + response.start_position_received, + response.end_position_received), + scoped_ptr<ResourceEntry>()); + return; + } + + callback.Run(response, entry.Pass()); +} + // The resource ID for the root directory for Drive API is defined in the spec: // https://developers.google.com/drive/folder const char kDriveApiRootDirectoryResourceId[] = "root"; @@ -509,8 +537,19 @@ void DriveAPIService::ResumeUpload( DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); - // TODO(kochi): Implement this. - NOTREACHED(); + runner_->StartOperationWithRetry( + new drive::ResumeUploadOperation( + operation_registry(), + url_request_context_getter_, + upload_mode, + drive_file_path, + upload_url, + start_position, + end_position, + content_length, + content_type, + buf, + base::Bind(&ParseResourceEntryForUploadRangeAndRun, callback))); } void DriveAPIService::GetUploadStatus( diff --git a/chrome/browser/google_apis/gdata_wapi_operations_unittest.cc b/chrome/browser/google_apis/gdata_wapi_operations_unittest.cc index 2041350..5f7c1a2 100644 --- a/chrome/browser/google_apis/gdata_wapi_operations_unittest.cc +++ b/chrome/browser/google_apis/gdata_wapi_operations_unittest.cc @@ -13,7 +13,6 @@ #include "base/message_loop_proxy.h" #include "base/stringprintf.h" #include "base/strings/string_number_conversions.h" -#include "base/strings/string_split.h" #include "chrome/browser/google_apis/gdata_wapi_operations.h" #include "chrome/browser/google_apis/gdata_wapi_parser.h" #include "chrome/browser/google_apis/gdata_wapi_url_generator.h" @@ -48,39 +47,6 @@ void CopyResultFromUploadRangeCallbackAndQuit( MessageLoop::current()->Quit(); } -// Parses a value of Content-Range header, which looks like -// "bytes <start_position>-<end_position>/<length>". -// Returns true on success. -bool ParseContentRangeHeader(const std::string& value, - int64* start_position, - int64* end_position, - int64* length) { - DCHECK(start_position); - DCHECK(end_position); - DCHECK(length); - - std::string remaining; - if (!test_util::RemovePrefix(value, "bytes ", &remaining)) - return false; - - std::vector<std::string> parts; - base::SplitString(remaining, '/', &parts); - if (parts.size() != 2U) - return false; - - const std::string range = parts[0]; - if (!base::StringToInt64(parts[1], length)) - return false; - - parts.clear(); - base::SplitString(range, '-', &parts); - if (parts.size() != 2U) - return false; - - return (base::StringToInt64(parts[0], start_position) && - base::StringToInt64(parts[1], end_position)); -} - class GDataWapiOperationsTest : public testing::Test { public: GDataWapiOperationsTest() @@ -304,10 +270,10 @@ class GDataWapiOperationsTest : public testing::Test { int64 length = 0; int64 start_position = 0; int64 end_position = 0; - if (!ParseContentRangeHeader(iter->second, - &start_position, - &end_position, - &length)) { + if (!test_util::ParseContentRangeHeader(iter->second, + &start_position, + &end_position, + &length)) { return scoped_ptr<test_server::HttpResponse>(); } EXPECT_EQ(start_position, received_bytes_); diff --git a/chrome/browser/google_apis/test_util.cc b/chrome/browser/google_apis/test_util.cc index b76f24f..11feb40 100644 --- a/chrome/browser/google_apis/test_util.cc +++ b/chrome/browser/google_apis/test_util.cc @@ -12,6 +12,8 @@ #include "base/pending_task.h" #include "base/string_util.h" #include "base/stringprintf.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" #include "base/threading/sequenced_worker_pool.h" #include "chrome/browser/google_apis/drive_api_parser.h" #include "chrome/browser/google_apis/gdata_wapi_operations.h" @@ -279,5 +281,35 @@ bool VerifyJsonData(const base::FilePath& expected_json_file_path, return true; } +bool ParseContentRangeHeader(const std::string& value, + int64* start_position, + int64* end_position, + int64* length) { + DCHECK(start_position); + DCHECK(end_position); + DCHECK(length); + + std::string remaining; + if (!RemovePrefix(value, "bytes ", &remaining)) + return false; + + std::vector<std::string> parts; + base::SplitString(remaining, '/', &parts); + if (parts.size() != 2U) + return false; + + const std::string range = parts[0]; + if (!base::StringToInt64(parts[1], length)) + return false; + + parts.clear(); + base::SplitString(range, '-', &parts); + if (parts.size() != 2U) + return false; + + return (base::StringToInt64(parts[0], start_position) && + base::StringToInt64(parts[1], end_position)); +} + } // namespace test_util } // namespace google_apis diff --git a/chrome/browser/google_apis/test_util.h b/chrome/browser/google_apis/test_util.h index fcd5b19..d482793 100644 --- a/chrome/browser/google_apis/test_util.h +++ b/chrome/browser/google_apis/test_util.h @@ -177,6 +177,14 @@ scoped_ptr<test_server::HttpResponse> HandleDownloadRequest( bool VerifyJsonData(const base::FilePath& expected_json_file_path, const base::Value* json_data); +// Parses a value of Content-Range header, which looks like +// "bytes <start_position>-<end_position>/<length>". +// Returns true on success. +bool ParseContentRangeHeader(const std::string& value, + int64* start_position, + int64* end_position, + int64* length); + } // namespace test_util } // namespace google_apis diff --git a/chrome/test/data/chromeos/drive/file_entry.json b/chrome/test/data/chromeos/drive/file_entry.json new file mode 100644 index 0000000..3a1f71a --- /dev/null +++ b/chrome/test/data/chromeos/drive/file_entry.json @@ -0,0 +1,49 @@ +{ + "kind": "drive#file", + "id": "0B4v7G8yEYAWHUmRrU2lMS2hLABC", + "etag": "\"WtRjAPZWbDA7_fkFjc5ojsEvDEF/MTM0MzM2NzgwMDIXYZ\"", + "selfLink": "https://www.googleapis.com/drive/v2/files/0B4v7G8yEYAWHUmRrU2lMS2hLABC", + "webContentLink": "https://docs.google.com/uc?id=0B4v7G8yEYAWHUmRrU2lMS2hLABC&export=download", + "alternateLink": "https://docs.google.com/file/d/0B4v7G8yEYAWHUmRrU2lMS2hLABC/edit", + "title": "My first file data", + "mimeType": "application/octet-stream", + "labels": { + "starred": false, + "hidden": false, + "trashed": false, + "restricted": false, + "viewed": true + }, + "createdDate": "2012-07-24T08:51:16.570Z", + "modifiedDate": "2012-07-27T05:43:20.269Z", + "modifiedByMeDate": "2012-07-27T05:43:20.269Z", + "lastViewedByMeDate": "2012-07-27T05:43:20.269Z", + "parents": [ + { + "kind": "drive#parentReference", + "id": "0B4v7G8yEYAWHYW1OcExsUVZLABC", + "selfLink": "https://www.googleapis.com/drive/v2/files/0B4v7G8yEYAWHNGpYVHJINEFFABC/parents/0B4v7G8yEYAWHYW1OcExsUVZLABC", + "parentLink": "https://www.googleapis.com/drive/v2/files/0B4v7G8yEYAWHYW1OcExsUVZLABC", + "isRoot": false + } + ], + "downloadUrl": "https://www.example.com/download", + "userPermission": { + "kind": "drive#permission", + "etag": "\"WtRjAPZWbDA7_fkFjc5ojsEvDEF/LKOJhhwatz2OKj-OblM3EvO8XYZ\"", + "id": "me", + "role": "owner", + "type": "user" + }, + "originalFilename": "Untitled Document.ext", + "fileExtension": "ext", + "md5Checksum": "d41d8cd98f00b204e9800998ecf8427e", + "fileSize": "1000", + "quotaBytesUsed": "1000", + "ownerNames": [ + "Test User" + ], + "lastModifyingUserName": "Test User", + "editable": true, + "writersCanShare": true +} |