blob: 4c5f42d177197840ebc3ec4e6beb0db2e81d7ae2 [file] [log] [blame]
// Copyright (c) 2006-2008 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 "chrome/browser/download/download_file.h"
#include "base/file_util.h"
#include "base/path_service.h"
#include "base/stl_util-inl.h"
#include "base/task.h"
#include "base/thread.h"
#include "base/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/chrome_thread.h"
#include "chrome/browser/download/download_manager.h"
#include "chrome/browser/download/download_util.h"
#include "chrome/browser/net/chrome_url_request_context.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/renderer_host/resource_dispatcher_host.h"
#include "chrome/browser/tab_contents/tab_util.h"
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/platform_util.h"
#include "googleurl/src/gurl.h"
#include "net/base/io_buffer.h"
#include "net/base/net_util.h"
#include "net/url_request/url_request_context.h"
#if defined(OS_WIN)
#include "app/win_util.h"
#include "chrome/common/win_safe_util.h"
#elif defined(OS_MACOSX)
#include "chrome/browser/cocoa/file_metadata.h"
#endif
// Throttle updates to the UI thread so that a fast moving download doesn't
// cause it to become unresponsive (in milliseconds).
static const int kUpdatePeriodMs = 500;
// Timer task for posting UI updates. This task is created and maintained by
// the DownloadFileManager long as there is an in progress download. The task
// is cancelled when all active downloads have completed, or in the destructor
// of the DownloadFileManager.
class DownloadFileUpdateTask : public Task {
public:
explicit DownloadFileUpdateTask(DownloadFileManager* manager)
: manager_(manager) {}
virtual void Run() {
manager_->UpdateInProgressDownloads();
}
private:
DownloadFileManager* manager_;
DISALLOW_COPY_AND_ASSIGN(DownloadFileUpdateTask);
};
// DownloadFile implementation -------------------------------------------------
DownloadFile::DownloadFile(const DownloadCreateInfo* info)
: file_stream_(info->save_info.file_stream),
source_url_(info->url),
referrer_url_(info->referrer_url),
id_(info->download_id),
child_id_(info->child_id),
render_view_id_(info->render_view_id),
request_id_(info->request_id),
bytes_so_far_(0),
full_path_(info->save_info.file_path),
path_renamed_(false),
in_progress_(true),
dont_sleep_(true),
save_info_(info->save_info) {
}
DownloadFile::~DownloadFile() {
Close();
}
bool DownloadFile::Initialize() {
if (!full_path_.empty() ||
download_util::CreateTemporaryFileForDownload(&full_path_))
return Open();
return false;
}
bool DownloadFile::AppendDataToFile(const char* data, int data_len) {
if (file_stream_.get()) {
// FIXME bug 595247: handle errors on file writes.
size_t written = file_stream_->Write(data, data_len, NULL);
bytes_so_far_ += written;
return true;
}
return false;
}
void DownloadFile::Cancel() {
Close();
if (!full_path_.empty())
file_util::Delete(full_path_, false);
}
// The UI has provided us with our finalized name.
bool DownloadFile::Rename(const FilePath& new_path) {
// If the new path is same as the old one, there is no need to perform the
// following renaming logic.
if (new_path == full_path_) {
path_renamed_ = true;
// Don't close the file if we're not done (finished or canceled).
if (!in_progress_)
Close();
return true;
}
Close();
#if defined(OS_WIN)
// We cannot rename because rename will keep the same security descriptor
// on the destination file. We want to recreate the security descriptor
// with the security that makes sense in the new path.
if (!file_util::RenameFileAndResetSecurityDescriptor(full_path_, new_path))
return false;
#elif defined(OS_POSIX)
{
// Similarly, on Unix, we're moving a temp file created with permissions
// 600 to |new_path|. Here, we try to fix up the destination file with
// appropriate permissions.
struct stat st;
// First check the file existence and create an empty file if it doesn't
// exist.
if (!file_util::PathExists(new_path))
file_util::WriteFile(new_path, "", 0);
bool stat_succeeded = (stat(new_path.value().c_str(), &st) == 0);
// TODO(estade): Move() falls back to copying and deleting when a simple
// rename fails. Copying sucks for large downloads. crbug.com/8737
if (!file_util::Move(full_path_, new_path))
return false;
if (stat_succeeded)
chmod(new_path.value().c_str(), st.st_mode);
}
#endif
full_path_ = new_path;
path_renamed_ = true;
// We don't need to re-open the file if we're done (finished or canceled).
if (!in_progress_)
return true;
if (!Open())
return false;
// Move to the end of the new file.
if (file_stream_->Seek(net::FROM_END, 0) < 0)
return false;
return true;
}
void DownloadFile::Close() {
if (file_stream_.get()) {
file_stream_->Close();
file_stream_.reset();
}
}
bool DownloadFile::Open() {
DCHECK(!full_path_.empty());
// Create a new file steram if it is not provided.
if (!file_stream_.get()) {
file_stream_.reset(new net::FileStream);
if (file_stream_->Open(full_path_,
base::PLATFORM_FILE_OPEN_ALWAYS |
base::PLATFORM_FILE_WRITE) != net::OK) {
file_stream_.reset();
return false;
}
}
#if defined(OS_WIN)
AnnotateWithSourceInformation();
#endif
return true;
}
void DownloadFile::AnnotateWithSourceInformation() {
#if defined(OS_WIN)
// Sets the Zone to tell Windows that this file comes from the internet.
// We ignore the return value because a failure is not fatal.
win_util::SetInternetZoneIdentifier(full_path_);
#elif defined(OS_MACOSX)
file_metadata::AddQuarantineMetadataToFile(full_path_, source_url_,
referrer_url_);
file_metadata::AddOriginMetadataToFile(full_path_, source_url_,
referrer_url_);
#endif
}
// DownloadFileManager implementation ------------------------------------------
DownloadFileManager::DownloadFileManager(ResourceDispatcherHost* rdh)
: next_id_(0),
resource_dispatcher_host_(rdh) {
}
DownloadFileManager::~DownloadFileManager() {
// Check for clean shutdown.
DCHECK(downloads_.empty());
ui_progress_.clear();
}
// Called during the browser shutdown process to clean up any state (open files,
// timers) that live on the download_thread_.
void DownloadFileManager::Shutdown() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
StopUpdateTimer();
ChromeThread::PostTask(
ChromeThread::FILE, FROM_HERE,
NewRunnableMethod(this, &DownloadFileManager::OnShutdown));
}
// Cease download thread operations.
void DownloadFileManager::OnShutdown() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
// Delete any partial downloads during shutdown.
for (DownloadFileMap::iterator it = downloads_.begin();
it != downloads_.end(); ++it) {
DownloadFile* download = it->second;
if (download->in_progress())
download->Cancel();
delete download;
}
downloads_.clear();
}
// Lookup one in-progress download.
DownloadFile* DownloadFileManager::LookupDownload(int id) {
DownloadFileMap::iterator it = downloads_.find(id);
return it == downloads_.end() ? NULL : it->second;
}
// The UI progress is updated on the file thread and removed on the UI thread.
void DownloadFileManager::RemoveDownloadFromUIProgress(int id) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
AutoLock lock(progress_lock_);
if (ui_progress_.find(id) != ui_progress_.end())
ui_progress_.erase(id);
}
// Throttle updates to the UI thread by only posting update notifications at a
// regularly controlled interval.
void DownloadFileManager::StartUpdateTimer() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
if (!update_timer_.IsRunning()) {
update_timer_.Start(base::TimeDelta::FromMilliseconds(kUpdatePeriodMs),
this, &DownloadFileManager::UpdateInProgressDownloads);
}
}
void DownloadFileManager::StopUpdateTimer() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
update_timer_.Stop();
}
// Called on the IO thread once the ResourceDispatcherHost has decided that a
// request is a download.
int DownloadFileManager::GetNextId() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO));
return next_id_++;
}
// Notifications sent from the IO thread and run on the download thread:
// The IO thread created 'info', but the download thread (this method) uses it
// to create a DownloadFile, then passes 'info' to the UI thread where it is
// finally consumed and deleted.
void DownloadFileManager::StartDownload(DownloadCreateInfo* info) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
DCHECK(info);
DownloadFile* download = new DownloadFile(info);
if (!download->Initialize()) {
// Couldn't open, cancel the operation. The UI thread does not yet know
// about this download so we have to clean up 'info'. We need to get back
// to the IO thread to cancel the network request and CancelDownloadRequest
// on the UI thread is the safe way to do that.
ChromeThread::PostTask(
ChromeThread::IO, FROM_HERE,
NewRunnableFunction(&DownloadManager::OnCancelDownloadRequest,
resource_dispatcher_host_,
info->child_id,
info->request_id));
delete info;
delete download;
return;
}
DCHECK(LookupDownload(info->download_id) == NULL);
downloads_[info->download_id] = download;
info->path = download->full_path();
{
AutoLock lock(progress_lock_);
ui_progress_[info->download_id] = info->received_bytes;
}
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(this, &DownloadFileManager::OnStartDownload, info));
}
// We don't forward an update to the UI thread here, since we want to throttle
// the UI update rate via a periodic timer. If the user has cancelled the
// download (in the UI thread), we may receive a few more updates before the IO
// thread gets the cancel message: we just delete the data since the
// DownloadFile has been deleted.
void DownloadFileManager::UpdateDownload(int id, DownloadBuffer* buffer) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
std::vector<DownloadBuffer::Contents> contents;
{
AutoLock auto_lock(buffer->lock);
contents.swap(buffer->contents);
}
DownloadFile* download = LookupDownload(id);
for (size_t i = 0; i < contents.size(); ++i) {
net::IOBuffer* data = contents[i].first;
const int data_len = contents[i].second;
if (download)
download->AppendDataToFile(data->data(), data_len);
data->Release();
}
if (download) {
AutoLock lock(progress_lock_);
ui_progress_[download->id()] = download->bytes_so_far();
}
}
void DownloadFileManager::DownloadFinished(int id, DownloadBuffer* buffer) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
delete buffer;
DownloadFileMap::iterator it = downloads_.find(id);
if (it != downloads_.end()) {
DownloadFile* download = it->second;
download->set_in_progress(false);
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(
this, &DownloadFileManager::OnDownloadFinished,
id, download->bytes_so_far()));
// We need to keep the download around until the UI thread has finalized
// the name.
if (download->path_renamed()) {
downloads_.erase(it);
delete download;
}
}
if (downloads_.empty())
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(this, &DownloadFileManager::StopUpdateTimer));
}
// This method will be sent via a user action, or shutdown on the UI thread, and
// run on the download thread. Since this message has been sent from the UI
// thread, the download may have already completed and won't exist in our map.
void DownloadFileManager::CancelDownload(int id) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
DownloadFileMap::iterator it = downloads_.find(id);
if (it != downloads_.end()) {
DownloadFile* download = it->second;
download->set_in_progress(false);
download->Cancel();
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(
this, &DownloadFileManager::RemoveDownloadFromUIProgress,
download->id()));
if (download->path_renamed()) {
downloads_.erase(it);
delete download;
}
}
if (downloads_.empty()) {
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(this, &DownloadFileManager::StopUpdateTimer));
}
}
// Our periodic timer has fired so send the UI thread updates on all in progress
// downloads.
void DownloadFileManager::UpdateInProgressDownloads() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
AutoLock lock(progress_lock_);
ProgressMap::iterator it = ui_progress_.begin();
for (; it != ui_progress_.end(); ++it) {
const int id = it->first;
DownloadManager* manager = LookupManager(id);
if (manager)
manager->UpdateDownload(id, it->second);
}
}
// Notifications sent from the download thread and run on the UI thread.
// Lookup the DownloadManager for this TabContents' profile and inform it of
// a new download.
// TODO(paulg): When implementing download restart via the Downloads tab,
// there will be no 'render_process_id' or 'render_view_id'.
void DownloadFileManager::OnStartDownload(DownloadCreateInfo* info) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
DownloadManager* manager = DownloadManagerFromRenderIds(info->child_id,
info->render_view_id);
if (!manager) {
ChromeThread::PostTask(
ChromeThread::IO, FROM_HERE,
NewRunnableFunction(&DownloadManager::OnCancelDownloadRequest,
resource_dispatcher_host_,
info->child_id,
info->request_id));
delete info;
return;
}
StartUpdateTimer();
// Add the download manager to our request maps for future updates. We want to
// be able to cancel all in progress downloads when a DownloadManager is
// deleted, such as when a profile is closed. We also want to be able to look
// up the DownloadManager associated with a given request without having to
// rely on using tab information, since a tab may be closed while a download
// initiated from that tab is still in progress.
DownloadRequests& downloads = requests_[manager];
downloads.insert(info->download_id);
// TODO(paulg): The manager will exist when restarts are implemented.
DownloadManagerMap::iterator dit = managers_.find(info->download_id);
if (dit == managers_.end())
managers_[info->download_id] = manager;
else
NOTREACHED();
// StartDownload will clean up |info|.
manager->StartDownload(info);
}
// Update the Download Manager with the finish state, and remove the request
// tracking entries.
void DownloadFileManager::OnDownloadFinished(int id,
int64 bytes_so_far) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
DownloadManager* manager = LookupManager(id);
if (manager)
manager->DownloadFinished(id, bytes_so_far);
RemoveDownload(id, manager);
RemoveDownloadFromUIProgress(id);
}
void DownloadFileManager::DownloadUrl(
const GURL& url,
const GURL& referrer,
const std::string& referrer_charset,
const DownloadSaveInfo& save_info,
int render_process_host_id,
int render_view_id,
URLRequestContextGetter* request_context_getter) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
ChromeThread::PostTask(
ChromeThread::IO, FROM_HERE,
NewRunnableMethod(this,
&DownloadFileManager::OnDownloadUrl,
url,
referrer,
referrer_charset,
save_info,
render_process_host_id,
render_view_id,
request_context_getter));
}
// Relate a download ID to its owning DownloadManager.
DownloadManager* DownloadFileManager::LookupManager(int download_id) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
DownloadManagerMap::iterator it = managers_.find(download_id);
if (it != managers_.end())
return it->second;
return NULL;
}
// Utility function for look up table maintenance, called on the UI thread.
// A manager may have multiple downloads in progress, so we just look up the
// one download (id) and remove it from the set, and remove the set if it
// becomes empty.
void DownloadFileManager::RemoveDownload(int id, DownloadManager* manager) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
if (manager) {
RequestMap::iterator it = requests_.find(manager);
if (it != requests_.end()) {
DownloadRequests& downloads = it->second;
DownloadRequests::iterator rit = downloads.find(id);
if (rit != downloads.end())
downloads.erase(rit);
if (downloads.empty())
requests_.erase(it);
}
}
// A download can only have one manager, so remove it if it exists.
DownloadManagerMap::iterator dit = managers_.find(id);
if (dit != managers_.end())
managers_.erase(dit);
}
// Utility function for converting request IDs to a TabContents. Must be called
// only on the UI thread since Profile operations may create UI objects, such as
// the first call to profile->GetDownloadManager().
// static
DownloadManager* DownloadFileManager::DownloadManagerFromRenderIds(
int render_process_id, int render_view_id) {
TabContents* contents = tab_util::GetTabContentsByID(render_process_id,
render_view_id);
if (contents) {
Profile* profile = contents->profile();
if (profile)
return profile->GetDownloadManager();
}
return NULL;
}
// Called by DownloadManagers in their destructor, and only on the UI thread.
void DownloadFileManager::RemoveDownloadManager(DownloadManager* manager) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
DCHECK(manager);
RequestMap::iterator it = requests_.find(manager);
if (it == requests_.end())
return;
const DownloadRequests& requests = it->second;
DownloadRequests::const_iterator i = requests.begin();
for (; i != requests.end(); ++i) {
DownloadManagerMap::iterator dit = managers_.find(*i);
if (dit != managers_.end()) {
DCHECK(dit->second == manager);
managers_.erase(dit);
}
}
requests_.erase(it);
}
// Notifications from the UI thread and run on the IO thread
// Initiate a request for URL to be downloaded.
void DownloadFileManager::OnDownloadUrl(
const GURL& url,
const GURL& referrer,
const std::string& referrer_charset,
const DownloadSaveInfo& save_info,
int render_process_host_id,
int render_view_id,
URLRequestContextGetter* request_context_getter) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO));
URLRequestContext* context = request_context_getter->GetURLRequestContext();
context->set_referrer_charset(referrer_charset);
resource_dispatcher_host_->BeginDownload(url,
referrer,
save_info,
render_process_host_id,
render_view_id,
context);
}
// Actions from the UI thread and run on the download thread
// Open a download, or show it in a file explorer window. We run on this
// thread to avoid blocking the UI with (potentially) slow Shell operations.
// TODO(paulg): File 'stat' operations.
#if !defined(OS_MACOSX)
void DownloadFileManager::OnShowDownloadInShell(const FilePath& full_path) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
platform_util::ShowItemInFolder(full_path);
}
#endif
// Launches the selected download using ShellExecute 'open' verb. For windows,
// if there is a valid parent window, the 'safer' version will be used which can
// display a modal dialog asking for user consent on dangerous files.
#if !defined(OS_MACOSX)
void DownloadFileManager::OnOpenDownloadInShell(const FilePath& full_path,
const GURL& url,
gfx::NativeView parent_window) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
#if defined(OS_WIN)
if (NULL != parent_window) {
win_util::SaferOpenItemViaShell(parent_window, L"", full_path,
UTF8ToWide(url.spec()));
return;
}
#endif
platform_util::OpenItem(full_path);
}
#endif // OS_MACOSX
// The DownloadManager in the UI thread has provided a final name for the
// download specified by 'id'. Rename the in progress download, and remove it
// from our table if it has been completed or cancelled already.
void DownloadFileManager::OnFinalDownloadName(int id,
const FilePath& full_path,
DownloadManager* manager) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
DownloadFileMap::iterator it = downloads_.find(id);
if (it == downloads_.end())
return;
file_util::CreateDirectory(full_path.DirName());
DownloadFile* download = it->second;
if (download->Rename(full_path)) {
#if defined(OS_MACOSX)
// Done here because we only want to do this once; see
// http://crbug.com/13120 for details.
download->AnnotateWithSourceInformation();
#endif
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(
manager, &DownloadManager::DownloadRenamedToFinalName, id,
full_path));
} else {
// Error. Between the time the UI thread generated 'full_path' to the time
// this code runs, something happened that prevents us from renaming.
DownloadManagerMap::iterator dmit = managers_.find(download->id());
if (dmit != managers_.end()) {
DownloadManager* dlm = dmit->second;
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(dlm, &DownloadManager::DownloadCancelled, id));
} else {
ChromeThread::PostTask(
ChromeThread::IO, FROM_HERE,
NewRunnableFunction(&DownloadManager::OnCancelDownloadRequest,
resource_dispatcher_host_,
download->child_id(),
download->request_id()));
}
}
// If the download has completed before we got this final name, we remove it
// from our in progress map.
if (!download->in_progress()) {
downloads_.erase(it);
delete download;
}
if (downloads_.empty()) {
ChromeThread::PostTask(
ChromeThread::UI, FROM_HERE,
NewRunnableMethod(this, &DownloadFileManager::StopUpdateTimer));
}
}
// static
void DownloadFileManager::DeleteFile(const FilePath& path) {
// Make sure we only delete files.
if (!file_util::DirectoryExists(path))
file_util::Delete(path, false);
}