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