1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/download/download_path_reservation_tracker.h" 6 7#include <map> 8 9#include "base/bind.h" 10#include "base/callback.h" 11#include "base/file_util.h" 12#include "base/logging.h" 13#include "base/path_service.h" 14#include "base/stl_util.h" 15#include "base/strings/string_util.h" 16#include "base/strings/stringprintf.h" 17#include "base/third_party/icu/icu_utf.h" 18#include "chrome/common/chrome_paths.h" 19#include "content/public/browser/browser_thread.h" 20#include "content/public/browser/download_item.h" 21 22using content::BrowserThread; 23using content::DownloadItem; 24 25namespace { 26 27typedef DownloadItem* ReservationKey; 28typedef std::map<ReservationKey, base::FilePath> ReservationMap; 29 30// The lower bound for file name truncation. If the truncation results in a name 31// shorter than this limit, we give up automatic truncation and prompt the user. 32static const size_t kTruncatedNameLengthLowerbound = 5; 33 34// The length of the suffix string we append for an intermediate file name. 35// In the file name truncation, we keep the margin to append the suffix. 36// TODO(kinaba): remove the margin. The user should be able to set maximum 37// possible filename. 38static const size_t kIntermediateNameSuffixLength = sizeof(".crdownload") - 1; 39 40// Map of download path reservations. Each reserved path is associated with a 41// ReservationKey=DownloadItem*. This object is destroyed in |Revoke()| when 42// there are no more reservations. 43// 44// It is not an error, although undesirable, to have multiple DownloadItem*s 45// that are mapped to the same path. This can happen if a reservation is created 46// that is supposed to overwrite an existing reservation. 47ReservationMap* g_reservation_map = NULL; 48 49// Observes a DownloadItem for changes to its target path and state. Updates or 50// revokes associated download path reservations as necessary. Created, invoked 51// and destroyed on the UI thread. 52class DownloadItemObserver : public DownloadItem::Observer { 53 public: 54 explicit DownloadItemObserver(DownloadItem* download_item); 55 56 private: 57 virtual ~DownloadItemObserver(); 58 59 // DownloadItem::Observer 60 virtual void OnDownloadUpdated(DownloadItem* download) OVERRIDE; 61 virtual void OnDownloadDestroyed(DownloadItem* download) OVERRIDE; 62 63 DownloadItem* download_item_; 64 65 // Last known target path for the download. 66 base::FilePath last_target_path_; 67 68 DISALLOW_COPY_AND_ASSIGN(DownloadItemObserver); 69}; 70 71// Returns true if the given path is in use by a path reservation. 72bool IsPathReserved(const base::FilePath& path) { 73 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 74 // No reservation map => no reservations. 75 if (g_reservation_map == NULL) 76 return false; 77 // Unfortunately path normalization doesn't work reliably for non-existant 78 // files. So given a FilePath, we can't derive a normalized key that we can 79 // use for lookups. We only expect a small number of concurrent downloads at 80 // any given time, so going through all of them shouldn't be too slow. 81 for (ReservationMap::const_iterator iter = g_reservation_map->begin(); 82 iter != g_reservation_map->end(); ++iter) { 83 if (iter->second == path) 84 return true; 85 } 86 return false; 87} 88 89// Returns true if the given path is in use by any path reservation or the 90// file system. Called on the FILE thread. 91bool IsPathInUse(const base::FilePath& path) { 92 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 93 // If there is a reservation, then the path is in use. 94 if (IsPathReserved(path)) 95 return true; 96 97 // If the path exists in the file system, then the path is in use. 98 if (base::PathExists(path)) 99 return true; 100 101 return false; 102} 103 104// Truncates path->BaseName() to make path->BaseName().value().size() <= limit. 105// - It keeps the extension as is. Only truncates the body part. 106// - It secures the base filename length to be more than or equals to 107// kTruncatedNameLengthLowerbound. 108// If it was unable to shorten the name, returns false. 109bool TruncateFileName(base::FilePath* path, size_t limit) { 110 base::FilePath basename(path->BaseName()); 111 // It is already short enough. 112 if (basename.value().size() <= limit) 113 return true; 114 115 base::FilePath dir(path->DirName()); 116 base::FilePath::StringType ext(basename.Extension()); 117 base::FilePath::StringType name(basename.RemoveExtension().value()); 118 119 // Impossible to satisfy the limit. 120 if (limit < kTruncatedNameLengthLowerbound + ext.size()) 121 return false; 122 limit -= ext.size(); 123 124 // Encoding specific truncation logic. 125 base::FilePath::StringType truncated; 126#if defined(OS_CHROMEOS) || defined(OS_MACOSX) 127 // UTF-8. 128 base::TruncateUTF8ToByteSize(name, limit, &truncated); 129#elif defined(OS_WIN) 130 // UTF-16. 131 DCHECK(name.size() > limit); 132 truncated = name.substr(0, CBU16_IS_TRAIL(name[limit]) ? limit - 1 : limit); 133#else 134 // We cannot generally assume that the file name encoding is in UTF-8 (see 135 // the comment for FilePath::AsUTF8Unsafe), hence no safe way to truncate. 136#endif 137 138 if (truncated.size() < kTruncatedNameLengthLowerbound) 139 return false; 140 *path = dir.Append(truncated + ext); 141 return true; 142} 143 144// Called on the FILE thread to reserve a download path. This method: 145// - Creates directory |default_download_path| if it doesn't exist. 146// - Verifies that the parent directory of |suggested_path| exists and is 147// writeable. 148// - Truncates the suggested name if it exceeds the filesystem's limit. 149// - Uniquifies |suggested_path| if |should_uniquify_path| is true. 150// - Schedules |callback| on the UI thread with the reserved path and a flag 151// indicating whether the returned path has been successfully verified. 152void CreateReservation( 153 ReservationKey key, 154 const base::FilePath& suggested_path, 155 const base::FilePath& default_download_path, 156 bool create_directory, 157 DownloadPathReservationTracker::FilenameConflictAction conflict_action, 158 const DownloadPathReservationTracker::ReservedPathCallback& callback) { 159 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 160 DCHECK(suggested_path.IsAbsolute()); 161 162 // Create a reservation map if one doesn't exist. It will be automatically 163 // deleted when all the reservations are revoked. 164 if (g_reservation_map == NULL) 165 g_reservation_map = new ReservationMap; 166 167 ReservationMap& reservations = *g_reservation_map; 168 DCHECK(!ContainsKey(reservations, key)); 169 170 base::FilePath target_path(suggested_path.NormalizePathSeparators()); 171 base::FilePath target_dir = target_path.DirName(); 172 base::FilePath filename = target_path.BaseName(); 173 bool is_path_writeable = true; 174 bool has_conflicts = false; 175 bool name_too_long = false; 176 177 // Create target_dir if necessary and appropriate. target_dir may be the last 178 // directory that the user selected in a FilePicker; if that directory has 179 // since been removed, do NOT automatically re-create it. Only automatically 180 // create the directory if it is the default Downloads directory or if the 181 // caller explicitly requested automatic directory creation. 182 if (!base::DirectoryExists(target_dir) && 183 (create_directory || 184 (!default_download_path.empty() && 185 (default_download_path == target_dir)))) { 186 base::CreateDirectory(target_dir); 187 } 188 189 // Check writability of the suggested path. If we can't write to it, default 190 // to the user's "My Documents" directory. We'll prompt them in this case. 191 if (!base::PathIsWritable(target_dir)) { 192 DVLOG(1) << "Unable to write to directory \"" << target_dir.value() << "\""; 193 is_path_writeable = false; 194 PathService::Get(chrome::DIR_USER_DOCUMENTS, &target_dir); 195 target_path = target_dir.Append(filename); 196 } 197 198 if (is_path_writeable) { 199 // Check the limit of file name length if it could be obtained. When the 200 // suggested name exceeds the limit, truncate or prompt the user. 201 int max_length = file_util::GetMaximumPathComponentLength(target_dir); 202 if (max_length != -1) { 203 int limit = max_length - kIntermediateNameSuffixLength; 204 if (limit <= 0 || !TruncateFileName(&target_path, limit)) 205 name_too_long = true; 206 } 207 208 // Uniquify the name, if it already exists. 209 if (!name_too_long && IsPathInUse(target_path)) { 210 has_conflicts = true; 211 if (conflict_action == DownloadPathReservationTracker::OVERWRITE) { 212 has_conflicts = false; 213 } 214 // If ...PROMPT, then |has_conflicts| will remain true, |verified| will be 215 // false, and CDMD will prompt. 216 if (conflict_action == DownloadPathReservationTracker::UNIQUIFY) { 217 for (int uniquifier = 1; 218 uniquifier <= DownloadPathReservationTracker::kMaxUniqueFiles; 219 ++uniquifier) { 220 // Append uniquifier. 221 std::string suffix(base::StringPrintf(" (%d)", uniquifier)); 222 base::FilePath path_to_check(target_path); 223 // If the name length limit is available (max_length != -1), and the 224 // the current name exceeds the limit, truncate. 225 if (max_length != -1) { 226 int limit = 227 max_length - kIntermediateNameSuffixLength - suffix.size(); 228 // If truncation failed, give up uniquification. 229 if (limit <= 0 || !TruncateFileName(&path_to_check, limit)) 230 break; 231 } 232 path_to_check = path_to_check.InsertBeforeExtensionASCII(suffix); 233 234 if (!IsPathInUse(path_to_check)) { 235 target_path = path_to_check; 236 has_conflicts = false; 237 break; 238 } 239 } 240 } 241 } 242 } 243 244 reservations[key] = target_path; 245 bool verified = (is_path_writeable && !has_conflicts && !name_too_long); 246 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, 247 base::Bind(callback, target_path, verified)); 248} 249 250// Called on the FILE thread to update the path of the reservation associated 251// with |key| to |new_path|. 252void UpdateReservation(ReservationKey key, const base::FilePath& new_path) { 253 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 254 DCHECK(g_reservation_map != NULL); 255 ReservationMap::iterator iter = g_reservation_map->find(key); 256 if (iter != g_reservation_map->end()) { 257 iter->second = new_path; 258 } else { 259 // This would happen if an UpdateReservation() notification was scheduled on 260 // the FILE thread before ReserveInternal(), or after a Revoke() 261 // call. Neither should happen. 262 NOTREACHED(); 263 } 264} 265 266// Called on the FILE thread to remove the path reservation associated with 267// |key|. 268void RevokeReservation(ReservationKey key) { 269 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 270 DCHECK(g_reservation_map != NULL); 271 DCHECK(ContainsKey(*g_reservation_map, key)); 272 g_reservation_map->erase(key); 273 if (g_reservation_map->size() == 0) { 274 // No more reservations. Delete map. 275 delete g_reservation_map; 276 g_reservation_map = NULL; 277 } 278} 279 280DownloadItemObserver::DownloadItemObserver(DownloadItem* download_item) 281 : download_item_(download_item), 282 last_target_path_(download_item->GetTargetFilePath()) { 283 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 284 download_item_->AddObserver(this); 285} 286 287DownloadItemObserver::~DownloadItemObserver() { 288 download_item_->RemoveObserver(this); 289} 290 291void DownloadItemObserver::OnDownloadUpdated(DownloadItem* download) { 292 switch (download->GetState()) { 293 case DownloadItem::IN_PROGRESS: { 294 // Update the reservation. 295 base::FilePath new_target_path = download->GetTargetFilePath(); 296 if (new_target_path != last_target_path_) { 297 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind( 298 &UpdateReservation, download, new_target_path)); 299 last_target_path_ = new_target_path; 300 } 301 break; 302 } 303 304 case DownloadItem::COMPLETE: 305 // If the download is complete, then it has already been renamed to the 306 // final name. The existence of the file on disk is sufficient to prevent 307 // conflicts from now on. 308 309 case DownloadItem::CANCELLED: 310 // We no longer need the reservation if the download is being removed. 311 312 case DownloadItem::INTERRUPTED: 313 // The download filename will need to be re-generated when the download is 314 // restarted. Holding on to the reservation now would prevent the name 315 // from being used for a subsequent retry attempt. 316 317 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind( 318 &RevokeReservation, download)); 319 delete this; 320 break; 321 322 case DownloadItem::MAX_DOWNLOAD_STATE: 323 // Compiler appeasement. 324 NOTREACHED(); 325 } 326} 327 328void DownloadItemObserver::OnDownloadDestroyed(DownloadItem* download) { 329 // Items should be COMPLETE/INTERRUPTED/CANCELLED before being destroyed. 330 NOTREACHED(); 331 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind( 332 &RevokeReservation, download)); 333 delete this; 334} 335 336} // namespace 337 338// static 339void DownloadPathReservationTracker::GetReservedPath( 340 DownloadItem* download_item, 341 const base::FilePath& target_path, 342 const base::FilePath& default_path, 343 bool create_directory, 344 FilenameConflictAction conflict_action, 345 const ReservedPathCallback& callback) { 346 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 347 // Attach an observer to the download item so that we know when the target 348 // path changes and/or the download is no longer active. 349 new DownloadItemObserver(download_item); 350 // DownloadItemObserver deletes itself. 351 352 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind( 353 &CreateReservation, 354 download_item, 355 target_path, 356 default_path, 357 create_directory, 358 conflict_action, 359 callback)); 360} 361 362// static 363bool DownloadPathReservationTracker::IsPathInUseForTesting( 364 const base::FilePath& path) { 365 return IsPathInUse(path); 366} 367