| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/download/download_file_impl.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/files/file_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "content/browser/byte_stream.h" |
| #include "content/browser/download/download_create_info.h" |
| #include "content/browser/download/download_interrupt_reasons_impl.h" |
| #include "content/browser/download/download_net_log_parameters.h" |
| #include "content/browser/download/download_stats.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/download_destination_observer.h" |
| #include "net/base/io_buffer.h" |
| |
| namespace content { |
| |
| const int kUpdatePeriodMs = 500; |
| const int kMaxTimeBlockingFileThreadMs = 1000; |
| |
| // These constants control the default retry behavior for failing renames. Each |
| // retry is performed after a delay that is twice the previous delay. The |
| // initial delay is specified by kInitialRenameRetryDelayMs. |
| const int kMaxRenameRetries = 3; |
| const int kInitialRenameRetryDelayMs = 200; |
| |
| int DownloadFile::number_active_objects_ = 0; |
| |
| DownloadFileImpl::DownloadFileImpl( |
| const DownloadSaveInfo& save_info, |
| const base::FilePath& default_download_directory, |
| const GURL& url, |
| const GURL& referrer_url, |
| bool calculate_hash, |
| base::File file, |
| scoped_ptr<ByteStreamReader> stream, |
| const net::BoundNetLog& bound_net_log, |
| base::WeakPtr<DownloadDestinationObserver> observer) |
| : file_(save_info.file_path, |
| url, |
| referrer_url, |
| save_info.offset, |
| calculate_hash, |
| save_info.hash_state, |
| std::move(file), |
| bound_net_log), |
| default_download_directory_(default_download_directory), |
| stream_reader_(std::move(stream)), |
| bytes_seen_(0), |
| bound_net_log_(bound_net_log), |
| observer_(observer), |
| weak_factory_(this) {} |
| |
| DownloadFileImpl::~DownloadFileImpl() { |
| DCHECK_CURRENTLY_ON(BrowserThread::FILE); |
| --number_active_objects_; |
| } |
| |
| void DownloadFileImpl::Initialize(const InitializeCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::FILE); |
| |
| update_timer_.reset(new base::RepeatingTimer()); |
| DownloadInterruptReason result = |
| file_.Initialize(default_download_directory_); |
| if (result != DOWNLOAD_INTERRUPT_REASON_NONE) { |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, base::Bind(callback, result)); |
| return; |
| } |
| |
| stream_reader_->RegisterCallback( |
| base::Bind(&DownloadFileImpl::StreamActive, weak_factory_.GetWeakPtr())); |
| |
| download_start_ = base::TimeTicks::Now(); |
| |
| // Primarily to make reset to zero in restart visible to owner. |
| SendUpdate(); |
| |
| // Initial pull from the straw. |
| StreamActive(); |
| |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, base::Bind( |
| callback, DOWNLOAD_INTERRUPT_REASON_NONE)); |
| |
| ++number_active_objects_; |
| } |
| |
| DownloadInterruptReason DownloadFileImpl::AppendDataToFile( |
| const char* data, size_t data_len) { |
| DCHECK_CURRENTLY_ON(BrowserThread::FILE); |
| |
| if (!update_timer_->IsRunning()) { |
| update_timer_->Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kUpdatePeriodMs), |
| this, &DownloadFileImpl::SendUpdate); |
| } |
| rate_estimator_.Increment(data_len); |
| return file_.AppendDataToFile(data, data_len); |
| } |
| |
| void DownloadFileImpl::RenameAndUniquify( |
| const base::FilePath& full_path, |
| const RenameCompletionCallback& callback) { |
| RenameWithRetryInternal( |
| full_path, UNIQUIFY, kMaxRenameRetries, base::TimeTicks(), callback); |
| } |
| |
| void DownloadFileImpl::RenameAndAnnotate( |
| const base::FilePath& full_path, |
| const RenameCompletionCallback& callback) { |
| RenameWithRetryInternal(full_path, |
| ANNOTATE_WITH_SOURCE_INFORMATION, |
| kMaxRenameRetries, |
| base::TimeTicks(), |
| callback); |
| } |
| |
| base::TimeDelta DownloadFileImpl::GetRetryDelayForFailedRename( |
| int attempt_number) { |
| DCHECK_GE(attempt_number, 0); |
| // |delay| starts at kInitialRenameRetryDelayMs and increases by a factor of |
| // 2 at each subsequent retry. Assumes that |retries_left| starts at |
| // kMaxRenameRetries. Also assumes that kMaxRenameRetries is less than the |
| // number of bits in an int. |
| return base::TimeDelta::FromMilliseconds(kInitialRenameRetryDelayMs) * |
| (1 << attempt_number); |
| } |
| |
| bool DownloadFileImpl::ShouldRetryFailedRename(DownloadInterruptReason reason) { |
| return reason == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR; |
| } |
| |
| void DownloadFileImpl::RenameWithRetryInternal( |
| const base::FilePath& full_path, |
| RenameOption option, |
| int retries_left, |
| base::TimeTicks time_of_first_failure, |
| const RenameCompletionCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::FILE); |
| |
| base::FilePath new_path(full_path); |
| |
| if ((option & UNIQUIFY) && full_path != file_.full_path()) { |
| int uniquifier = |
| base::GetUniquePathNumber(new_path, base::FilePath::StringType()); |
| if (uniquifier > 0) |
| new_path = new_path.InsertBeforeExtensionASCII( |
| base::StringPrintf(" (%d)", uniquifier)); |
| } |
| |
| DownloadInterruptReason reason = file_.Rename(new_path); |
| |
| // Attempt to retry the rename if possible. If the rename failed and the |
| // subsequent open also failed, then in_progress() would be false. We don't |
| // try to retry renames if the in_progress() was false to begin with since we |
| // have less assurance that the file at file_.full_path() was the one we were |
| // working with. |
| if (ShouldRetryFailedRename(reason) && file_.in_progress() && |
| retries_left > 0) { |
| int attempt_number = kMaxRenameRetries - retries_left; |
| BrowserThread::PostDelayedTask( |
| BrowserThread::FILE, |
| FROM_HERE, |
| base::Bind(&DownloadFileImpl::RenameWithRetryInternal, |
| weak_factory_.GetWeakPtr(), |
| full_path, |
| option, |
| --retries_left, |
| time_of_first_failure.is_null() ? base::TimeTicks::Now() |
| : time_of_first_failure, |
| callback), |
| GetRetryDelayForFailedRename(attempt_number)); |
| return; |
| } |
| |
| if (!time_of_first_failure.is_null()) |
| RecordDownloadFileRenameResultAfterRetry( |
| base::TimeTicks::Now() - time_of_first_failure, reason); |
| |
| if (reason == DOWNLOAD_INTERRUPT_REASON_NONE && |
| (option & ANNOTATE_WITH_SOURCE_INFORMATION)) { |
| // Doing the annotation after the rename rather than before leaves |
| // a very small window during which the file has the final name but |
| // hasn't been marked with the Mark Of The Web. However, it allows |
| // anti-virus scanners on Windows to actually see the data |
| // (http://crbug.com/127999) under the correct name (which is information |
| // it uses). |
| reason = file_.AnnotateWithSourceInformation(); |
| } |
| |
| if (reason != DOWNLOAD_INTERRUPT_REASON_NONE) { |
| // Make sure our information is updated, since we're about to |
| // error out. |
| SendUpdate(); |
| |
| // Null out callback so that we don't do any more stream processing. |
| stream_reader_->RegisterCallback(base::Closure()); |
| |
| new_path.clear(); |
| } |
| |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(callback, reason, new_path)); |
| } |
| |
| void DownloadFileImpl::Detach() { |
| file_.Detach(); |
| } |
| |
| void DownloadFileImpl::Cancel() { |
| file_.Cancel(); |
| } |
| |
| base::FilePath DownloadFileImpl::FullPath() const { |
| return file_.full_path(); |
| } |
| |
| bool DownloadFileImpl::InProgress() const { |
| return file_.in_progress(); |
| } |
| |
| int64_t DownloadFileImpl::CurrentSpeed() const { |
| return rate_estimator_.GetCountPerSecond(); |
| } |
| |
| bool DownloadFileImpl::GetHash(std::string* hash) { |
| return file_.GetHash(hash); |
| } |
| |
| std::string DownloadFileImpl::GetHashState() { |
| return file_.GetHashState(); |
| } |
| |
| void DownloadFileImpl::SetClientGuid(const std::string& guid) { |
| file_.SetClientGuid(guid); |
| } |
| |
| void DownloadFileImpl::StreamActive() { |
| base::TimeTicks start(base::TimeTicks::Now()); |
| base::TimeTicks now; |
| scoped_refptr<net::IOBuffer> incoming_data; |
| size_t incoming_data_size = 0; |
| size_t total_incoming_data_size = 0; |
| size_t num_buffers = 0; |
| ByteStreamReader::StreamState state(ByteStreamReader::STREAM_EMPTY); |
| DownloadInterruptReason reason = DOWNLOAD_INTERRUPT_REASON_NONE; |
| base::TimeDelta delta( |
| base::TimeDelta::FromMilliseconds(kMaxTimeBlockingFileThreadMs)); |
| |
| // Take care of any file local activity required. |
| do { |
| state = stream_reader_->Read(&incoming_data, &incoming_data_size); |
| |
| switch (state) { |
| case ByteStreamReader::STREAM_EMPTY: |
| break; |
| case ByteStreamReader::STREAM_HAS_DATA: |
| { |
| ++num_buffers; |
| base::TimeTicks write_start(base::TimeTicks::Now()); |
| reason = AppendDataToFile( |
| incoming_data.get()->data(), incoming_data_size); |
| disk_writes_time_ += (base::TimeTicks::Now() - write_start); |
| bytes_seen_ += incoming_data_size; |
| total_incoming_data_size += incoming_data_size; |
| } |
| break; |
| case ByteStreamReader::STREAM_COMPLETE: |
| { |
| reason = static_cast<DownloadInterruptReason>( |
| stream_reader_->GetStatus()); |
| SendUpdate(); |
| base::TimeTicks close_start(base::TimeTicks::Now()); |
| if (reason == DOWNLOAD_INTERRUPT_REASON_NONE) |
| file_.Finish(); |
| else |
| file_.FinishWithError(); |
| base::TimeTicks now(base::TimeTicks::Now()); |
| disk_writes_time_ += (now - close_start); |
| RecordFileBandwidth( |
| bytes_seen_, disk_writes_time_, now - download_start_); |
| update_timer_.reset(); |
| } |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| now = base::TimeTicks::Now(); |
| } while (state == ByteStreamReader::STREAM_HAS_DATA && |
| reason == DOWNLOAD_INTERRUPT_REASON_NONE && |
| now - start <= delta); |
| |
| // If we're stopping to yield the thread, post a task so we come back. |
| if (state == ByteStreamReader::STREAM_HAS_DATA && |
| now - start > delta) { |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| base::Bind(&DownloadFileImpl::StreamActive, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| if (total_incoming_data_size) |
| RecordFileThreadReceiveBuffers(num_buffers); |
| |
| RecordContiguousWriteTime(now - start); |
| |
| // Take care of communication with our observer. |
| if (reason != DOWNLOAD_INTERRUPT_REASON_NONE) { |
| // Error case for both upstream source and file write. |
| // Shut down processing and signal an error to our observer. |
| // Our observer will clean us up. |
| stream_reader_->RegisterCallback(base::Closure()); |
| weak_factory_.InvalidateWeakPtrs(); |
| SendUpdate(); // Make info up to date before error. |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&DownloadDestinationObserver::DestinationError, |
| observer_, reason)); |
| } else if (state == ByteStreamReader::STREAM_COMPLETE) { |
| // Signal successful completion and shut down processing. |
| stream_reader_->RegisterCallback(base::Closure()); |
| weak_factory_.InvalidateWeakPtrs(); |
| std::string hash; |
| if (!GetHash(&hash) || file_.IsEmptyHash(hash)) |
| hash.clear(); |
| SendUpdate(); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind( |
| &DownloadDestinationObserver::DestinationCompleted, |
| observer_, hash)); |
| } |
| if (bound_net_log_.IsCapturing()) { |
| bound_net_log_.AddEvent( |
| net::NetLog::TYPE_DOWNLOAD_STREAM_DRAINED, |
| base::Bind(&FileStreamDrainedNetLogCallback, total_incoming_data_size, |
| num_buffers)); |
| } |
| } |
| |
| void DownloadFileImpl::SendUpdate() { |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&DownloadDestinationObserver::DestinationUpdate, |
| observer_, file_.bytes_so_far(), CurrentSpeed(), |
| GetHashState())); |
| } |
| |
| // static |
| int DownloadFile::GetNumberOfDownloadFiles() { |
| return number_active_objects_; |
| } |
| |
| } // namespace content |