1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "base/file_util.h"
6 #include "base/message_loop/message_loop.h"
7 #include "base/strings/string_number_conversions.h"
8 #include "base/test/test_file_util.h"
9 #include "content/browser/browser_thread_impl.h"
10 #include "content/browser/byte_stream.h"
11 #include "content/browser/download/download_create_info.h"
12 #include "content/browser/download/download_file_impl.h"
13 #include "content/browser/download/download_request_handle.h"
14 #include "content/public/browser/download_destination_observer.h"
15 #include "content/public/browser/download_interrupt_reasons.h"
16 #include "content/public/browser/download_manager.h"
17 #include "content/public/test/mock_download_manager.h"
18 #include "net/base/file_stream.h"
19 #include "net/base/mock_file_stream.h"
20 #include "net/base/net_errors.h"
21 #include "testing/gmock/include/gmock/gmock.h"
22 #include "testing/gtest/include/gtest/gtest.h"
25 using ::testing::AnyNumber
;
26 using ::testing::DoAll
;
27 using ::testing::InSequence
;
28 using ::testing::Return
;
29 using ::testing::SetArgPointee
;
30 using ::testing::StrictMock
;
35 class MockByteStreamReader
: public ByteStreamReader
{
37 MockByteStreamReader() {}
38 ~MockByteStreamReader() {}
40 // ByteStream functions
41 MOCK_METHOD2(Read
, ByteStreamReader::StreamState(
42 scoped_refptr
<net::IOBuffer
>*, size_t*));
43 MOCK_CONST_METHOD0(GetStatus
, int());
44 MOCK_METHOD1(RegisterCallback
, void(const base::Closure
&));
47 class MockDownloadDestinationObserver
: public DownloadDestinationObserver
{
49 MOCK_METHOD3(DestinationUpdate
, void(int64
, int64
, const std::string
&));
50 MOCK_METHOD1(DestinationError
, void(DownloadInterruptReason
));
51 MOCK_METHOD1(DestinationCompleted
, void(const std::string
&));
53 // Doesn't override any methods in the base class. Used to make sure
54 // that the last DestinationUpdate before a Destination{Completed,Error}
55 // had the right values.
56 MOCK_METHOD3(CurrentUpdateStatus
,
57 void(int64
, int64
, const std::string
&));
60 MATCHER(IsNullCallback
, "") { return (arg
.is_null()); }
64 class DownloadFileTest
: public testing::Test
{
67 static const char* kTestData1
;
68 static const char* kTestData2
;
69 static const char* kTestData3
;
70 static const char* kDataHash
;
71 static const uint32 kDummyDownloadId
;
72 static const int kDummyChildId
;
73 static const int kDummyRequestId
;
76 observer_(new StrictMock
<MockDownloadDestinationObserver
>),
77 observer_factory_(observer_
.get()),
82 ui_thread_(BrowserThread::UI
, &loop_
),
83 file_thread_(BrowserThread::FILE, &loop_
) {
86 virtual ~DownloadFileTest() {
89 void SetUpdateDownloadInfo(int64 bytes
, int64 bytes_per_sec
,
90 const std::string
& hash_state
) {
92 bytes_per_sec_
= bytes_per_sec
;
93 hash_state_
= hash_state
;
96 void ConfirmUpdateDownloadInfo() {
97 observer_
->CurrentUpdateStatus(bytes_
, bytes_per_sec_
, hash_state_
);
100 virtual void SetUp() {
101 EXPECT_CALL(*(observer_
.get()), DestinationUpdate(_
, _
, _
))
103 .WillRepeatedly(Invoke(this, &DownloadFileTest::SetUpdateDownloadInfo
));
106 // Mock calls to this function are forwarded here.
107 void RegisterCallback(base::Closure sink_callback
) {
108 sink_callback_
= sink_callback
;
111 void SetInterruptReasonCallback(bool* was_called
,
112 DownloadInterruptReason
* reason_p
,
113 DownloadInterruptReason reason
) {
118 bool CreateDownloadFile(int offset
, bool calculate_hash
) {
119 // There can be only one.
120 DCHECK(!download_file_
.get());
122 input_stream_
= new StrictMock
<MockByteStreamReader
>();
124 // TODO: Need to actually create a function that'll set the variables
125 // based on the inputs from the callback.
126 EXPECT_CALL(*input_stream_
, RegisterCallback(_
))
127 .WillOnce(Invoke(this, &DownloadFileTest::RegisterCallback
))
128 .RetiresOnSaturation();
130 scoped_ptr
<DownloadSaveInfo
> save_info(new DownloadSaveInfo());
131 download_file_
.reset(
132 new DownloadFileImpl(save_info
.Pass(),
137 scoped_ptr
<ByteStreamReader
>(input_stream_
),
139 observer_factory_
.GetWeakPtr()));
140 download_file_
->SetClientGuid(
141 "12345678-ABCD-1234-DCBA-123456789ABC");
143 EXPECT_CALL(*input_stream_
, Read(_
, _
))
144 .WillOnce(Return(ByteStreamReader::STREAM_EMPTY
))
145 .RetiresOnSaturation();
147 base::WeakPtrFactory
<DownloadFileTest
> weak_ptr_factory(this);
149 DownloadInterruptReason result
;
150 download_file_
->Initialize(base::Bind(
151 &DownloadFileTest::SetInterruptReasonCallback
,
152 weak_ptr_factory
.GetWeakPtr(), &called
, &result
));
153 loop_
.RunUntilIdle();
156 ::testing::Mock::VerifyAndClearExpectations(input_stream_
);
157 return result
== DOWNLOAD_INTERRUPT_REASON_NONE
;
160 void DestroyDownloadFile(int offset
) {
161 EXPECT_FALSE(download_file_
->InProgress());
163 // Make sure the data has been properly written to disk.
164 std::string disk_data
;
165 EXPECT_TRUE(base::ReadFileToString(download_file_
->FullPath(), &disk_data
));
166 EXPECT_EQ(expected_data_
, disk_data
);
168 // Make sure the Browser and File threads outlive the DownloadFile
169 // to satisfy thread checks inside it.
170 download_file_
.reset();
173 // Setup the stream to do be a data append; don't actually trigger
174 // the callback or do verifications.
175 void SetupDataAppend(const char **data_chunks
, size_t num_chunks
,
176 ::testing::Sequence s
) {
177 DCHECK(input_stream_
);
178 for (size_t i
= 0; i
< num_chunks
; i
++) {
179 const char *source_data
= data_chunks
[i
];
180 size_t length
= strlen(source_data
);
181 scoped_refptr
<net::IOBuffer
> data
= new net::IOBuffer(length
);
182 memcpy(data
->data(), source_data
, length
);
183 EXPECT_CALL(*input_stream_
, Read(_
, _
))
185 .WillOnce(DoAll(SetArgPointee
<0>(data
),
186 SetArgPointee
<1>(length
),
187 Return(ByteStreamReader::STREAM_HAS_DATA
)))
188 .RetiresOnSaturation();
189 expected_data_
+= source_data
;
193 void VerifyStreamAndSize() {
194 ::testing::Mock::VerifyAndClearExpectations(input_stream_
);
196 EXPECT_TRUE(base::GetFileSize(download_file_
->FullPath(), &size
));
197 EXPECT_EQ(expected_data_
.size(), static_cast<size_t>(size
));
200 // TODO(rdsmith): Manage full percentage issues properly.
201 void AppendDataToFile(const char **data_chunks
, size_t num_chunks
) {
202 ::testing::Sequence s1
;
203 SetupDataAppend(data_chunks
, num_chunks
, s1
);
204 EXPECT_CALL(*input_stream_
, Read(_
, _
))
206 .WillOnce(Return(ByteStreamReader::STREAM_EMPTY
))
207 .RetiresOnSaturation();
208 sink_callback_
.Run();
209 VerifyStreamAndSize();
212 void SetupFinishStream(DownloadInterruptReason interrupt_reason
,
213 ::testing::Sequence s
) {
214 EXPECT_CALL(*input_stream_
, Read(_
, _
))
216 .WillOnce(Return(ByteStreamReader::STREAM_COMPLETE
))
217 .RetiresOnSaturation();
218 EXPECT_CALL(*input_stream_
, GetStatus())
220 .WillOnce(Return(interrupt_reason
))
221 .RetiresOnSaturation();
222 EXPECT_CALL(*input_stream_
, RegisterCallback(_
))
223 .RetiresOnSaturation();
226 void FinishStream(DownloadInterruptReason interrupt_reason
,
227 bool check_observer
) {
228 ::testing::Sequence s1
;
229 SetupFinishStream(interrupt_reason
, s1
);
230 sink_callback_
.Run();
231 VerifyStreamAndSize();
232 if (check_observer
) {
233 EXPECT_CALL(*(observer_
.get()), DestinationCompleted(_
));
234 loop_
.RunUntilIdle();
235 ::testing::Mock::VerifyAndClearExpectations(observer_
.get());
236 EXPECT_CALL(*(observer_
.get()), DestinationUpdate(_
, _
, _
))
238 .WillRepeatedly(Invoke(this,
239 &DownloadFileTest::SetUpdateDownloadInfo
));
243 DownloadInterruptReason
RenameAndUniquify(
244 const base::FilePath
& full_path
,
245 base::FilePath
* result_path_p
) {
246 base::WeakPtrFactory
<DownloadFileTest
> weak_ptr_factory(this);
247 DownloadInterruptReason
result_reason(DOWNLOAD_INTERRUPT_REASON_NONE
);
248 bool callback_was_called(false);
249 base::FilePath result_path
;
251 download_file_
->RenameAndUniquify(
252 full_path
, base::Bind(&DownloadFileTest::SetRenameResult
,
253 weak_ptr_factory
.GetWeakPtr(),
254 &callback_was_called
,
255 &result_reason
, result_path_p
));
256 loop_
.RunUntilIdle();
258 EXPECT_TRUE(callback_was_called
);
259 return result_reason
;
262 DownloadInterruptReason
RenameAndAnnotate(
263 const base::FilePath
& full_path
,
264 base::FilePath
* result_path_p
) {
265 base::WeakPtrFactory
<DownloadFileTest
> weak_ptr_factory(this);
266 DownloadInterruptReason
result_reason(DOWNLOAD_INTERRUPT_REASON_NONE
);
267 bool callback_was_called(false);
268 base::FilePath result_path
;
270 download_file_
->RenameAndAnnotate(
271 full_path
, base::Bind(&DownloadFileTest::SetRenameResult
,
272 weak_ptr_factory
.GetWeakPtr(),
273 &callback_was_called
,
274 &result_reason
, result_path_p
));
275 loop_
.RunUntilIdle();
277 EXPECT_TRUE(callback_was_called
);
278 return result_reason
;
282 scoped_ptr
<StrictMock
<MockDownloadDestinationObserver
> > observer_
;
283 base::WeakPtrFactory
<DownloadDestinationObserver
> observer_factory_
;
285 // DownloadFile instance we are testing.
286 scoped_ptr
<DownloadFile
> download_file_
;
288 // Stream for sending data into the download file.
289 // Owned by download_file_; will be alive for lifetime of download_file_.
290 StrictMock
<MockByteStreamReader
>* input_stream_
;
292 // Sink callback data for stream.
293 base::Closure sink_callback_
;
295 // Latest update sent to the observer.
297 int64 bytes_per_sec_
;
298 std::string hash_state_
;
300 base::MessageLoop loop_
;
303 void SetRenameResult(bool* called_p
,
304 DownloadInterruptReason
* reason_p
,
305 base::FilePath
* result_path_p
,
306 DownloadInterruptReason reason
,
307 const base::FilePath
& result_path
) {
313 *result_path_p
= result_path
;
317 BrowserThreadImpl ui_thread_
;
318 // File thread to satisfy debug checks in DownloadFile.
319 BrowserThreadImpl file_thread_
;
321 // Keep track of what data should be saved to the disk file.
322 std::string expected_data_
;
325 const char* DownloadFileTest::kTestData1
=
326 "Let's write some data to the file!\n";
327 const char* DownloadFileTest::kTestData2
= "Writing more data.\n";
328 const char* DownloadFileTest::kTestData3
= "Final line.";
329 const char* DownloadFileTest::kDataHash
=
330 "CBF68BF10F8003DB86B31343AFAC8C7175BD03FB5FC905650F8C80AF087443A8";
332 const uint32
DownloadFileTest::kDummyDownloadId
= 23;
333 const int DownloadFileTest::kDummyChildId
= 3;
334 const int DownloadFileTest::kDummyRequestId
= 67;
336 // Rename the file before any data is downloaded, after some has, after it all
337 // has, and after it's closed.
338 TEST_F(DownloadFileTest
, RenameFileFinal
) {
339 ASSERT_TRUE(CreateDownloadFile(0, true));
340 base::FilePath
initial_path(download_file_
->FullPath());
341 EXPECT_TRUE(base::PathExists(initial_path
));
342 base::FilePath
path_1(initial_path
.InsertBeforeExtensionASCII("_1"));
343 base::FilePath
path_2(initial_path
.InsertBeforeExtensionASCII("_2"));
344 base::FilePath
path_3(initial_path
.InsertBeforeExtensionASCII("_3"));
345 base::FilePath
path_4(initial_path
.InsertBeforeExtensionASCII("_4"));
346 base::FilePath
path_5(initial_path
.InsertBeforeExtensionASCII("_5"));
347 base::FilePath output_path
;
349 // Rename the file before downloading any data.
350 EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE
,
351 RenameAndUniquify(path_1
, &output_path
));
352 base::FilePath renamed_path
= download_file_
->FullPath();
353 EXPECT_EQ(path_1
, renamed_path
);
354 EXPECT_EQ(path_1
, output_path
);
357 EXPECT_FALSE(base::PathExists(initial_path
));
358 EXPECT_TRUE(base::PathExists(path_1
));
360 // Download the data.
361 const char* chunks1
[] = { kTestData1
, kTestData2
};
362 AppendDataToFile(chunks1
, 2);
364 // Rename the file after downloading some data.
365 EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE
,
366 RenameAndUniquify(path_2
, &output_path
));
367 renamed_path
= download_file_
->FullPath();
368 EXPECT_EQ(path_2
, renamed_path
);
369 EXPECT_EQ(path_2
, output_path
);
372 EXPECT_FALSE(base::PathExists(path_1
));
373 EXPECT_TRUE(base::PathExists(path_2
));
375 const char* chunks2
[] = { kTestData3
};
376 AppendDataToFile(chunks2
, 1);
378 // Rename the file after downloading all the data.
379 EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE
,
380 RenameAndUniquify(path_3
, &output_path
));
381 renamed_path
= download_file_
->FullPath();
382 EXPECT_EQ(path_3
, renamed_path
);
383 EXPECT_EQ(path_3
, output_path
);
386 EXPECT_FALSE(base::PathExists(path_2
));
387 EXPECT_TRUE(base::PathExists(path_3
));
389 // Should not be able to get the hash until the file is closed.
391 EXPECT_FALSE(download_file_
->GetHash(&hash
));
392 FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE
, true);
393 loop_
.RunUntilIdle();
395 // Rename the file after downloading all the data and closing the file.
396 EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE
,
397 RenameAndUniquify(path_4
, &output_path
));
398 renamed_path
= download_file_
->FullPath();
399 EXPECT_EQ(path_4
, renamed_path
);
400 EXPECT_EQ(path_4
, output_path
);
403 EXPECT_FALSE(base::PathExists(path_3
));
404 EXPECT_TRUE(base::PathExists(path_4
));
407 EXPECT_TRUE(download_file_
->GetHash(&hash
));
408 EXPECT_EQ(kDataHash
, base::HexEncode(hash
.data(), hash
.size()));
410 // Check that a rename with overwrite to an existing file succeeds.
411 std::string file_contents
;
412 ASSERT_FALSE(base::PathExists(path_5
));
413 static const char file_data
[] = "xyzzy";
414 ASSERT_EQ(static_cast<int>(sizeof(file_data
) - 1),
415 base::WriteFile(path_5
, file_data
, sizeof(file_data
) - 1));
416 ASSERT_TRUE(base::PathExists(path_5
));
417 EXPECT_TRUE(base::ReadFileToString(path_5
, &file_contents
));
418 EXPECT_EQ(std::string(file_data
), file_contents
);
420 EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE
,
421 RenameAndAnnotate(path_5
, &output_path
));
422 EXPECT_EQ(path_5
, output_path
);
425 EXPECT_TRUE(base::ReadFileToString(path_5
, &file_contents
));
426 EXPECT_NE(std::string(file_data
), file_contents
);
428 DestroyDownloadFile(0);
431 // Test to make sure the rename uniquifies if we aren't overwriting
432 // and there's a file where we're aiming.
433 TEST_F(DownloadFileTest
, RenameUniquifies
) {
434 ASSERT_TRUE(CreateDownloadFile(0, true));
435 base::FilePath
initial_path(download_file_
->FullPath());
436 EXPECT_TRUE(base::PathExists(initial_path
));
437 base::FilePath
path_1(initial_path
.InsertBeforeExtensionASCII("_1"));
438 base::FilePath
path_1_suffixed(path_1
.InsertBeforeExtensionASCII(" (1)"));
440 ASSERT_FALSE(base::PathExists(path_1
));
441 static const char file_data
[] = "xyzzy";
442 ASSERT_EQ(static_cast<int>(sizeof(file_data
)),
443 base::WriteFile(path_1
, file_data
, sizeof(file_data
)));
444 ASSERT_TRUE(base::PathExists(path_1
));
446 EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE
, RenameAndUniquify(path_1
, NULL
));
447 EXPECT_TRUE(base::PathExists(path_1_suffixed
));
449 FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE
, true);
450 loop_
.RunUntilIdle();
451 DestroyDownloadFile(0);
454 // Test to make sure we get the proper error on failure.
455 TEST_F(DownloadFileTest
, RenameError
) {
456 ASSERT_TRUE(CreateDownloadFile(0, true));
457 base::FilePath
initial_path(download_file_
->FullPath());
459 // Create a subdirectory.
460 base::FilePath
tempdir(
461 initial_path
.DirName().Append(FILE_PATH_LITERAL("tempdir")));
462 ASSERT_TRUE(base::CreateDirectory(tempdir
));
463 base::FilePath
target_path(tempdir
.Append(initial_path
.BaseName()));
466 base::FilePath
target_path_suffixed(
467 target_path
.InsertBeforeExtensionASCII(" (1)"));
468 ASSERT_FALSE(base::PathExists(target_path
));
469 ASSERT_FALSE(base::PathExists(target_path_suffixed
));
471 // Make the directory unwritable and try to rename within it.
473 base::FilePermissionRestorer
restorer(tempdir
);
474 ASSERT_TRUE(base::MakeFileUnwritable(tempdir
));
476 // Expect nulling out of further processing.
477 EXPECT_CALL(*input_stream_
, RegisterCallback(IsNullCallback()));
478 EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED
,
479 RenameAndAnnotate(target_path
, NULL
));
480 EXPECT_FALSE(base::PathExists(target_path_suffixed
));
483 FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE
, true);
484 loop_
.RunUntilIdle();
485 DestroyDownloadFile(0);
488 // Various tests of the StreamActive method.
489 TEST_F(DownloadFileTest
, StreamEmptySuccess
) {
490 ASSERT_TRUE(CreateDownloadFile(0, true));
491 base::FilePath
initial_path(download_file_
->FullPath());
492 EXPECT_TRUE(base::PathExists(initial_path
));
494 // Test that calling the sink_callback_ on an empty stream shouldn't
496 AppendDataToFile(NULL
, 0);
498 // Finish the download this way and make sure we see it on the
500 EXPECT_CALL(*(observer_
.get()), DestinationCompleted(_
));
501 FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE
, false);
502 loop_
.RunUntilIdle();
504 DestroyDownloadFile(0);
507 TEST_F(DownloadFileTest
, StreamEmptyError
) {
508 ASSERT_TRUE(CreateDownloadFile(0, true));
509 base::FilePath
initial_path(download_file_
->FullPath());
510 EXPECT_TRUE(base::PathExists(initial_path
));
512 // Finish the download in error and make sure we see it on the
514 EXPECT_CALL(*(observer_
.get()),
516 DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED
))
517 .WillOnce(InvokeWithoutArgs(
518 this, &DownloadFileTest::ConfirmUpdateDownloadInfo
));
520 // If this next EXPECT_CALL fails flakily, it's probably a real failure.
521 // We'll be getting a stream of UpdateDownload calls from the timer, and
522 // the last one may have the correct information even if the failure
523 // doesn't produce an update, as the timer update may have triggered at the
525 EXPECT_CALL(*(observer_
.get()), CurrentUpdateStatus(0, _
, _
));
527 FinishStream(DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED
, false);
529 loop_
.RunUntilIdle();
531 DestroyDownloadFile(0);
534 TEST_F(DownloadFileTest
, StreamNonEmptySuccess
) {
535 ASSERT_TRUE(CreateDownloadFile(0, true));
536 base::FilePath
initial_path(download_file_
->FullPath());
537 EXPECT_TRUE(base::PathExists(initial_path
));
539 const char* chunks1
[] = { kTestData1
, kTestData2
};
540 ::testing::Sequence s1
;
541 SetupDataAppend(chunks1
, 2, s1
);
542 SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NONE
, s1
);
543 EXPECT_CALL(*(observer_
.get()), DestinationCompleted(_
));
544 sink_callback_
.Run();
545 VerifyStreamAndSize();
546 loop_
.RunUntilIdle();
547 DestroyDownloadFile(0);
550 TEST_F(DownloadFileTest
, StreamNonEmptyError
) {
551 ASSERT_TRUE(CreateDownloadFile(0, true));
552 base::FilePath
initial_path(download_file_
->FullPath());
553 EXPECT_TRUE(base::PathExists(initial_path
));
555 const char* chunks1
[] = { kTestData1
, kTestData2
};
556 ::testing::Sequence s1
;
557 SetupDataAppend(chunks1
, 2, s1
);
558 SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED
, s1
);
560 EXPECT_CALL(*(observer_
.get()),
562 DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED
))
563 .WillOnce(InvokeWithoutArgs(
564 this, &DownloadFileTest::ConfirmUpdateDownloadInfo
));
566 // If this next EXPECT_CALL fails flakily, it's probably a real failure.
567 // We'll be getting a stream of UpdateDownload calls from the timer, and
568 // the last one may have the correct information even if the failure
569 // doesn't produce an update, as the timer update may have triggered at the
571 EXPECT_CALL(*(observer_
.get()),
572 CurrentUpdateStatus(strlen(kTestData1
) + strlen(kTestData2
),
575 sink_callback_
.Run();
576 loop_
.RunUntilIdle();
577 VerifyStreamAndSize();
578 DestroyDownloadFile(0);
581 // Send some data, wait 3/4s of a second, run the message loop, and
582 // confirm the values the observer received are correct.
583 TEST_F(DownloadFileTest
, ConfirmUpdate
) {
584 CreateDownloadFile(0, true);
586 const char* chunks1
[] = { kTestData1
, kTestData2
};
587 AppendDataToFile(chunks1
, 2);
589 // Run the message loops for 750ms and check for results.
590 loop_
.PostDelayedTask(FROM_HERE
,
591 base::MessageLoop::QuitClosure(),
592 base::TimeDelta::FromMilliseconds(750));
595 EXPECT_EQ(static_cast<int64
>(strlen(kTestData1
) + strlen(kTestData2
)),
597 EXPECT_EQ(download_file_
->GetHashState(), hash_state_
);
599 FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE
, true);
600 DestroyDownloadFile(0);
603 } // namespace content