diff options
author | paulg@google.com <paulg@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-14 01:09:50 +0000 |
---|---|---|
committer | paulg@google.com <paulg@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-14 01:09:50 +0000 |
commit | 37936eefdf1bf98d1a4458d8f3b85233d28c37e4 (patch) | |
tree | 96e1ffcf546b7fb4ad458f9a2ab8b8268d5f122a /chrome/browser/download | |
parent | da5de3c98d0613411df3acb9b692fa200e51dcfb (diff) | |
download | chromium_src-37936eefdf1bf98d1a4458d8f3b85233d28c37e4.zip chromium_src-37936eefdf1bf98d1a4458d8f3b85233d28c37e4.tar.gz chromium_src-37936eefdf1bf98d1a4458d8f3b85233d28c37e4.tar.bz2 |
Move the Save Page code to the browser/download/ directory.
Review URL: http://codereview.chromium.org/3040
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@2174 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/download')
-rw-r--r-- | chrome/browser/download/save_file.cc | 103 | ||||
-rw-r--r-- | chrome/browser/download/save_file.h | 80 | ||||
-rw-r--r-- | chrome/browser/download/save_file_manager.cc | 582 | ||||
-rw-r--r-- | chrome/browser/download/save_file_manager.h | 262 | ||||
-rw-r--r-- | chrome/browser/download/save_item.cc | 132 | ||||
-rw-r--r-- | chrome/browser/download/save_item.h | 112 | ||||
-rw-r--r-- | chrome/browser/download/save_package.cc | 1064 | ||||
-rw-r--r-- | chrome/browser/download/save_package.h | 309 | ||||
-rw-r--r-- | chrome/browser/download/save_package_unittest.cc | 170 | ||||
-rw-r--r-- | chrome/browser/download/save_page_model.cc | 48 | ||||
-rw-r--r-- | chrome/browser/download/save_page_model.h | 38 | ||||
-rw-r--r-- | chrome/browser/download/save_page_uitest.cc | 112 | ||||
-rw-r--r-- | chrome/browser/download/save_types.h | 71 |
13 files changed, 3083 insertions, 0 deletions
diff --git a/chrome/browser/download/save_file.cc b/chrome/browser/download/save_file.cc new file mode 100644 index 0000000..b92edf2 --- /dev/null +++ b/chrome/browser/download/save_file.cc @@ -0,0 +1,103 @@ +// 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/save_file.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/path_service.h" +#include "chrome/browser/download/save_types.h" +#include "chrome/common/win_util.h" +#include "chrome/common/win_safe_util.h" + +SaveFile::SaveFile(const SaveFileCreateInfo* info) + : info_(info), + file_(NULL), + bytes_so_far_(0), + path_renamed_(false), + in_progress_(true) { + DCHECK(info); + DCHECK(info->path.empty()); + if (file_util::CreateTemporaryFileName(&full_path_)) + Open(L"wb"); +} + +SaveFile::~SaveFile() { + Close(); +} + +// Return false indicate that we got disk error, save file manager will tell +// SavePackage this error, then SavePackage will call its Cancel() method to +// cancel whole save job. +bool SaveFile::AppendDataToFile(const char* data, int data_len) { + if (file_) { + if (data_len == fwrite(data, 1, data_len, file_)) { + bytes_so_far_ += data_len; + return true; + } else { + Close(); + return false; + } + } + // No file_, treat it as disk error. + return false; +} + +void SaveFile::Cancel() { + Close(); + // If this job has been canceled, and it has created file, + // We need to delete this created file. + if (!full_path_.empty()) { + DeleteFile(full_path_.c_str()); + } +} + +// Rename the file when we have final name. +bool SaveFile::Rename(const std::wstring& new_path) { + Close(); + + DCHECK(!path_renamed()); + // 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 the last parameter + // is FALSE and the new file already exists, the function overwrites the + // existing file and succeeds. + if (!CopyFile(full_path_.c_str(), new_path.c_str(), FALSE)) + return false; + + DeleteFile(full_path_.c_str()); + + full_path_ = new_path; + path_renamed_ = true; + + // Still in saving process, reopen the file. + if (in_progress_ && !Open(L"a+b")) + return false; + return true; +} + +void SaveFile::Finish() { + Close(); + in_progress_ = false; +} + +void SaveFile::Close() { + if (file_) { + fclose(file_); + file_ = NULL; + } +} + +bool SaveFile::Open(const wchar_t* open_mode) { + DCHECK(!full_path_.empty()); + if (_wfopen_s(&file_, full_path_.c_str(), open_mode)) { + file_ = NULL; + return false; + } + // 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_); + return true; +} + diff --git a/chrome/browser/download/save_file.h b/chrome/browser/download/save_file.h new file mode 100644 index 0000000..3d5e06c --- /dev/null +++ b/chrome/browser/download/save_file.h @@ -0,0 +1,80 @@ +// 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. + +#ifndef CHROME_BROWSER_DOWNLOAD_SAVE_FILE_H__ +#define CHROME_BROWSER_DOWNLOAD_SAVE_FILE_H__ + +#include <string> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/download/save_types.h" + +// SaveFile ---------------------------------------------------------------- + +// These objects live exclusively on the file thread and handle the writing +// operations for one save item. These objects live only for the duration that +// the saving job is 'in progress': once the saving job has been completed or +// canceled, the SaveFile is destroyed. One SaveFile object represents one item +// in a save session. +class SaveFile { + public: + explicit SaveFile(const SaveFileCreateInfo* info); + ~SaveFile(); + + // Write a new chunk of data to the file. Returns true on success. + bool AppendDataToFile(const char* data, int data_len); + + // Abort the saving job and automatically close the file. + void Cancel(); + + // Rename the saved file. Returns 'true' if the rename was successful. + bool Rename(const std::wstring& full_path); + + void Finish(); + + // Accessors. + int save_id() const { return info_->save_id; } + int render_process_id() const { return info_->render_process_id; } + int render_view_id() const { return info_->render_view_id; } + int request_id() const { return info_->request_id; } + SaveFileCreateInfo::SaveFileSource save_source() const { + return info_->save_source; + } + + int64 bytes_so_far() const { return bytes_so_far_; } + std::wstring full_path() const { return full_path_; } + bool path_renamed() const { return path_renamed_; } + bool in_progress() const { return in_progress_; } + + private: + // Open or Close the OS file handle. The file is opened in the constructor + // based on creation information passed to it, and automatically closed in + // the destructor. + void Close(); + bool Open(const wchar_t* open_mode); + + scoped_ptr<const SaveFileCreateInfo> info_; + + // OS file handle for writing + FILE* file_; + + // Amount of data received up to this point. We may not know in advance how + // much data to expect since some servers don't provide that information. + int64 bytes_so_far_; + + // Full path to the saved file including the file name. + std::wstring full_path_; + + // Whether the saved file is still using its initial temporary path. + bool path_renamed_; + + // Whether the saved file is still receiving data. + bool in_progress_; + + DISALLOW_EVIL_CONSTRUCTORS(SaveFile); +}; + +#endif // CHROME_BROWSER_DOWNLOAD_SAVE_FILE_H__ + diff --git a/chrome/browser/download/save_file_manager.cc b/chrome/browser/download/save_file_manager.cc new file mode 100644 index 0000000..2a909b72 --- /dev/null +++ b/chrome/browser/download/save_file_manager.cc @@ -0,0 +1,582 @@ +// 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 <Windows.h> +#include <objbase.h> + +#include "chrome/browser/download/save_file_manager.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "base/task.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/download/save_file.h" +#include "chrome/browser/download/save_package.h" +#include "chrome/browser/resource_dispatcher_host.h" +#include "chrome/browser/tab_contents.h" +#include "chrome/browser/tab_util.h" +#include "chrome/browser/web_contents.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/stl_util-inl.h" +#include "chrome/common/win_util.h" +#include "chrome/common/win_safe_util.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_util.h" +#include "net/url_request/url_request_context.h" + +SaveFileManager::SaveFileManager(MessageLoop* ui_loop, + MessageLoop* io_loop, + ResourceDispatcherHost* rdh) + : next_id_(0), + ui_loop_(ui_loop), + io_loop_(io_loop), + resource_dispatcher_host_(rdh) { + DCHECK(ui_loop_); + // Need to make sure that we are in UI thread because using g_browser_process + // on a non-UI thread can cause crashes during shutdown. + DCHECK(ui_loop_ == MessageLoop::current()); + // Cache the message loop of file thread. + base::Thread* thread = g_browser_process->file_thread(); + if (thread) + file_loop_ = thread->message_loop(); + else + // It could be NULL when it is created in unit test of + // ResourceDispatcherHost. + file_loop_ = NULL; + DCHECK(resource_dispatcher_host_); +} + +SaveFileManager::~SaveFileManager() { + // Check for clean shutdown. + DCHECK(save_file_map_.empty()); +} + +// Called during the browser shutdown process to clean up any state (open files, +// timers) that live on the saving thread (file thread). +void SaveFileManager::Shutdown() { + MessageLoop* loop = GetSaveLoop(); + if (loop) { + loop->PostTask(FROM_HERE, + NewRunnableMethod(this, &SaveFileManager::OnShutdown)); + } +} + +// Stop file thread operations. +void SaveFileManager::OnShutdown() { + DCHECK(MessageLoop::current() == GetSaveLoop()); + STLDeleteValues(&save_file_map_); +} + +SaveFile* SaveFileManager::LookupSaveFile(int save_id) { + SaveFileMap::iterator it = save_file_map_.find(save_id); + return it == save_file_map_.end() ? NULL : it->second; +} + +// Called on the IO thread when +// a) The ResourceDispatcherHost has decided that a request is savable. +// b) The resource does not come from the network, but we still need a +// save ID for for managing the status of the saving operation. So we +// file a request from the file thread to the IO thread to generate a +// unique save ID. +int SaveFileManager::GetNextId() { + DCHECK(MessageLoop::current() == io_loop_); + return next_id_++; +} + +void SaveFileManager::RegisterStartingRequest(const std::wstring& save_url, + SavePackage* save_package) { + // Make sure it runs in the UI thread. + DCHECK(MessageLoop::current() == ui_loop_); + int tab_id = save_package->GetTabId(); + + // Register this starting request. + StartingRequestsMap& starting_requests = tab_starting_requests_[tab_id]; + bool never_present = starting_requests.insert( + StartingRequestsMap::value_type(save_url, save_package)).second; + DCHECK(never_present); +} + +SavePackage* SaveFileManager::UnregisterStartingRequest( + const std::wstring& save_url, int tab_id) { + // Make sure it runs in UI thread. + DCHECK(MessageLoop::current() == ui_loop_); + + TabToStartingRequestsMap::iterator it = tab_starting_requests_.find(tab_id); + if (it != tab_starting_requests_.end()) { + StartingRequestsMap& requests = it->second; + StartingRequestsMap::iterator sit = requests.find(save_url); + if (sit == requests.end()) + return NULL; + + // Found, erase it from starting list and return SavePackage. + SavePackage* save_package = sit->second; + requests.erase(sit); + // If there is no element in requests, remove it + if (requests.empty()) + tab_starting_requests_.erase(it); + return save_package; + } + + return NULL; +} + +void SaveFileManager::RequireSaveJobFromOtherSource(SaveFileCreateInfo* info) { + // This function must be called on the UI thread, because the io_loop_ + // pointer may be junk when we use it on file thread. We can only rely on the + // io_loop_ pointer being valid when we run code on the UI thread (or on + // the IO thread. + DCHECK(MessageLoop::current() == ui_loop_); + DCHECK(info->save_id == -1); + // Since the data will come from render process, so we need to start + // this kind of save job by ourself. + io_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnRequireSaveJobFromOtherSource, + info)); +} + +// Look up a SavePackage according to a save id. +SavePackage* SaveFileManager::LookupPackage(int save_id) { + DCHECK(MessageLoop::current() == ui_loop_); + SavePackageMap::iterator it = packages_.find(save_id); + if (it != packages_.end()) + return it->second; + return NULL; +} + +// Call from SavePackage for starting a saving job +void SaveFileManager::SaveURL(const std::wstring& url, + const std::wstring& referrer, + int render_process_host_id, + int render_view_id, + SaveFileCreateInfo::SaveFileSource save_source, + const std::wstring& file_full_path, + URLRequestContext* request_context, + SavePackage* save_package) { + DCHECK(MessageLoop::current() == ui_loop_); + if (!io_loop_) { + NOTREACHED(); // Net IO thread must exist. + return; + } + + // Register a saving job. + RegisterStartingRequest(url, save_package); + if (save_source == SaveFileCreateInfo::SAVE_FILE_FROM_NET) { + GURL save_url(url); + DCHECK(save_url.is_valid()); + + io_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnSaveURL, + save_url, + GURL(referrer), + render_process_host_id, + render_view_id, + request_context)); + } else { + // We manually start the save job. + SaveFileCreateInfo* info = new SaveFileCreateInfo(file_full_path, + url, + save_source, + -1); + info->render_process_id = render_process_host_id; + info->render_view_id = render_view_id; + RequireSaveJobFromOtherSource(info); + } +} + +// Utility function for look up table maintenance, called on the UI thread. +// A manager may have multiple save page job (SavePackage) in progress, +// so we just look up the save id and remove it from the tracking table. +// If the save id is -1, it means we just send a request to save, but the +// saving action has still not happened, need to call UnregisterStartingRequest +// to remove it from the tracking map. +void SaveFileManager::RemoveSaveFile(int save_id, const std::wstring& save_url, + SavePackage* package) { + DCHECK(MessageLoop::current() == ui_loop_ && package); + // A save page job(SavePackage) can only have one manager, + // so remove it if it exists. + if (save_id == -1) { + SavePackage* old_package = UnregisterStartingRequest(save_url, + package->GetTabId()); + DCHECK(old_package == package); + } else { + SavePackageMap::iterator it = packages_.find(save_id); + if (it != packages_.end()) + packages_.erase(it); + } +} + +// Static +// Utility function for converting request IDs to a TabContents. Must be called +// only on the UI thread. +SavePackage* SaveFileManager::GetSavePackageFromRenderIds( + int render_process_id, int render_view_id) { + TabContents* contents = tab_util::GetTabContentsByID(render_process_id, + render_view_id); + if (contents && contents->type() == TAB_CONTENTS_WEB) { + // Convert const pointer of WebContents to pointer of WebContents. + const WebContents* web_contents = contents->AsWebContents(); + if (web_contents) + return web_contents->get_save_package(); + } + + return NULL; +} + +// Utility function for deleting specified file. +void SaveFileManager::DeleteDirectoryOrFile(const std::wstring& full_path, + bool is_dir) { + DCHECK(MessageLoop::current() == ui_loop_); + MessageLoop* loop = GetSaveLoop(); + DCHECK(loop); + loop->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnDeleteDirectoryOrFile, + full_path, + is_dir)); +} + +void SaveFileManager::SendCancelRequest(int save_id) { + // Cancel the request which has specific save id. + DCHECK(save_id > -1); + MessageLoop* loop = GetSaveLoop(); + DCHECK(loop); + loop->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::CancelSave, + save_id)); +} + +// Notifications sent from the IO thread and run on the file thread: + +// The IO thread created |info|, but the file thread (this method) uses it +// to create a SaveFile which will hold and finally destroy |info|. It will +// then passes |info| to the UI thread for reporting saving status. +void SaveFileManager::StartSave(SaveFileCreateInfo* info) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + DCHECK(info); + SaveFile* save_file = new SaveFile(info); + DCHECK(LookupSaveFile(info->save_id) == NULL); + save_file_map_[info->save_id] = save_file; + info->path = save_file->full_path(); + + ui_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnStartSave, + info)); +} + +// We do forward an update to the UI thread here, since we do not use timer to +// update the UI. If the user has canceled the saving action (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 SaveFile has been deleted. +void SaveFileManager::UpdateSaveProgress(int save_id, + char* data, + int data_len) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + SaveFile* save_file = LookupSaveFile(save_id); + if (save_file) { + bool write_success = save_file->AppendDataToFile(data, data_len); + ui_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnUpdateSaveProgress, + save_file->save_id(), + save_file->bytes_so_far(), + write_success)); + } + delete [] data; +} + +// The IO thread will call this when saving is completed or it got error when +// fetching data. In the former case, we forward the message to OnSaveFinished +// in UI thread. In the latter case, the save ID will be -1, which means the +// saving action did not even start, so we need to call OnErrorFinished in UI +// thread, which will use the save URL to find corresponding request record and +// delete it. +void SaveFileManager::SaveFinished(int save_id, + std::wstring save_url, + int render_process_id, + bool is_success) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + SaveFileMap::iterator it = save_file_map_.find(save_id); + if (it != save_file_map_.end()) { + SaveFile* save_file = it->second; + ui_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnSaveFinished, + save_id, + save_file->bytes_so_far(), + is_success)); + + save_file->Finish(); + } else if (save_id == -1) { + // Before saving started, we got error. We still call finish process. + DCHECK(!save_url.empty()); + ui_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnErrorFinished, + save_url, + render_process_id)); + } +} + +// Notifications sent from the file thread and run on the UI thread. + +void SaveFileManager::OnStartSave(const SaveFileCreateInfo* info) { + DCHECK(MessageLoop::current() == ui_loop_); + SavePackage* save_package = + GetSavePackageFromRenderIds(info->render_process_id, + info->render_view_id); + if (!save_package) { + // Cancel this request. + SendCancelRequest(info->save_id); + return; + } + + // Insert started saving job to tracking list. + SavePackageMap::iterator sit = packages_.find(info->save_id); + if (sit == packages_.end()) { + // Find the registered request. If we can not find, it means we have + // canceled the job before. + SavePackage* old_save_package = UnregisterStartingRequest(info->url, + info->render_process_id); + if (!old_save_package) { + // Cancel this request. + SendCancelRequest(info->save_id); + return; + } + DCHECK(old_save_package == save_package); + packages_[info->save_id] = save_package; + } else { + NOTREACHED(); + } + + // Forward this message to SavePackage. + save_package->StartSave(info); +} + +void SaveFileManager::OnUpdateSaveProgress(int save_id, int64 bytes_so_far, + bool write_success) { + DCHECK(MessageLoop::current() == ui_loop_); + SavePackage* package = LookupPackage(save_id); + if (package) + package->UpdateSaveProgress(save_id, bytes_so_far, write_success); + else + SendCancelRequest(save_id); +} + +void SaveFileManager::OnSaveFinished(int save_id, + int64 bytes_so_far, + bool is_success) { + DCHECK(MessageLoop::current() == ui_loop_); + SavePackage* package = LookupPackage(save_id); + if (package) + package->SaveFinished(save_id, bytes_so_far, is_success); +} + +void SaveFileManager::OnErrorFinished(std::wstring save_url, int tab_id) { + DCHECK(MessageLoop::current() == ui_loop_); + SavePackage* save_package = UnregisterStartingRequest(save_url, tab_id); + if (save_package) + save_package->SaveFailed(save_url); +} + +void SaveFileManager::OnCancelSaveRequest(int render_process_id, + int request_id) { + DCHECK(MessageLoop::current() == ui_loop_); + DCHECK(io_loop_); + io_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::ExecuteCancelSaveRequest, + render_process_id, + request_id)); +} + +// Notifications sent from the UI thread and run on the IO thread. + +void SaveFileManager::OnSaveURL(const GURL& url, + const GURL& referrer, + int render_process_host_id, + int render_view_id, + URLRequestContext* request_context) { + DCHECK(MessageLoop::current() == io_loop_); + resource_dispatcher_host_->BeginSaveFile(url, + referrer, + render_process_host_id, + render_view_id, + request_context); +} + +void SaveFileManager::OnRequireSaveJobFromOtherSource( + SaveFileCreateInfo* info) { + DCHECK(MessageLoop::current() == io_loop_); + DCHECK(info->save_id == -1); + // Generate a unique save id. + info->save_id = GetNextId(); + // Start real saving action. + MessageLoop* loop = GetSaveLoop(); + DCHECK(loop); + loop->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::StartSave, + info)); +} + +void SaveFileManager::ExecuteCancelSaveRequest(int render_process_id, + int request_id) { + DCHECK(MessageLoop::current() == io_loop_); + resource_dispatcher_host_->CancelRequest(render_process_id, + request_id, + false); +} + +// Notifications sent from the UI thread and run on the file thread. + +// This method will be sent via a user action, or shutdown on the UI thread, +// and run on the file thread. We don't post a message back for cancels, +// but we do forward the cancel to the IO thread. Since this message has been +// sent from the UI thread, the saving job may have already completed and +// won't exist in our map. +void SaveFileManager::CancelSave(int save_id) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + SaveFileMap::iterator it = save_file_map_.find(save_id); + if (it != save_file_map_.end()) { + SaveFile* save_file = it->second; + + // If the data comes from the net IO thread, then forward the cancel + // message to IO thread. If the data comes from other sources, just + // ignore the cancel message. + if (save_file->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_NET) { + ui_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnCancelSaveRequest, + save_file->render_process_id(), + save_file->request_id())); + + // UI thread will notify the render process to stop sending data, + // so in here, we need not to do anything, just close the save file. + save_file->Cancel(); + } else { + // If we did not find SaveFile in map, the saving job should either get + // data from other sources or have finished. + DCHECK(save_file->save_source() != + SaveFileCreateInfo::SAVE_FILE_FROM_NET || + !save_file->in_progress()); + } + // Whatever the save file is renamed or not, just delete it. + save_file_map_.erase(it); + delete save_file; + } +} + +// It is possible that SaveItem which has specified save_id has been canceled +// before this function runs. So if we can not find corresponding SaveFile by +// using specified save_id, just return. +void SaveFileManager::SaveLocalFile(const std::wstring& original_file_url, + int save_id, + int render_process_id) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + SaveFile* save_file = LookupSaveFile(save_id); + if (!save_file) + return; + DCHECK(!save_file->path_renamed()); + // If it has finished, just return. + if (!save_file->in_progress()) + return; + + // Close the save file before the copy operation. + save_file->Finish(); + + GURL file_url(original_file_url); + DCHECK(file_url.SchemeIsFile()); + std::wstring file_path; + net::FileURLToFilePath(file_url, &file_path); + // If we can not get valid file path from original URL, treat it as + // disk error. + if (file_path.empty()) + SaveFinished(save_id, original_file_url, render_process_id, false); + + // Copy the local file to the temporary file. It will be renamed to its + // final name later. + bool success = CopyFile(file_path.c_str(), + save_file->full_path().c_str(), FALSE) != 0; + if (!success) + file_util::Delete(save_file->full_path(), false); + SaveFinished(save_id, original_file_url, render_process_id, success); +} + +void SaveFileManager::OnDeleteDirectoryOrFile(const std::wstring& full_path, + bool is_dir) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + DCHECK(!full_path.empty()); + + file_util::Delete(full_path, is_dir); +} + +// Open a saved page package, show it in a Windows Explorer window. +// We run on this thread to avoid blocking the UI with slow Shell operations. +void SaveFileManager::OnShowSavedFileInShell(const std::wstring full_path) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + win_util::ShowItemInFolder(full_path); +} + +void SaveFileManager::RenameAllFiles( + const FinalNameList& final_names, + const std::wstring& resource_dir, + int render_process_id, + int render_view_id) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + + if (!resource_dir.empty() && !file_util::PathExists(resource_dir)) + file_util::CreateDirectory(resource_dir); + + for (FinalNameList::const_iterator i = final_names.begin(); + i != final_names.end(); ++i) { + SaveFileMap::iterator it = save_file_map_.find(i->first); + if (it != save_file_map_.end()) { + SaveFile* save_file = it->second; + DCHECK(!save_file->in_progress()); + save_file->Rename(i->second); + delete save_file; + save_file_map_.erase(it); + } + } + + ui_loop_->PostTask(FROM_HERE, + NewRunnableMethod(this, + &SaveFileManager::OnFinishSavePageJob, + render_process_id, + render_view_id)); +} + +void SaveFileManager::OnFinishSavePageJob(int render_process_id, + int render_view_id) { + DCHECK(MessageLoop::current() == ui_loop_); + + SavePackage* save_package = + GetSavePackageFromRenderIds(render_process_id, render_view_id); + + save_package->Finish(); +} + +void SaveFileManager::RemoveSavedFileFromFileMap( + const SaveIDList& save_ids) { + DCHECK(MessageLoop::current() == GetSaveLoop()); + + for (SaveIDList::const_iterator i = save_ids.begin(); + i != save_ids.end(); ++i) { + SaveFileMap::iterator it = save_file_map_.find(*i); + if (it != save_file_map_.end()) { + SaveFile* save_file = it->second; + DCHECK(!save_file->in_progress()); + DeleteFile(save_file->full_path().c_str()); + delete save_file; + save_file_map_.erase(it); + } + } +} + diff --git a/chrome/browser/download/save_file_manager.h b/chrome/browser/download/save_file_manager.h new file mode 100644 index 0000000..8bd6e89 --- /dev/null +++ b/chrome/browser/download/save_file_manager.h @@ -0,0 +1,262 @@ +// 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. +// +// Objects that handle file operations for saving files, on the file thread. +// +// The SaveFileManager owns a set of SaveFile objects, each of which connects +// with a SaveItem object which belongs to one SavePackage and runs on the file +// thread for saving data in order to avoid disk activity on either network IO +// thread or the UI thread. It coordinates the notifications from the network +// and UI. +// +// The SaveFileManager itself is a singleton object owned by the +// ResourceDispatcherHost. +// +// The data sent to SaveFileManager have 2 sources, one is from +// ResourceDispatcherHost, run in network IO thread, the all sub-resources +// and save-only-HTML pages will be got from network IO. The second is from +// render process, those html pages which are serialized from DOM will be +// composed in render process and encoded to its original encoding, then sent +// to UI loop in browser process, then UI loop will dispatch the data to +// SaveFileManager on the file thread. SaveFileManager will directly +// call SaveFile's method to persist data. +// +// A typical saving job operation involves multiple threads: +// +// Updating an in progress save file +// io_thread +// |----> data from net ---->| +// | +// | +// |----> data from ---->| | +// | render process | | +// ui_thread | | +// file_thread (writes to disk) +// |----> stats ---->| +// ui_thread (feedback for user) +// +// +// Cancel operations perform the inverse order when triggered by a user action: +// ui_thread (user click) +// |----> cancel command ---->| +// | | file_thread (close file) +// | |---------------------> cancel command ---->| +// | io_thread (stops net IO +// ui_thread (user close tab) for saving) +// |----> cancel command ---->| +// Render process(stop serializing DOM and sending +// data) +// +// +// The SaveFileManager tracks saving requests, mapping from a save ID +// (unique integer created in the IO thread) to the SavePackage for the +// tab where the saving job was initiated. In the event of a tab closure +// during saving, the SavePackage will notice the SaveFileManage to +// cancel all SaveFile job. + +#ifndef CHROME_BROWSER_DOWNLOAD_SAVE_FILE_MANAGER_H__ +#define CHROME_BROWSER_DOWNLOAD_SAVE_FILE_MANAGER_H__ + +#include <utility> + +#include "base/basictypes.h" +#include "base/hash_tables.h" +#include "base/ref_counted.h" +#include "base/thread.h" +#include "chrome/browser/download/save_types.h" + +class GURL; +class SaveFile; +class SavePackage; +class MessageLoop; +class ResourceDispatcherHost; +class Task; +class URLRequestContext; + +class SaveFileManager + : public base::RefCountedThreadSafe<SaveFileManager> { + public: + SaveFileManager(MessageLoop* ui_loop, + MessageLoop* io_loop, + ResourceDispatcherHost* rdh); + ~SaveFileManager(); + + // Lifetime management. + void Shutdown(); + + // Called on the IO thread + int GetNextId(); + + // Save the specified URL. Called on the UI thread and forwarded to the + // ResourceDispatcherHost on the IO thread. + void SaveURL(const std::wstring& url, + const std::wstring& referrer, + int render_process_host_id, + int render_view_id, + SaveFileCreateInfo::SaveFileSource save_source, + const std::wstring& file_full_path, + URLRequestContext* request_context, + SavePackage* save_package); + + // Notifications sent from the IO thread and run on the file thread: + void StartSave(SaveFileCreateInfo* info); + void UpdateSaveProgress(int save_id, char* data, int size); + void SaveFinished(int save_id, std::wstring save_url, + int render_process_id, bool is_success); + + // Notifications sent from the UI thread and run on the file thread. + // Cancel a SaveFile instance which has specified save id. + void CancelSave(int save_id); + + // Called on the UI thread to remove a save package from SaveFileManager's + // tracking map. + void RemoveSaveFile(int save_id, const std::wstring& save_url, + SavePackage* package); + + // Handler for shell operations sent from the UI to the file thread. + void OnShowSavedFileInShell(const std::wstring full_path); + + // Helper function for deleting specified file. + void DeleteDirectoryOrFile(const std::wstring& full_path, bool is_dir); + + // For posting notifications from the UI and IO threads. + MessageLoop* GetSaveLoop() const { return file_loop_; } + + // Runs on file thread to save a file by copying from file system when + // original url is using file scheme. + void SaveLocalFile(const std::wstring& original_file_url, + int save_id, + int render_process_id); + + // Renames all the successfully saved files. + // |final_names| points to a vector which contains pairs of save ids and + // final names of successfully saved files. + void RenameAllFiles( + const FinalNameList& final_names, + const std::wstring& resource_dir, + int render_process_id, + int render_view_id); + + // When the user cancels the saving, we need to remove all remaining saved + // files of this page saving job from save_file_map_. + void RemoveSavedFileFromFileMap(const SaveIDList & save_ids); + + private: + // A cleanup helper that runs on the file thread. + void OnShutdown(); + + // The resource does not come from the network, but we still needs to call + // this function for getting unique save ID by calling + // OnRequireSaveJobFromOtherSource in the net IO thread and start saving + // operation. This function is called on the UI thread. + void RequireSaveJobFromOtherSource(SaveFileCreateInfo* info); + + // Called only on UI thread to get the SavePackage for a tab's profile. + static SavePackage* GetSavePackageFromRenderIds(int render_process_id, + int review_view_id); + + // Register a starting request. Associate the save URL with a + // SavePackage for further matching. + void RegisterStartingRequest(const std::wstring& save_url, + SavePackage* save_package); + // Unregister a start request according save URL, disassociate + // the save URL and SavePackage. + SavePackage* UnregisterStartingRequest(const std::wstring& save_url, + int tab_id); + + // Look up the SavePackage according to save id. + SavePackage* LookupPackage(int save_id); + + // Called only on the file thread. + // Look up one in-progress saving item according to save id. + SaveFile* LookupSaveFile(int save_id); + + // Help function for sending notification of canceling specific request. + void SendCancelRequest(int save_id); + + // Notifications sent from the file thread and run on the UI thread. + + // Lookup the SaveManager for this WebContents' saving profile and inform it + // the saving job has been started. + void OnStartSave(const SaveFileCreateInfo* info); + // Update the SavePackage with the current state of a started saving job. + // If the SavePackage for this saving job is gone, cancel the request. + void OnUpdateSaveProgress(int save_id, + int64 bytes_so_far, + bool write_success); + // Update the SavePackage with the finish state, and remove the request + // tracking entries. + void OnSaveFinished(int save_id, int64 bytes_so_far, bool is_success); + // For those requests that do not have valid save id, use + // map:(url, SavePackage) to find the request and remove it. + void OnErrorFinished(std::wstring save_url, int tab_id); + // Handler for a notification sent to the UI thread. + // The user has requested a cancel in the UI thread, so send a cancel request + // to stop the network requests in net IO thread. + void OnCancelSaveRequest(int render_process_id, int request_id); + // Notifies SavePackage that the whole page saving job is finished. + void OnFinishSavePageJob(int render_process_id, int render_view_id); + + // Notifications sent from the UI thread and run on the file thread. + + // Deletes a specified file on the file thread. + void OnDeleteDirectoryOrFile(const std::wstring& full_path, bool is_dir); + + // Notifications sent from the UI thread and run on the IO thread + + // Initiates a request for URL to be saved. + void OnSaveURL(const GURL& url, + const GURL& referrer, + int render_process_host_id, + int render_view_id, + URLRequestContext* request_context); + // Handler for a notification sent to the IO thread for generating save id. + void OnRequireSaveJobFromOtherSource(SaveFileCreateInfo* info); + // Call ResourceDispatcherHost's CancelRequest method to execute cancel + // action in the IO thread. + void ExecuteCancelSaveRequest(int render_process_id, int request_id); + + // Unique ID for the next SaveFile object. + int next_id_; + + // A map of all saving jobs by using save id. + typedef base::hash_map<int, SaveFile*> SaveFileMap; + SaveFileMap save_file_map_; + + // Message loop that the SavePackages live on. + MessageLoop* ui_loop_; + + // We cache the IO loop, we will use it to request resources from network. + MessageLoop* io_loop_; + + // We cache the file loop, we will use it to do real file operation. + // We guarantee that we won't access them incorrectly during the shutdown + // process + MessageLoop* file_loop_; + + ResourceDispatcherHost* resource_dispatcher_host_; + + // Tracks which SavePackage to send data to, called only on UI thread. + // SavePackageMap maps save IDs to their SavePackage. + typedef base::hash_map<int, SavePackage*> SavePackageMap; + SavePackageMap packages_; + + // There is a gap between after calling SaveURL() and before calling + // StartSave(). In this gap, each request does not have save id for tracking. + // But sometimes users might want to stop saving job or ResourceDispatcherHost + // calls SaveFinished with save id -1 for network error. We name the requests + // as starting requests. For tracking those starting requests, we need to + // have some data structure. + // First we use a hashmap to map the request URL to SavePackage, then we + // use a hashmap to map the tab id (we actually use render_process_id) to the + // hashmap since it is possible to save same URL in different tab at + // same time. + typedef base::hash_map<std::wstring, SavePackage*> StartingRequestsMap; + typedef base::hash_map<int, StartingRequestsMap> TabToStartingRequestsMap; + TabToStartingRequestsMap tab_starting_requests_; + + DISALLOW_EVIL_CONSTRUCTORS(SaveFileManager); +}; + +#endif // CHROME_BROWSER_DOWNLOAD_SAVE_FILE_MANAGER_H__ diff --git a/chrome/browser/download/save_item.cc b/chrome/browser/download/save_item.cc new file mode 100644 index 0000000..b7b7ee40 --- /dev/null +++ b/chrome/browser/download/save_item.cc @@ -0,0 +1,132 @@ +// 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/save_item.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "base/win_util.h" +#include "chrome/browser/download/save_file.h" +#include "chrome/browser/download/save_file_manager.h" +#include "chrome/browser/download/save_package.h" + +// Constructor for SaveItem when creating each saving job. +SaveItem::SaveItem(const std::wstring& url, + const std::wstring& referrer, + SavePackage* package, + SaveFileCreateInfo::SaveFileSource save_source) + : save_id_(-1), + save_source_(save_source), + url_(url), + referrer_(referrer), + total_bytes_(0), + received_bytes_(0), + state_(WAIT_START), + package_(package), + has_final_name_(false), + is_success_(false) { + DCHECK(package); +} + +SaveItem::~SaveItem() { +} + +// Set start state for save item. +void SaveItem::Start() { + DCHECK(state_ == WAIT_START); + state_ = IN_PROGRESS; +} + +// If we've received more data than we were expecting (bad server info?), +// revert to 'unknown size mode'. +void SaveItem::UpdateSize(int64 bytes_so_far) { + received_bytes_ = bytes_so_far; + if (received_bytes_ >= total_bytes_) + total_bytes_ = 0; +} + +// Updates from the file thread may have been posted while this saving job +// was being canceled in the UI thread, so we'll accept them unless we're +// complete. +void SaveItem::Update(int64 bytes_so_far) { + if (state_ != IN_PROGRESS) { + NOTREACHED(); + return; + } + UpdateSize(bytes_so_far); +} + +// Cancel this saving item job. If the job is not in progress, ignore +// this command. The SavePackage will each in-progress SaveItem's cancel +// when canceling whole saving page job. +void SaveItem::Cancel() { + // If item is in WAIT_START mode, which means no request has been sent. + // So we need not to cancel it. + if (state_ != IN_PROGRESS) { + // Small downloads might be complete before method has a chance to run. + return; + } + state_ = CANCELED; + is_success_ = false; + Finish(received_bytes_, false); + package_->SaveCanceled(this); +} + +// Set finish state for a save item +void SaveItem::Finish(int64 size, bool is_success) { + // When this function is called, the SaveItem should be one of following + // three situations. + // a) The data of this SaveItem is finished saving. So it should have + // generated final name. + // b) Error happened before the start of saving process. So no |save_id_| is + // generated for this SaveItem and the |is_success_| should be false. + // c) Error happened in the start of saving process, the SaveItem has a save + // id, |is_success_| should be false, and the |size| should be 0. + DCHECK(has_final_name() || (save_id_ == -1 && !is_success_) || + (save_id_ != -1 && !is_success_ && !size)); + state_ = COMPLETE; + is_success_ = is_success; + UpdateSize(size); +} + +// Calculate the percentage of the save item +int SaveItem::PercentComplete() const { + switch (state_) { + case COMPLETE: + case CANCELED: + return 100; + case WAIT_START: + return 0; + case IN_PROGRESS: { + int percent = 0; + if (total_bytes_ > 0) + percent = static_cast<int>(received_bytes_ * 100.0 / total_bytes_); + return percent; + } + default: { + NOTREACHED(); + return -1; + } + } +} + +// Rename the save item with new path. +void SaveItem::Rename(const std::wstring& full_path) { + DCHECK(!full_path.empty() && !has_final_name()); + full_path_ = full_path; + file_name_ = file_util::GetFilenameFromPath(full_path_); + has_final_name_ = true; +} + +void SaveItem::SetSaveId(int32 save_id) { + DCHECK(save_id_ == -1); + save_id_ = save_id; +} + +void SaveItem::SetTotalBytes(int64 total_bytes) { + DCHECK(total_bytes_ == 0); + total_bytes_ = total_bytes; +} + diff --git a/chrome/browser/download/save_item.h b/chrome/browser/download/save_item.h new file mode 100644 index 0000000..3c3ed4a --- /dev/null +++ b/chrome/browser/download/save_item.h @@ -0,0 +1,112 @@ +// 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. +// +#ifndef CHROME_BROWSER_DOWNLOAD_SAVE_ITEM_H__ +#define CHROME_BROWSER_DOWNLOAD_SAVE_ITEM_H__ + +#include <string> + +#include "base/basictypes.h" +#include "chrome/browser/download/save_types.h" + +class SavePackage; + +// One SaveItem per save file. This is the model class that stores all the +// state for one save file. +class SaveItem { + public: + enum SaveState { + WAIT_START, + IN_PROGRESS, + COMPLETE, + CANCELED + }; + + SaveItem(const std::wstring& url, + const std::wstring& referrer, + SavePackage* package, + SaveFileCreateInfo::SaveFileSource save_source); + + ~SaveItem(); + + void Start(); + + // Received a new chunk of data. + void Update(int64 bytes_so_far); + + // Cancel saving item. + void Cancel(); + + // Saving operation completed. + void Finish(int64 size, bool is_success); + + // Rough percent complete, -1 means we don't know (since we didn't receive a + // total size). + int PercentComplete() const; + + // Update path for SaveItem, the actual file is renamed on the file thread. + void Rename(const std::wstring& full_path); + + void SetSaveId(int32 save_id); + + void SetTotalBytes(int64 total_bytes); + + // Accessors. + SaveState state() const { return state_; } + const std::wstring full_path() const { return full_path_; } + const std::wstring file_name() const { return file_name_; } + const std::wstring& url() const { return url_; } + const std::wstring& referrer() const { return referrer_; } + int64 total_bytes() const { return total_bytes_; } + int64 received_bytes() const { return received_bytes_; } + int32 save_id() const { return save_id_; } + bool has_final_name() const { return has_final_name_; } + bool success() const { return is_success_; } + SaveFileCreateInfo::SaveFileSource save_source() const { + return save_source_; + } + SavePackage* package() const { return package_; } + + private: + // Internal helper for maintaining consistent received and total sizes. + void UpdateSize(int64 size); + + // Request ID assigned by the ResourceDispatcherHost. + int32 save_id_; + + // Full path to the save item file. + std::wstring full_path_; + + // Short display version of the file. + std::wstring file_name_; + + // The URL for this save item. + std::wstring url_; + std::wstring referrer_; + + // Total bytes expected. + int64 total_bytes_; + + // Current received bytes. + int64 received_bytes_; + + // The current state of this save item. + SaveState state_; + + // Specifies if this name is a final or not. + bool has_final_name_; + + // Flag indicates whether SaveItem has error while in saving process. + bool is_success_; + + SaveFileCreateInfo::SaveFileSource save_source_; + + // Our owning object. + SavePackage* package_; + + DISALLOW_EVIL_CONSTRUCTORS(SaveItem); +}; + +#endif // CHROME_BROWSER_DOWNLOAD_SAVE_ITEM_H__ + diff --git a/chrome/browser/download/save_package.cc b/chrome/browser/download/save_package.cc new file mode 100644 index 0000000..5410d31 --- /dev/null +++ b/chrome/browser/download/save_package.cc @@ -0,0 +1,1064 @@ +// 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/save_package.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/path_service.h" +#include "base/string_util.h" +#include "base/task.h" +#include "base/thread.h" +#include "base/win_util.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/download/download_manager.h" +#include "chrome/browser/download/save_file.h" +#include "chrome/browser/download/save_file_manager.h" +#include "chrome/browser/download/save_page_model.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/render_process_host.h" +#include "chrome/browser/render_view_host.h" +#include "chrome/browser/render_view_host_delegate.h" +#include "chrome/browser/resource_dispatcher_host.h" +#include "chrome/browser/tab_util.h" +#include "chrome/browser/web_contents.h" +#include "chrome/browser/views/download_shelf_view.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/l10n_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/pref_service.h" +#include "chrome/common/stl_util-inl.h" +#include "chrome/common/win_util.h" +#include "net/base/net_util.h" +#include "net/url_request/url_request_context.h" +#include "webkit/glue/dom_serializer_delegate.h" + +#include "generated_resources.h" + +// Default name which will be used when we can not get proper name from +// resource URL. +static const wchar_t kDefaultSaveName[] = L"saved_resource"; + +// Maximum number of file ordinal number. I think it's big enough for resolving +// name-conflict files which has same base file name. +static const int32 kMaxFileOrdinalNumber = 9999; + +// Maximum length for file path. Since Windows have MAX_PATH limitation for +// file path, we need to make sure length of file path of every saved file +// is less than MAX_PATH +static const uint32 kMaxFilePathLength = MAX_PATH - 1; + +// Maximum length for file ordinal number part. Since we only support the +// maximum 9999 for ordinal number, which means maximum file ordinal number part +// should be "(9998)", so the value is 6. +static const uint32 kMaxFileOrdinalNumberPartLength = 6; + +SavePackage::SavePackage(WebContents* web_content, + SavePackageType save_type, + const std::wstring& file_full_path, + const std::wstring& directory_full_path) + : web_contents_(web_content), + save_type_(save_type), + saved_main_file_path_(file_full_path), + saved_main_directory_path_(directory_full_path), + all_save_items_count_(0), + disk_error_occurred_(false), + user_canceled_(false), + download_(NULL), + finished_(false), + wait_state_(INITIALIZE) { + DCHECK(web_content); + const GURL& current_page_url = web_contents_->GetURL(); + DCHECK(current_page_url.is_valid()); + page_url_ = UTF8ToWide(current_page_url.spec()); + DCHECK(save_type_ == SAVE_AS_ONLY_HTML || + save_type_ == SAVE_AS_COMPLETE_HTML); + DCHECK(!saved_main_file_path_.empty() && + saved_main_file_path_.length() <= kMaxFilePathLength); + DCHECK(!saved_main_directory_path_.empty() && + saved_main_directory_path_.length() < kMaxFilePathLength); +} + +// This is for testing use. Set |finished_| as true because we don't want +// method Cancel to be be called in destructor in test mode. +SavePackage::SavePackage(const wchar_t* file_full_path, + const wchar_t* directory_full_path) + : all_save_items_count_(0), + saved_main_file_path_(file_full_path), + saved_main_directory_path_(directory_full_path), + finished_(true), + download_(NULL), + user_canceled_(false), + disk_error_occurred_(false) { + DCHECK(!saved_main_file_path_.empty() && + saved_main_file_path_.length() <= kMaxFilePathLength); + DCHECK(!saved_main_directory_path_.empty() && + saved_main_directory_path_.length() < kMaxFilePathLength); +} + +SavePackage::~SavePackage() { + // Stop receiving saving job's updates + if (!finished_ && !canceled()) { + // Unexpected quit. + Cancel(true); + } + + DCHECK(all_save_items_count_ == (waiting_item_queue_.size() + + completed_count() + + in_process_count())); + // Free all SaveItems. + while (!waiting_item_queue_.empty()) { + // We still have some items which are waiting for start to save. + SaveItem* save_item = waiting_item_queue_.front(); + waiting_item_queue_.pop(); + delete save_item; + } + + STLDeleteValues(&saved_success_items_); + STLDeleteValues(&in_progress_items_); + STLDeleteValues(&saved_failed_items_); + + if (download_) { + // We call this to remove the view from the shelf. It will invoke + // DownloadManager::RemoveDownload, but since the fake DownloadItem is not + // owned by DownloadManager, it will do nothing to our fake item. + download_->Remove(); + delete download_; + download_ = NULL; + } + file_manager_ = NULL; +} + +// Cancel all in progress request, might be called by user or internal error. +void SavePackage::Cancel(bool user_action) { + if (!canceled()) { + if (user_action) + user_canceled_ = true; + else + disk_error_occurred_ = true; + Stop(); + } +} + +// Initialize the SavePackage. +bool SavePackage::Init() { + // Set proper running state. + if (wait_state_ != INITIALIZE) + return false; + + wait_state_ = START_PROCESS; + + // Initialize the request context and resource dispatcher. + Profile* profile = web_contents_->profile(); + if (!profile) { + NOTREACHED(); + return false; + } + + request_context_ = profile->GetRequestContext(); + + ResourceDispatcherHost* rdh = g_browser_process->resource_dispatcher_host(); + if (!rdh) { + NOTREACHED(); + return false; + } + + file_manager_ = rdh->save_file_manager(); + if (!file_manager_) { + NOTREACHED(); + return false; + } + + // Create the fake DownloadItem and display the view. + download_ = new DownloadItem(1, saved_main_file_path_, + page_url_, Time::Now(), 0, -1, -1); + download_->set_manager(web_contents_->profile()->GetDownloadManager()); + DownloadShelfView* shelf = web_contents_->GetDownloadShelfView(); + shelf->AddDownloadView(new DownloadItemView( + download_, shelf, new SavePageModel(this, download_))); + web_contents_->SetDownloadShelfVisible(true); + + // Check save type and process the save page job. + if (save_type_ == SAVE_AS_COMPLETE_HTML) { + // Get directory + DCHECK(!saved_main_directory_path_.empty()); + GetAllSavableResourceLinksForCurrentPage(); + } else { + wait_state_ = NET_FILES; + GURL u(page_url_); + SaveFileCreateInfo::SaveFileSource save_source = u.SchemeIsFile() ? + SaveFileCreateInfo::SAVE_FILE_FROM_FILE : + SaveFileCreateInfo::SAVE_FILE_FROM_NET; + SaveItem* save_item = new SaveItem(page_url_, + L"", + this, + save_source); + // Add this item to waiting list. + waiting_item_queue_.push(save_item); + all_save_items_count_ = 1; + download_->set_total_bytes(1); + + DoSavingProcess(); + } + + return true; +} + +// Generate name for saving resource. +bool SavePackage::GenerateFilename(const std::string& disposition, + const std::wstring& url, + bool need_html_ext, + std::wstring* generated_name) { + std::wstring file_name = + net::GetSuggestedFilename(GURL(url), disposition, kDefaultSaveName); + + DCHECK(!file_name.empty()); + // Check whether we have same name before. + std::wstring::size_type last_dot = file_name.rfind(L'.'); + std::wstring pure_file_name, file_name_ext; + if (last_dot == std::wstring::npos) { + pure_file_name = file_name; + } else { + pure_file_name = std::wstring(file_name, 0, last_dot); + file_name_ext = std::wstring(file_name, last_dot); + } + // If it is HTML resource, use ".htm" as its extension name. + if (need_html_ext) + file_name_ext = L".htm"; + if (file_name_ext == L".") + file_name_ext.clear(); + + // Get safe pure file name. + if (!GetSafePureFileName(saved_main_directory_path_, file_name_ext, + kMaxFilePathLength, &pure_file_name)) + return false; + + file_name = pure_file_name + file_name_ext; + + // Check whether we already have same name. + if (file_name_set_.find(file_name) == file_name_set_.end()) { + file_name_set_.insert(file_name); + } else { + // Found same name, increase the ordinal number for the file name. + std::wstring base_file_name, file_ordinal_number; + + if (!GetBaseFileNameAndFileOrdinalNumber(pure_file_name, &base_file_name, + &file_ordinal_number)) + base_file_name = pure_file_name; + + // We need to make sure the length of base file name plus maximum ordinal + // number path will be less than or equal to kMaxFilePathLength. + if (!GetSafePureFileName(saved_main_directory_path_, file_name_ext, + kMaxFilePathLength - kMaxFileOrdinalNumberPartLength, &base_file_name)) + return false; + + // Prepare the new ordinal number. + uint32 ordinal_number; + FileNameCountMap::iterator it = file_name_count_map_.find(base_file_name); + if (it == file_name_count_map_.end()) { + // First base-name-conflict resolving, use 1 as initial ordinal number. + file_name_count_map_[base_file_name] = 1; + ordinal_number = 1; + } else { + // We have met same base-name conflict, use latest ordinal number. + ordinal_number = it->second; + } + + if (ordinal_number > (kMaxFileOrdinalNumber - 1)) { + // Use a random file from temporary file. + file_util::CreateTemporaryFileName(&file_name); + file_name = file_util::GetFilenameFromPath(file_name); + // Get safe pure file name. + if (!GetSafePureFileName(saved_main_directory_path_, std::wstring(), + kMaxFilePathLength, &file_name)) + return false; + } else { + uint32 i; + for (i = ordinal_number; i < kMaxFileOrdinalNumber; ++i) { + std::wstring new_name = + StringPrintf(L"%ls(%d)", base_file_name.c_str(), i) + file_name_ext; + if (file_name_set_.find(new_name) == file_name_set_.end()) { + // Resolved name conflict. + file_name = new_name; + file_name_count_map_[base_file_name] = ++i; + break; + } + } + } + + file_name_set_.insert(file_name); + } + + DCHECK(!file_name.empty()); + generated_name->assign(file_name); + + return true; +} + +// We have received a message from SaveFileManager about a new saving job. We +// create a SaveItem and store it in our in_progress list. +void SavePackage::StartSave(const SaveFileCreateInfo* info) { + DCHECK(info && !info->url.empty()); + + SaveUrlItemMap::iterator it = in_progress_items_.find(info->url); + if (it == in_progress_items_.end()) { + // If not found, we must have cancel action. + DCHECK(canceled()); + return; + } + SaveItem* save_item = it->second; + + DCHECK(!saved_main_file_path_.empty()); + + save_item->SetSaveId(info->save_id); + save_item->SetTotalBytes(info->total_bytes); + + // Determine the proper path for a saving job, by choosing either the default + // save directory, or prompting the user. + DCHECK(!save_item->has_final_name()); + if (info->url != page_url_) { + std::wstring generated_name; + // For HTML resource file, make sure it will have .htm as extension name, + // otherwise, when you open the saved page in Chrome again, download + // file manager will treat it as downloadable resource, and download it + // instead of opening it as HTML. + bool need_html_ext = + info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_DOM; + if (!GenerateFilename(info->content_disposition, + info->url, + need_html_ext, + &generated_name)) { + // We can not generate file name for this SaveItem, so we cancel the + // saving page job if the save source is from serialized DOM data. + // Otherwise, it means this SaveItem is sub-resource type, we treat it + // as an error happened on saving. We can ignore this type error for + // sub-resource links which will be resolved as absolute links instead + // of local links in final saved contents. + if (info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_DOM) + Cancel(true); + else + SaveFinished(save_item->save_id(), 0, false); + return; + } + + // When saving page as only-HTML, we only have a SaveItem whose url + // must be page_url_. + DCHECK(save_type_ == SAVE_AS_COMPLETE_HTML); + DCHECK(!saved_main_directory_path_.empty()); + + // Now we get final name retrieved from GenerateFilename, we will use it + // rename the SaveItem. + std::wstring final_name = saved_main_directory_path_; + file_util::AppendToPath(&final_name, generated_name); + save_item->Rename(final_name); + } else { + // It is the main HTML file, use the name chosen by the user. + save_item->Rename(saved_main_file_path_); + } + + // If the save source is from file system, inform SaveFileManager to copy + // corresponding file to the file path which this SaveItem specifies. + if (info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_FILE) { + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::SaveLocalFile, + save_item->url(), + save_item->save_id(), + GetTabId())); + return; + } + + // Check whether we begin to require serialized HTML data. + if (save_type_ == SAVE_AS_COMPLETE_HTML && wait_state_ == HTML_DATA) { + // Inform backend to serialize the all frames' DOM and send serialized + // HTML data back. + GetSerializedHtmlDataForCurrentPageWithLocalLinks(); + } +} + +// Look up SaveItem by save id from in progress map. +SaveItem* SavePackage::LookupItemInProcessBySaveId(int32 save_id) { + if (in_process_count()) { + SaveUrlItemMap::iterator it = in_progress_items_.begin(); + for (; it != in_progress_items_.end(); ++it) { + SaveItem* save_item = it->second; + DCHECK(save_item->state() == SaveItem::IN_PROGRESS); + if (save_item->save_id() == save_id) + return save_item; + } + } + return NULL; +} + +// Remove SaveItem from in progress map and put it to saved map. +void SavePackage::PutInProgressItemToSavedMap(SaveItem* save_item) { + SaveUrlItemMap::iterator it = in_progress_items_.find( + save_item->url()); + DCHECK(it != in_progress_items_.end()); + DCHECK(save_item == it->second); + in_progress_items_.erase(it); + + if (save_item->success()) { + // Add it to saved_success_items_. + DCHECK(saved_success_items_.find(save_item->save_id()) == + saved_success_items_.end()); + saved_success_items_[save_item->save_id()] = save_item; + } else { + // Add it to saved_failed_items_. + DCHECK(saved_failed_items_.find(save_item->url()) == + saved_failed_items_.end()); + saved_failed_items_[save_item->url()] = save_item; + } +} + +// Called for updating saving state. +bool SavePackage::UpdateSaveProgress(int32 save_id, + int64 size, + bool write_success) { + // Because we might have canceled this saving job before, + // so we might not find corresponding SaveItem. + SaveItem* save_item = LookupItemInProcessBySaveId(save_id); + if (!save_item) + return false; + + save_item->Update(size); + + // If we got disk error, cancel whole save page job. + if (!write_success) { + // Cancel job with reason of disk error. + Cancel(false); + } + return true; +} + +// Stop all page saving jobs that are in progress and instruct the file thread +// to delete all saved files. +void SavePackage::Stop() { + // When stopping, if it still has some items in in_progress, cancel them. + DCHECK(canceled()); + if (in_process_count()) { + SaveUrlItemMap::iterator it = in_progress_items_.begin(); + for (; it != in_progress_items_.end(); ++it) { + SaveItem* save_item = it->second; + DCHECK(save_item->state() == SaveItem::IN_PROGRESS); + save_item->Cancel(); + } + // Remove all in progress item to saved map. For failed items, they will + // be put into saved_failed_items_, for successful item, they will be put + // into saved_success_items_. + while (in_process_count()) + PutInProgressItemToSavedMap(in_progress_items_.begin()->second); + } + + // This vector contains the save ids of the save files which SaveFileManager + // needs to remove from its save_file_map_. + SaveIDList save_ids; + for (SavedItemMap::iterator it = saved_success_items_.begin(); + it != saved_success_items_.end(); ++it) + save_ids.push_back(it->first); + for (SaveUrlItemMap::iterator it = saved_failed_items_.begin(); + it != saved_failed_items_.end(); ++it) + save_ids.push_back(it->second->save_id()); + + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::RemoveSavedFileFromFileMap, + save_ids)); + + finished_ = true; + wait_state_ = FAILED; + + // Inform the DownloadItem we have canceled whole save page job. + DCHECK(download_); + download_->Cancel(false); +} + +void SavePackage::CheckFinish() { + if (in_process_count() || finished_) + return; + + std::wstring dir = save_type_ == SAVE_AS_COMPLETE_HTML ? + saved_main_directory_path_ : + L""; + + // This vector contains the final names of all the successfully saved files + // along with their save ids. It will be passed to SaveFileManager to do the + // renaming job. + FinalNameList final_names; + for (SavedItemMap::iterator it = saved_success_items_.begin(); + it != saved_success_items_.end(); ++it) + final_names.push_back(std::make_pair(it->first, + it->second->full_path())); + + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::RenameAllFiles, + final_names, + dir, + web_contents_->process()->host_id(), + web_contents_->render_view_host()->routing_id())); +} + +// Successfully finished all items of this SavePackage. +void SavePackage::Finish() { + // User may cancel the job when we're moving files to the final directory. + if (canceled()) + return; + + wait_state_ = SUCCESSFUL; + finished_ = true; + + // This vector contains the save ids of the save files which SaveFileManager + // needs to remove from its save_file_map_. + SaveIDList save_ids; + for (SaveUrlItemMap::iterator it = saved_failed_items_.begin(); + it != saved_failed_items_.end(); ++it) + save_ids.push_back(it->second->save_id()); + + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::RemoveSavedFileFromFileMap, + save_ids)); + + DCHECK(download_); + download_->Finished(all_save_items_count_); +} + +// Called for updating end state. +void SavePackage::SaveFinished(int32 save_id, int64 size, bool is_success) { + // Because we might have canceled this saving job before, + // so we might not find corresponding SaveItem. Just ignore it. + SaveItem* save_item = LookupItemInProcessBySaveId(save_id); + if (!save_item) + return; + + // Let SaveItem set end state. + save_item->Finish(size, is_success); + // Remove the associated save id and SavePackage. + file_manager_->RemoveSaveFile(save_id, save_item->url(), this); + + PutInProgressItemToSavedMap(save_item); + + // Inform the DownloadItem to update UI. + DCHECK(download_); + // We use the received bytes as number of saved files. + download_->Update(completed_count()); + + if (save_item->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM && + save_item->url() == page_url_ && !save_item->received_bytes()) { + // If size of main HTML page is 0, treat it as disk error. + Cancel(false); + return; + } + + if (canceled()) { + DCHECK(finished_); + return; + } + + // Continue processing the save page job. + DoSavingProcess(); + + // Check whether we can successfully finish whole job. + CheckFinish(); +} + +// Sometimes, the net io will only call SaveFileManager::SaveFinished with +// save id -1 when it encounters error. Since in this case, save id will be +// -1, so we can only use URL to find which SaveItem is associated with +// this error. +// Saving an item failed. If it's a sub-resource, ignore it. If the error comes +// from serializing HTML data, then cancel saving page. +void SavePackage::SaveFailed(const std::wstring& save_url) { + SaveUrlItemMap::iterator it = in_progress_items_.find(save_url); + if (it == in_progress_items_.end()) { + NOTREACHED(); // Should not exist! + return; + } + SaveItem* save_item = it->second; + + save_item->Finish(0, false); + + PutInProgressItemToSavedMap(save_item); + + // Inform the DownloadItem to update UI. + DCHECK(download_); + // We use the received bytes as number of saved files. + download_->Update(completed_count()); + + if (save_type_ == SAVE_AS_ONLY_HTML || + save_item->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM) { + // We got error when saving page. Treat it as disk error. + Cancel(true); + } + + if (canceled()) { + DCHECK(finished_); + return; + } + + // Continue processing the save page job. + DoSavingProcess(); + + CheckFinish(); +} + +void SavePackage::SaveCanceled(SaveItem* save_item) { + // Call the RemoveSaveFile in UI thread. + file_manager_->RemoveSaveFile(save_item->save_id(), + save_item->url(), + this); + if (save_item->save_id() != -1) + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::CancelSave, + save_item->save_id())); +} + +// Initiate a saving job of a specific URL. We send the request to +// SaveFileManager, which will dispatch it to different approach according to +// the save source. Parameter process_all_remaining_items indicates whether +// we need to save all remaining items. +void SavePackage::SaveNextFile(bool process_all_remaining_items) { + DCHECK(web_contents_); + DCHECK(waiting_item_queue_.size()); + + do { + // Pop SaveItem from waiting list. + SaveItem* save_item = waiting_item_queue_.front(); + waiting_item_queue_.pop(); + + // Add the item to in_progress_items_. + SaveUrlItemMap::iterator it = in_progress_items_.find( + save_item->url()); + DCHECK(it == in_progress_items_.end()); + in_progress_items_[save_item->url()] = save_item; + save_item->Start(); + file_manager_->SaveURL(save_item->url(), + save_item->referrer(), + web_contents_->process()->host_id(), + web_contents_->render_view_host()->routing_id(), + save_item->save_source(), + save_item->full_path(), + request_context_.get(), + this); + } while (process_all_remaining_items && waiting_item_queue_.size()); +} + + +// Open download page in windows explorer on file thread, to avoid blocking the +// user interface. +void SavePackage::ShowDownloadInShell() { + DCHECK(file_manager_); + DCHECK(finished_ && !canceled() && !saved_main_file_path_.empty()); + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::OnShowSavedFileInShell, + saved_main_file_path_)); +} + +// Calculate the percentage of whole save page job. +int SavePackage::PercentComplete() { + if (!all_save_items_count_) + return 0; + else if (!in_process_count()) + return 100; + else + return completed_count() / all_save_items_count_; +} + +// Continue processing the save page job after one SaveItem has been +// finished. +void SavePackage::DoSavingProcess() { + if (save_type_ == SAVE_AS_COMPLETE_HTML) { + // We guarantee that images and JavaScripts must be downloaded first. + // So when finishing all those sub-resources, we will know which + // sub-resource's link can be replaced with local file path, which + // sub-resource's link need to be replaced with absolute URL which + // point to its internet address because it got error when saving its data. + SaveItem* save_item = NULL; + // Start a new SaveItem job if we still have job in waiting queue. + if (waiting_item_queue_.size()) { + DCHECK(wait_state_ == NET_FILES); + save_item = waiting_item_queue_.front(); + if (save_item->save_source() != SaveFileCreateInfo::SAVE_FILE_FROM_DOM) { + SaveNextFile(false); + } else if (!in_process_count()) { + // If there is no in-process SaveItem, it means all sub-resources + // have been processed. Now we need to start serializing HTML DOM + // for the current page to get the generated HTML data. + wait_state_ = HTML_DATA; + // All non-HTML resources have been finished, start all remaining + // HTML files. + SaveNextFile(true); + } + } else if (in_process_count()) { + // Continue asking for HTML data. + DCHECK(wait_state_ == HTML_DATA); + } + } else { + // Save as HTML only. + DCHECK(wait_state_ == NET_FILES); + DCHECK(save_type_ == SAVE_AS_ONLY_HTML); + if (waiting_item_queue_.size()) { + DCHECK(all_save_items_count_ == waiting_item_queue_.size()); + SaveNextFile(false); + } + } +} + +int SavePackage::GetTabId() { + DCHECK(web_contents_); + return web_contents_->process()->host_id(); +} + +// After finishing all SaveItems which need to get data from net. +// We collect all URLs which have local storage and send the +// map:(originalURL:currentLocalPath) to render process (backend). +// Then render process will serialize DOM and send data to us. +void SavePackage::GetSerializedHtmlDataForCurrentPageWithLocalLinks() { + if (wait_state_ != HTML_DATA) + return; + std::vector<std::wstring> saved_links; + std::vector<std::wstring> saved_file_paths; + int successful_started_items_count = 0; + + // Collect all saved items which have local storage. + // First collect the status of all the resource files and check whether they + // have created local files although they have not been completely saved. + // If yes, the file can be saved. Otherwise, there is a disk error, so we + // need to cancel the page saving job. + for (SaveUrlItemMap::iterator it = in_progress_items_.begin(); + it != in_progress_items_.end(); ++it) { + DCHECK(it->second->save_source() == + SaveFileCreateInfo::SAVE_FILE_FROM_DOM); + if (it->second->has_final_name()) + successful_started_items_count++; + saved_links.push_back(it->second->url()); + saved_file_paths.push_back(it->second->file_name()); + } + + // If not all file of HTML resource have been started, then wait. + if (successful_started_items_count != in_process_count()) + return; + + // Collect all saved success items. + for (SavedItemMap::iterator it = saved_success_items_.begin(); + it != saved_success_items_.end(); ++it) { + DCHECK(it->second->has_final_name()); + saved_links.push_back(it->second->url()); + saved_file_paths.push_back(it->second->file_name()); + } + + // Get the relative directory name. + std::wstring::size_type last_slash = saved_main_directory_path_.rfind(L'\\'); + DCHECK(last_slash != std::wstring::npos); + std::wstring relative_dir_name = std::wstring(saved_main_directory_path_, + last_slash + 1); + + relative_dir_name = std::wstring(L"./") + relative_dir_name + L"/"; + + web_contents_->GetSerializedHtmlDataForCurrentPageWithLocalLinks( + saved_links, saved_file_paths, relative_dir_name); +} + +// Process the serialized HTML content data of a specified web page +// retrieved from render process. +void SavePackage::ProcessSerializedHtmlData(const GURL& frame_url, + const std::string& data, + int32 status) { + webkit_glue::DomSerializerDelegate::PageSavingSerializationStatus flag = + static_cast<webkit_glue::DomSerializerDelegate::PageSavingSerializationStatus> + (status); + // Check current state. + if (wait_state_ != HTML_DATA) + return; + + int tab_id = GetTabId(); + // If the all frames are finished saving, we need to close the + // remaining SaveItems. + if (flag == webkit_glue::DomSerializerDelegate::ALL_FRAMES_ARE_FINISHED) { + for (SaveUrlItemMap::iterator it = in_progress_items_.begin(); + it != in_progress_items_.end(); ++it) { + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::SaveFinished, + it->second->save_id(), + it->second->url(), + tab_id, + true)); + } + return; + } + + std::wstring current_frame_url = UTF8ToWide(frame_url.spec()); + SaveUrlItemMap::iterator it = in_progress_items_.find(current_frame_url); + if (it == in_progress_items_.end()) + return; + SaveItem* save_item = it->second; + DCHECK(save_item->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM); + + if (!data.empty()) { + // Prepare buffer for saving HTML data. + char* new_data = static_cast<char*>(new char[data.size()]); + memcpy(new_data, data.data(), data.size()); + + // Call write file functionality in file thread. + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::UpdateSaveProgress, + save_item->save_id(), + new_data, + static_cast<int>(data.size()))); + } + + // Current frame is completed saving, call finish in file thread. + if (flag == webkit_glue::DomSerializerDelegate::CURRENT_FRAME_IS_FINISHED) { + file_manager_->GetSaveLoop()->PostTask(FROM_HERE, + NewRunnableMethod(file_manager_, + &SaveFileManager::SaveFinished, + save_item->save_id(), + save_item->url(), + tab_id, + true)); + } +} + +// Ask for all savable resource links from backend, include main frame and +// sub-frame. +void SavePackage::GetAllSavableResourceLinksForCurrentPage() { + if (wait_state_ != START_PROCESS) + return; + + wait_state_ = RESOURCES_LIST; + GURL main_page_url(page_url_); + web_contents_->GetAllSavableResourceLinksForCurrentPage(main_page_url); +} + +// Give backend the lists which contain all resource links that have local +// storage, after which, render process will serialize DOM for generating +// HTML data. +void SavePackage::ProcessCurrentPageAllSavableResourceLinks( + const std::vector<GURL>& resources_list, + const std::vector<GURL>& referrers_list, + const std::vector<GURL>& frames_list) { + if (wait_state_ != RESOURCES_LIST) + return; + + DCHECK(resources_list.size() == referrers_list.size()); + all_save_items_count_ = static_cast<int>(resources_list.size()) + + static_cast<int>(frames_list.size()); + + // We use total bytes as the total number of files we want to save. + download_->set_total_bytes(all_save_items_count_); + + if (all_save_items_count_) { + // Put all sub-resources to wait list. + for (int i = 0; i < static_cast<int>(resources_list.size()); ++i) { + const GURL& u = resources_list[i]; + DCHECK(u.is_valid()); + SaveFileCreateInfo::SaveFileSource save_source = u.SchemeIsFile() ? + SaveFileCreateInfo::SAVE_FILE_FROM_FILE : + SaveFileCreateInfo::SAVE_FILE_FROM_NET; + SaveItem* save_item = new SaveItem(UTF8ToWide(u.spec()), + UTF8ToWide(referrers_list[i].spec()), this, save_source); + waiting_item_queue_.push(save_item); + } + // Put all HTML resources to wait list. + for (int i = 0; i < static_cast<int>(frames_list.size()); ++i) { + const GURL& u = frames_list[i]; + DCHECK(u.is_valid()); + SaveItem* save_item = new SaveItem(UTF8ToWide(u.spec()), L"", + this, SaveFileCreateInfo::SAVE_FILE_FROM_DOM); + waiting_item_queue_.push(save_item); + } + wait_state_ = NET_FILES; + DoSavingProcess(); + } else { + // No resource files need to be saved, treat it as user cancel. + Cancel(true); + } +} + +std::wstring SavePackage::GetSuggestNameForSaveAs(PrefService* prefs, + const std::wstring& name) { + // Check whether the preference has the preferred directory for saving file. + // If not, initialize it with default directory. + if (!prefs->IsPrefRegistered(prefs::kSaveFileDefaultDirectory)) { + std::wstring default_save_path; + PathService::Get(chrome::DIR_USER_DOCUMENTS, &default_save_path); + file_util::AppendToPath(&default_save_path, + l10n_util::GetString(IDS_DOWNLOAD_DIRECTORY)); + prefs->RegisterStringPref(prefs::kSaveFileDefaultDirectory, + default_save_path); + } + + // Get the directory from preference. + StringPrefMember save_file_path; + save_file_path.Init(prefs::kSaveFileDefaultDirectory, prefs, NULL); + DCHECK(!(*save_file_path).empty()); + + // Ask user for getting final saving name. + std::wstring suggest_name, file_name; + + file_name = name; + file_util::ReplaceIllegalCharacters(&file_name, L' '); + suggest_name = *save_file_path; + file_util::AppendToPath(&suggest_name, file_name); + + return suggest_name; +} + +// Static. +bool SavePackage::GetSaveInfo(const std::wstring& suggest_name, + HWND container_hwnd, + SavePackageParam* param) { + // Use "Web Page, Complete" option as default choice of saving page. + unsigned index = 2; + + // If the conetnts can not be saved as complete-HTML, do not show the + // file filters. + if (CanSaveAsComplete(param->current_tab_mime_type)) { + // Create filter string. + std::wstring filter = l10n_util::GetString(IDS_SAVE_PAGE_FILTER); + filter.resize(filter.size() + 2); + filter[filter.size() - 1] = L'\0'; + filter[filter.size() - 2] = L'\0'; + + if (!win_util::SaveFileAsWithFilter(container_hwnd, + suggest_name, filter.c_str(), L"htm", + &index, ¶m->saved_main_file_path)) + return false; + } else { + if (!win_util::SaveFileAs(container_hwnd, suggest_name, + ¶m->saved_main_file_path)) + return false; + // Set save-as type to only-HTML if the contents of current tab can not be + // saved as complete-HTML. + index = 1; + } + // The option index is not zero-based. + DCHECK(index > 0 && index < 3); + param->dir = file_util::GetDirectoryFromPath(param->saved_main_file_path); + + StringPrefMember save_file_path; + save_file_path.Init(prefs::kSaveFileDefaultDirectory, param->prefs, NULL); + // If user change the default saving directory, we will remember it just + // like IE and FireFox. + if (save_file_path.GetValue() != param->dir) + save_file_path.SetValue(param->dir); + + param->save_type = (index == 1) ? SavePackage::SAVE_AS_ONLY_HTML : + SavePackage::SAVE_AS_COMPLETE_HTML; + + if (param->save_type == SavePackage::SAVE_AS_COMPLETE_HTML) { + // Make new directory for saving complete file. + std::wstring file_name = + file_util::GetFilenameFromPath(param->saved_main_file_path); + std::wstring::size_type last_dot = file_name.rfind(L'.'); + std::wstring pure_file_name; + if (last_dot == std::wstring::npos) + pure_file_name = file_name; + else + pure_file_name = std::wstring(file_name, 0, last_dot); + pure_file_name += L"_files"; + file_util::AppendToPath(¶m->dir, pure_file_name); + } + + return true; +} + +// Static. +bool SavePackage::GetBaseFileNameAndFileOrdinalNumber( + const std::wstring& file_name, + std::wstring* base_file_name, + std::wstring* file_ordinal_number) { + if (file_name.empty() || !base_file_name || !file_ordinal_number) + return false; + + // Find dot position. + std::wstring::size_type dot_position = file_name.rfind(L"."); + // Find position of right parenthesis. + std::wstring::size_type parenthesis_right; + if (std::wstring::npos == dot_position) + parenthesis_right = file_name.rfind(L')'); + else + parenthesis_right = dot_position - 1; + // The latest character of pure file name is not ")", return false. + if (std::wstring::npos == parenthesis_right) + return false; + if (file_name.at(parenthesis_right) != L')') + return false; + // Find position of left parenthesis. + std::wstring::size_type parenthesis_left = file_name.rfind(L'('); + if (std::wstring::npos == parenthesis_left) + return false; + + if (parenthesis_right <= parenthesis_left) + return false; + // Check whether content between left parenthesis and right parenthesis is + // numeric or not. + std::wstring ordinal_number(file_name, parenthesis_left + 1, + parenthesis_right - parenthesis_left - 1); + for (std::wstring::const_iterator cit = ordinal_number.begin(); + cit != ordinal_number.end(); ++cit) + if (!IsAsciiDigit(*cit)) + return false; + + *base_file_name = std::wstring(file_name, 0, parenthesis_left); + *file_ordinal_number = ordinal_number; + return true; +} + +// Static +bool SavePackage::IsSavableURL(const GURL& url) { + return url.SchemeIs("http") || url.SchemeIs("https") || + url.SchemeIs("file") || url.SchemeIs("ftp"); +} + +// Static +bool SavePackage::IsSavableContents(const std::string& contents_mime_type) { + // WebKit creates Document object when MIME type is application/xhtml+xml, + // so we also support this MIME type. + return contents_mime_type == "text/html" || + contents_mime_type == "text/xml" || + contents_mime_type == "application/xhtml+xml" || + contents_mime_type == "text/plain"; +} + +// Static +bool SavePackage::CanSaveAsComplete(const std::string& contents_mime_type) { + return contents_mime_type == "text/html"; +} + +// Static +bool SavePackage::GetSafePureFileName(const std::wstring& dir_path, + const std::wstring& file_name_ext, + uint32 max_file_path_len, + std::wstring* pure_file_name) { + DCHECK(!pure_file_name->empty()); + std::wstring final_name = dir_path; + file_util::AppendToPath(&final_name, *pure_file_name); + // Get total length of dir path, including ending "\". + const std::wstring::size_type dir_path_length = + final_name.length() - pure_file_name->length(); + // Get available length for putting dir path and pure file name. + const std::wstring::size_type available_length = + static_cast<std::wstring::size_type>(max_file_path_len) - + file_name_ext.length(); + + if (final_name.length() <= available_length) + return true; + + if (available_length > dir_path_length) { + *pure_file_name = + pure_file_name->substr(0, available_length - dir_path_length); + return true; + } else { + pure_file_name->clear(); + return false; + } +} + diff --git a/chrome/browser/download/save_package.h b/chrome/browser/download/save_package.h new file mode 100644 index 0000000..8c12190 --- /dev/null +++ b/chrome/browser/download/save_package.h @@ -0,0 +1,309 @@ +// 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. +// +// The SavePackage object manages the process of saving a page as only-html or +// complete-html and providing the information for displaying saving status. +// Saving page as only-html means means that we save web page to a single HTML +// file regardless internal sub resources and sub frames. +// Saving page as complete-html page means we save not only the main html file +// the user told it to save but also a directory for the auxiliary files such +// as all sub-frame html files, image files, css files and js files. +// +// Each page saving job may include one or multiple files which need to be +// saved. Each file is represented by a SaveItem, and all SaveItems are owned +// by the SavePackage. SaveItems are created when a user initiates a page +// saving job, and exist for the duration of one tab's life time. + +#ifndef CHROME_BROWSER_DOWNLOAD_SAVE_PACKAGE_H__ +#define CHROME_BROWSER_DOWNLOAD_SAVE_PACKAGE_H__ + +#include <string> +#include <vector> +#include <queue> +#include <utility> + +#include "base/basictypes.h" +#include "base/hash_tables.h" +#include "base/ref_counted.h" +#include "base/time.h" +#include "chrome/common/pref_member.h" +#include "chrome/browser/download/save_item.h" +#include "chrome/browser/download/save_types.h" + +class SaveFileManager; +class SavePackage; +class DownloadItem; +class GURL; +class MessageLoop; +class PrefService; +class Profile; +class WebContents; +class URLRequestContext; +class WebContents; +class Time; + +namespace base { +class Thread; +} + +// save package: manages all save item. +class SavePackage : public base::RefCountedThreadSafe<SavePackage> { + public: + enum SavePackageType { + // User chose to save only the HTML of the page. + SAVE_AS_ONLY_HTML = 0, + // User chose to save complete-html page. + SAVE_AS_COMPLETE_HTML = 1 + }; + + enum WaitState { + // State when created but not initialized. + INITIALIZE = 0, + // State when after initializing, but not yet saving. + START_PROCESS, + // Waiting on a list of savable resources from the backend. + RESOURCES_LIST, + // Waiting for data sent from net IO or from file system. + NET_FILES, + // Waiting for html DOM data sent from render process. + HTML_DATA, + // Saving page finished successfully. + SUCCESSFUL, + // Failed to save page. + FAILED + }; + + SavePackage(WebContents* web_content, + SavePackageType save_type, + const std::wstring& file_full_path, + const std::wstring& directory_full_path); + + ~SavePackage(); + + // Initialize the SavePackage. Returns true if it initializes properly. + // Need to make sure that this method must be called in the UI thread because + // using g_browser_process on a non-UI thread can cause crashes during + // shutdown. + bool Init(); + + void Cancel(bool user_action); + + void Finish(); + + // Notifications sent from the file thread to the UI thread. + void StartSave(const SaveFileCreateInfo* info); + bool UpdateSaveProgress(int32 save_id, int64 size, bool write_success); + void SaveFinished(int32 save_id, int64 size, bool is_success); + void SaveFailed(const std::wstring& save_url); + void SaveCanceled(SaveItem* save_item); + + // Process current page's all savable links of sub resources, resources' + // referrer and frames(include main frame and sub frames) gotten from + // render process. + void ProcessCurrentPageAllSavableResourceLinks( + const std::vector<GURL>& resources_list, + const std::vector<GURL>& referrers_list, + const std::vector<GURL>& frames_list); + + // Process the serialized html content data of a specified web page + // gotten from render process. + void ProcessSerializedHtmlData(const GURL& frame_url, + const std::string& data, + int32 status); + + // Rough percent complete, -1 means we don't know (since we didn't receive a + // total size). + int PercentComplete(); + + // Show or Open a saved page via the Windows shell. + void ShowDownloadInShell(); + + bool canceled() { return user_canceled_ || disk_error_occurred_; } + + // Accessor + bool finished() { return finished_; } + SavePackageType save_type() { return save_type_; } + + // Since for one tab, it can only have one SavePackage in same time. + // Now we actually use render_process_id as tab's unique id. + int GetTabId(); + + // Helper function for preparing suggested name for the SaveAs Dialog. The + // suggested name is composed of the default save path and the web document's + // title. + static std::wstring GetSuggestNameForSaveAs(PrefService* prefs, + const std::wstring& name); + + // This structure is for storing parameters which we will use to create + // a SavePackage object later. + struct SavePackageParam { + // MIME type of current tab contents. + const std::string& current_tab_mime_type; + // Pointer to preference service. + PrefService* prefs; + // Type about saving page as only-html or complete-html. + SavePackageType save_type; + // File path for main html file. + std::wstring saved_main_file_path; + // Directory path for saving sub resources and sub html frames. + std::wstring dir; + + SavePackageParam(const std::string& mime_type) + : current_tab_mime_type(mime_type) { } + }; + static bool GetSaveInfo(const std::wstring& suggest_name, + HWND container_hwnd, + SavePackageParam* param); + + // File name is consist of pure file name, dot and file extension name. File + // name might has no dot and file extension, or has multiple dot inside file + // name. The dot, which separates the pure file name and file extension name, + // is last dot in the file name. If the file name matches following patterns: + // base_file_name(ordinal_number) or base_file_name(ordinal_number).extension, + // this function will return true and get the base file name part and + // ordinal_number part via output parameters. The |file_ordinal_number| could + // be empty if there is no content in ordinal_number part. If the file name + // does not match the pattern or the ordinal_number part has non-digit + // content, just return false. + static bool GetBaseFileNameAndFileOrdinalNumber( + const std::wstring& file_name, + std::wstring* base_file_name, + std::wstring* file_ordinal_number); + + // Check whether we can do the saving page operation for the specified URL. + static bool IsSavableURL(const GURL& url); + + // Check whether we can do the saving page operation for the contents which + // have the specified MIME type. + static bool IsSavableContents(const std::string& contents_mime_type); + + // Check whether we can save page as complete-HTML for the contents which + // have specified a MIME type. Now only contents which have the MIME type + // "text/html" can be saved as complete-HTML. + static bool CanSaveAsComplete(const std::string& contents_mime_type); + + // File name is considered being consist of pure file name, dot and file + // extension name. File name might has no dot and file extension, or has + // multiple dot inside file name. The dot, which separates the pure file + // name and file extension name, is last dot in the whole file name. + // This function is for making sure the length of specified file path is not + // great than the specified maximum length of file path and getting safe pure + // file name part if the input pure file name is too long. + // The parameter |dir_path| specifies directory part of the specified + // file path. The parameter |file_name_ext| specifies file extension + // name part of the specified file path (including start dot). The parameter + // |max_file_path_len| specifies maximum length of the specified file path. + // The parameter |pure_file_name| input pure file name part of the specified + // file path. If the length of specified file path is great than + // |max_file_path_len|, the |pure_file_name| will output new pure file name + // part for making sure the length of specified file path is less than + // specified maximum length of file path. Return false if the function can + // not get a safe pure file name, otherwise it returns true. + static bool GetSafePureFileName(const std::wstring& dir_path, + const std::wstring& file_name_ext, + uint32 max_file_path_len, + std::wstring* pure_file_name); + + private: + // For testing. + friend class SavePackageTest; + SavePackage(const wchar_t* file_full_path, + const wchar_t* directory_full_path); + + void Stop(); + void CheckFinish(); + void SaveNextFile(bool process_all_remainder_items); + void DoSavingProcess(); + + // Create a file name based on the response from the server. + bool GenerateFilename(const std::string& disposition, + const std::wstring& url, + bool need_html_ext, + std::wstring* generated_name); + + // Get all savable resource links from current web page, include main + // frame and sub-frame. + void GetAllSavableResourceLinksForCurrentPage(); + // Get html data by serializing all frames of current page with lists + // which contain all resource links that have local copy. + void GetSerializedHtmlDataForCurrentPageWithLocalLinks(); + + SaveItem* LookupItemInProcessBySaveId(int32 save_id); + void PutInProgressItemToSavedMap(SaveItem* save_item); + + typedef base::hash_map<std::wstring, SaveItem*> SaveUrlItemMap; + // in_progress_items_ is map of all saving job in in-progress state. + SaveUrlItemMap in_progress_items_; + // saved_failed_items_ is map of all saving job which are failed. + SaveUrlItemMap saved_failed_items_; + + // The number of in process SaveItems. + int in_process_count() const { + return static_cast<int>(in_progress_items_.size()); + } + + // The number of all SaveItems which have completed, including success items + // and failed items. + int completed_count() const { + return static_cast<int>(saved_success_items_.size() + + saved_failed_items_.size()); + } + + typedef std::queue<SaveItem*> SaveItemQueue; + // A queue for items we are about to start saving. + SaveItemQueue waiting_item_queue_; + + typedef base::hash_map<int32, SaveItem*> SavedItemMap; + // saved_success_items_ is map of all saving job which are successfully saved. + SavedItemMap saved_success_items_; + + // The request context which provides application-specific context for + // URLRequest instances. + scoped_refptr<URLRequestContext> request_context_; + + // Non-owning pointer for handling file writing on the file thread. + SaveFileManager* file_manager_; + + WebContents* web_contents_; + + // We use a fake DownloadItem here in order to reuse the DownloadItemView. + // This class owns the pointer. + DownloadItem* download_; + + // The URL of the page the user wants to save. + std::wstring page_url_; + std::wstring saved_main_file_path_; + std::wstring saved_main_directory_path_; + + // Indicates whether the actual saving job is finishing or not. + bool finished_; + + // Indicates whether user canceled the saving job. + bool user_canceled_; + + // Indicates whether user get disk error. + bool disk_error_occurred_; + + // Type about saving page as only-html or complete-html. + SavePackageType save_type_; + + // Number of all need to be saved resources. + int all_save_items_count_; + + typedef base::hash_set<std::wstring> FileNameSet; + // This set is used to eliminate duplicated file names in saving directory. + FileNameSet file_name_set_; + + typedef base::hash_map<std::wstring, uint32> FileNameCountMap; + // This map is used to track serial number for specified filename. + FileNameCountMap file_name_count_map_; + + // Indicates current waiting state when SavePackage try to get something + // from outside. + WaitState wait_state_; + + DISALLOW_EVIL_CONSTRUCTORS(SavePackage); +}; + +#endif // CHROME_BROWSER_DOWNLOAD_SAVE_PACKAGE_H__ diff --git a/chrome/browser/download/save_package_unittest.cc b/chrome/browser/download/save_package_unittest.cc new file mode 100644 index 0000000..1889653 --- /dev/null +++ b/chrome/browser/download/save_package_unittest.cc @@ -0,0 +1,170 @@ +// 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 "base/logging.h" +#include "chrome/browser/download/save_package.h" +#include "testing/gtest/include/gtest/gtest.h" + +// 0 1 2 3 4 5 6 +// 0123456789012345678901234567890123456789012345678901234567890123456789 +static const wchar_t* const kLongFilePath = + L"C:\\EFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz01234567" + L"89ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz012345" + L"6789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123" + L"456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789a.htm"; + +static const wchar_t* const kLongDirPath = + L"C:\\EFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz01234567" + L"89ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz012345" + L"6789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123" + L"456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789a_files"; + +class SavePackageTest : public testing::Test { + public: + SavePackageTest() { + save_package_success_ = new SavePackage(L"c:\\testfile.htm", + L"c:\\testfile_files"); + save_package_fail_ = new SavePackage(kLongFilePath, kLongDirPath); + } + + bool GetGeneratedFilename(bool need_success_generate_filename, + const std::string& disposition, + const std::wstring& url, + bool need_htm_ext, + std::wstring* generated_name) { + SavePackage* save_package; + if (need_success_generate_filename) + save_package = save_package_success_.get(); + else + save_package = save_package_fail_.get(); + return save_package->GenerateFilename(disposition, url, need_htm_ext, + generated_name); + } + + protected: + DISALLOW_EVIL_CONSTRUCTORS(SavePackageTest); + + private: + // SavePackage for successfully generating file name. + scoped_refptr<SavePackage> save_package_success_; + // SavePackage for failed generating file name. + scoped_refptr<SavePackage> save_package_fail_; +}; + +static const struct { + const char* disposition; + const wchar_t* url; + const wchar_t* expected_name; + bool need_htm_ext; +} kGeneratedFiles[] = { + // We mainly focus on testing duplicated names here, since retrieving file + // name from disposition and url has been tested in DownloadManagerTest. + + // No useful information in disposition or URL, use default. + {"1.html", L"http://www.savepage.com/", L"saved_resource.htm", true}, + + // No duplicate occurs. + {"filename=1.css", L"http://www.savepage.com", L"1.css", false}, + + // No duplicate occurs. + {"filename=1.js", L"http://www.savepage.com", L"1.js", false}, + + // Append numbers for duplicated names. + {"filename=1.css", L"http://www.savepage.com", L"1(1).css", false}, + + // No duplicate occurs. + {"filename=1(1).js", L"http://www.savepage.com", L"1(1).js", false}, + + // Append numbers for duplicated names. + {"filename=1.css", L"http://www.savepage.com", L"1(2).css", false}, + + // Change number for duplicated names. + {"filename=1(1).css", L"http://www.savepage.com", L"1(3).css", false}, + + // No duplicate occurs. + {"filename=1(11).css", L"http://www.savepage.com", L"1(11).css", false}, + + // test length of file path is great than MAX_PATH(260 characters). + {"", + // 0 1 2 3 4 5 6 + // 0123456789012345678901234567890123456789012345678901234567890123456789 + // 0 1 2 3 4 + // 01234567890123456789012345678901234567890123456789 + L"http://www.google.com/ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmn" + L"opqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijkl" + L"mnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghij" + L"klmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefgh" + L"test.css", + // 0 1 2 3 4 5 6 + // 0123456789012345678901234567890123456789012345678901234567890123456789 + L"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz01234567" + L"89ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz012345" + L"6789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123" + L"456789ABCDEFGHIJKLMNOPQRSTU.css", + false}, + + // test duplicated file name, its length of file path is great than + // MAX_PATH(260 characters). + {"", + // 0 1 2 3 4 5 6 + // 0123456789012345678901234567890123456789012345678901234567890123456789 + // 0 1 2 3 4 + // 01234567890123456789012345678901234567890123456789 + L"http://www.google.com/ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmn" + L"opqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijkl" + L"mnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghij" + L"klmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefgh" + L"test.css", + // 0 1 2 3 4 5 6 + // 0123456789012345678901234567890123456789012345678901234567890123456789 + L"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz01234567" + L"89ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz012345" + L"6789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123" + L"456789ABCDEFGHIJKLMNO(1).css", + false}, + + {"", + // 0 1 2 3 4 5 6 + // 0123456789012345678901234567890123456789012345678901234567890123456789 + // 0 1 2 3 4 + // 01234567890123456789012345678901234567890123456789 + L"http://www.google.com/ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmn" + L"opqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijkl" + L"mnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghij" + L"klmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefgh" + L"test.css", + // 0 1 2 3 4 5 6 + // 0123456789012345678901234567890123456789012345678901234567890123456789 + L"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz01234567" + L"89ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz012345" + L"6789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123" + L"456789ABCDEFGHIJKLMNO(2).css", + false} +}; + +TEST_F(SavePackageTest, TestSuccessfullyGenerateSavePackageFilename) { + for (int i = 0; i < arraysize(kGeneratedFiles); ++i) { + std::wstring file_name; + bool ok = GetGeneratedFilename(true, + kGeneratedFiles[i].disposition, + kGeneratedFiles[i].url, + kGeneratedFiles[i].need_htm_ext, + &file_name); + ASSERT_TRUE(ok); + EXPECT_EQ(file_name, kGeneratedFiles[i].expected_name); + } +} + +TEST_F(SavePackageTest, TestUnSuccessfullyGenerateSavePackageFilename) { + for (int i = 0; i < arraysize(kGeneratedFiles); ++i) { + std::wstring file_name; + bool ok = GetGeneratedFilename(false, + kGeneratedFiles[i].disposition, + kGeneratedFiles[i].url, + kGeneratedFiles[i].need_htm_ext, + &file_name); + ASSERT_FALSE(ok); + } +} + diff --git a/chrome/browser/download/save_page_model.cc b/chrome/browser/download/save_page_model.cc new file mode 100644 index 0000000..1a78e24 --- /dev/null +++ b/chrome/browser/download/save_page_model.cc @@ -0,0 +1,48 @@ +// 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/save_page_model.h" + +#include "base/string_util.h" +#include "chrome/browser/download/download_manager.h" +#include "chrome/browser/download/save_package.h" +#include "chrome/common/l10n_util.h" + +#include "generated_resources.h" + +SavePageModel::SavePageModel(SavePackage* save, DownloadItem* download) + : save_(save), + download_(download) { +} + +void SavePageModel::CancelTask() { + save_->Cancel(true); +} + +std::wstring SavePageModel::GetStatusText() { + int64 size = download_->received_bytes(); + int64 total_size = download_->total_bytes(); + + std::wstring status_text; + switch (download_->state()) { + case DownloadItem::IN_PROGRESS: + status_text = l10n_util::GetStringF(IDS_SAVE_PAGE_PROGRESS, + FormatNumber(size), + FormatNumber(total_size)); + break; + case DownloadItem::COMPLETE: + status_text = l10n_util::GetString(IDS_SAVE_PAGE_STATUS_COMPLETED); + break; + case DownloadItem::CANCELLED: + status_text = l10n_util::GetString(IDS_SAVE_PAGE_STATUS_CANCELLED); + break; + case DownloadItem::REMOVING: + break; + default: + NOTREACHED(); + } + + return status_text; +} + diff --git a/chrome/browser/download/save_page_model.h b/chrome/browser/download/save_page_model.h new file mode 100644 index 0000000..75a4497 --- /dev/null +++ b/chrome/browser/download/save_page_model.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef CHROME_BROWSER_DOWNLOAD_SAVE_PAGE_MODEL_H__ +#define CHROME_BROWSER_DOWNLOAD_SAVE_PAGE_MODEL_H__ + +#include "chrome/browser/views/download_item_view.h" + +class DownloadItem; +class SavePackage; + +// This class is a model class for DownloadItemView. It provides cancel +// functionality for saving page, and also the text for displaying saving +// status. +class SavePageModel : public DownloadItemView::BaseDownloadItemModel { + public: + SavePageModel(SavePackage* save, DownloadItem* download); + virtual ~SavePageModel() { } + + // Cancel the page saving. + virtual void CancelTask(); + + // Get page saving status text. + virtual std::wstring GetStatusText(); + + private: + // Saving page management. + SavePackage* save_; + + // A fake download item for saving page use. + DownloadItem* download_; + + DISALLOW_EVIL_CONSTRUCTORS(SavePageModel); +}; + +#endif // CHROME_BROWSER_DOWNLOAD_SAVE_PAGE_MODEL_H__ + diff --git a/chrome/browser/download/save_page_uitest.cc b/chrome/browser/download/save_page_uitest.cc new file mode 100644 index 0000000..4e9fca6 --- /dev/null +++ b/chrome/browser/download/save_page_uitest.cc @@ -0,0 +1,112 @@ +// 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 "base/file_util.h" +#include "base/path_service.h" +#include "base/string_util.h" +#include "chrome/browser/automation/url_request_mock_http_job.h" +#include "chrome/browser/download/save_package.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/automation/tab_proxy.h" +#include "chrome/test/ui/ui_test.h" +#include "net/url_request/url_request_unittest.h" + +const std::wstring kTestDir = L"save_page"; + +class SavePageTest : public UITest { + protected: + SavePageTest() : UITest() {} + + void CheckFile(const std::wstring& client_file, + const std::wstring& server_file, + bool check_equal) { + bool exist = false; + for (int i = 0; i < 20; ++i) { + if (file_util::PathExists(client_file)) { + exist = true; + break; + } + Sleep(kWaitForActionMaxMsec / 20); + } + EXPECT_TRUE(exist); + + if (check_equal) { + std::wstring server_file_name; + ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, + &server_file_name)); + server_file_name += L"\\" + kTestDir + L"\\" + server_file; + ASSERT_TRUE(file_util::PathExists(server_file_name)); + + int64 client_file_size = 0; + int64 server_file_size = 0; + EXPECT_TRUE(file_util::GetFileSize(client_file, &client_file_size)); + EXPECT_TRUE(file_util::GetFileSize(server_file_name, &server_file_size)); + EXPECT_EQ(client_file_size, server_file_size); + EXPECT_PRED2(file_util::ContentsEqual, client_file, server_file_name); + } + + EXPECT_TRUE(DieFileDie(client_file, false)); + } + + virtual void SetUp() { + UITest::SetUp(); + EXPECT_TRUE(file_util::CreateNewTempDirectory(L"", &save_dir_)); + save_dir_ += file_util::kPathSeparator; + } + + std::wstring save_dir_; +}; + +TEST_F(SavePageTest, SaveHTMLOnly) { + std::wstring file_name = L"a.htm"; + std::wstring full_file_name = save_dir_ + file_name; + std::wstring dir = save_dir_ + L"a_files"; + + GURL url = URLRequestMockHTTPJob::GetMockUrl(kTestDir + L"/" + file_name); + scoped_ptr<TabProxy> tab(GetActiveTab()); + ASSERT_TRUE(tab->NavigateToURL(url)); + WaitUntilTabCount(1); + + EXPECT_TRUE(tab->SavePage(full_file_name, dir, + SavePackage::SAVE_AS_ONLY_HTML)); + EXPECT_TRUE(WaitForDownloadShelfVisible(tab.get())); + + CheckFile(full_file_name, file_name, true); + EXPECT_FALSE(file_util::PathExists(dir)); +} + +TEST_F(SavePageTest, SaveCompleteHTML) { + std::wstring file_name = L"b.htm"; + std::wstring full_file_name = save_dir_ + file_name; + std::wstring dir = save_dir_ + L"b_files"; + + GURL url = URLRequestMockHTTPJob::GetMockUrl(kTestDir + L"/" + file_name); + scoped_ptr<TabProxy> tab(GetActiveTab()); + ASSERT_TRUE(tab->NavigateToURL(url)); + WaitUntilTabCount(1); + + EXPECT_TRUE(tab->SavePage(full_file_name, dir, + SavePackage::SAVE_AS_COMPLETE_HTML)); + EXPECT_TRUE(WaitForDownloadShelfVisible(tab.get())); + + CheckFile(dir + L"\\1.png", L"1.png", true); + CheckFile(dir + L"\\1.css", L"1.css", true); + CheckFile(full_file_name, file_name, false); + EXPECT_TRUE(DieFileDie(dir, true)); +} + +TEST_F(SavePageTest, NoSave) { + std::wstring file_name = L"c.htm"; + std::wstring full_file_name = save_dir_ + file_name; + std::wstring dir = save_dir_ + L"c_files"; + + scoped_ptr<TabProxy> tab(GetActiveTab()); + ASSERT_TRUE(tab->NavigateToURL(GURL(L"about:blank"))); + WaitUntilTabCount(1); + + EXPECT_FALSE(tab->SavePage(full_file_name, dir, + SavePackage::SAVE_AS_ONLY_HTML)); + EXPECT_FALSE(WaitForDownloadShelfVisible(tab.get())); +} + diff --git a/chrome/browser/download/save_types.h b/chrome/browser/download/save_types.h new file mode 100644 index 0000000..1a8832c --- /dev/null +++ b/chrome/browser/download/save_types.h @@ -0,0 +1,71 @@ +// 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. + +#ifndef CHROME_BROWSER_DOWNLOAD_SAVE_TYPES_H__ +#define CHROME_BROWSER_DOWNLOAD_SAVE_TYPES_H__ + +#include <vector> +#include <utility> + +#include "base/basictypes.h" + +typedef std::vector<std::pair<int, std::wstring> > FinalNameList; +typedef std::vector<int> SaveIDList; + +// This structure is used to handle and deliver some info +// when processing each save item job. +struct SaveFileCreateInfo { + enum SaveFileSource { + // This type indicates the save item that need to be retrieved + // from the network. + SAVE_FILE_FROM_NET = 0, + // This type indicates the save item that need to be retrieved + // from serializing DOM. + SAVE_FILE_FROM_DOM, + // This type indicates the save item that need to be retrieved + // through local file system. + SAVE_FILE_FROM_FILE + }; + + SaveFileCreateInfo(const std::wstring& path, + const std::wstring& url, + SaveFileSource save_source, + int32 save_id) + : path(path), + url(url), + save_id(save_id), + save_source(save_source), + render_process_id(-1), + render_view_id(-1), + request_id(-1), + total_bytes(0) { + } + + SaveFileCreateInfo() : save_id(-1) {} + + // SaveItem fields. + // The local file path of saved file. + std::wstring path; + // Original URL of the saved resource. + std::wstring url; + // Final URL of the saved resource since some URL might be redirected. + std::wstring final_url; + // The unique identifier for saving job, assigned at creation by + // the SaveFileManager for its internal record keeping. + int save_id; + // IDs for looking up the tab we are associated with. + int render_process_id; + int render_view_id; + // Handle for informing the ResourceDispatcherHost of a UI based cancel. + int request_id; + // Disposition info from HTTP response. + std::string content_disposition; + // Total bytes of saved file. + int64 total_bytes; + // Source type of saved file. + SaveFileSource save_source; +}; + +#endif // CHROME_BROWSER_DOWNLOAD_SAVE_TYPES_H__ + |