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 "components/download/public/common/download_path_reservation_tracker.h"
6
7 #include <stddef.h>
8
9 #include <map>
10 #include <string>
11
12 #include "base/bind.h"
13 #include "base/callback.h"
14 #include "base/files/file_path.h"
15 #include "base/files/file_util.h"
16 #include "base/logging.h"
17 #include "base/macros.h"
18 #include "base/memory/ptr_util.h"
19 #include "base/sequenced_task_runner.h"
20 #include "base/stl_util.h"
21 #include "base/strings/string_util.h"
22 #include "base/strings/stringprintf.h"
23 #include "base/task/lazy_thread_pool_task_runner.h"
24 #include "base/task/post_task.h"
25 #include "base/task_runner_util.h"
26 #include "base/third_party/icu/icu_utf.h"
27 #include "base/time/time.h"
28 #include "build/build_config.h"
29 #include "components/download/public/common/download_features.h"
30 #include "components/download/public/common/download_item.h"
31 #include "components/filename_generation/filename_generation.h"
32 #include "net/base/filename_util.h"
33 #include "url/gurl.h"
34
35 #if defined(OS_ANDROID)
36 #include "components/download/internal/common/android/download_collection_bridge.h"
37 #endif
38
39 namespace download {
40
41 namespace {
42
43 typedef DownloadItem* ReservationKey;
44 typedef std::map<ReservationKey, base::FilePath> ReservationMap;
45
46 // The length of the suffix string we append for an intermediate file name.
47 // In the file name truncation, we keep the margin to append the suffix.
48 // TODO(kinaba): remove the margin. The user should be able to set maximum
49 // possible filename.
50 const size_t kIntermediateNameSuffixLength = sizeof(".crdownload") - 1;
51
52 #if defined(OS_WIN)
53 // On windows, zone identifier is appended to the downloaded file name during
54 // annotation. That increases the length of the final target path.
55 const size_t kZoneIdentifierLength = sizeof(":Zone.Identifier") - 1;
56 #endif // defined(OS_WIN)
57
58 // Map of download path reservations. Each reserved path is associated with a
59 // ReservationKey=DownloadItem*. This object is destroyed in |Revoke()| when
60 // there are no more reservations.
61 ReservationMap* g_reservation_map = NULL;
62
63 base::LazyThreadPoolSequencedTaskRunner g_sequenced_task_runner =
64 LAZY_THREAD_POOL_SEQUENCED_TASK_RUNNER_INITIALIZER(
65 base::TaskTraits(base::MayBlock()));
66
67 // Observes a DownloadItem for changes to its target path and state. Updates or
68 // revokes associated download path reservations as necessary. Created, invoked
69 // and destroyed on the UI thread.
70 class DownloadItemObserver : public DownloadItem::Observer,
71 public base::SupportsUserData::Data {
72 public:
73 explicit DownloadItemObserver(DownloadItem* download_item);
74 ~DownloadItemObserver() override;
75
76 private:
77 // DownloadItem::Observer
78 void OnDownloadUpdated(DownloadItem* download) override;
79 void OnDownloadDestroyed(DownloadItem* download) override;
80
81 DownloadItem* download_item_;
82
83 // Last known target path for the download.
84 base::FilePath last_target_path_;
85
86 static const int kUserDataKey;
87
88 DISALLOW_COPY_AND_ASSIGN(DownloadItemObserver);
89 };
90
91 // Returns true if the given path is in use by a path reservation.
IsPathReserved(const base::FilePath & path)92 bool IsPathReserved(const base::FilePath& path) {
93 // No reservation map => no reservations.
94 if (g_reservation_map == NULL)
95 return false;
96
97 // We only expect a small number of concurrent downloads at any given time, so
98 // going through all of them shouldn't be too slow.
99 for (ReservationMap::const_iterator iter = g_reservation_map->begin();
100 iter != g_reservation_map->end(); ++iter) {
101 if (base::FilePath::CompareEqualIgnoreCase(iter->second.value(),
102 path.value()))
103 return true;
104 }
105 return false;
106 }
107
108 // Returns true if the given file name is in use by any path reservation or the
109 // file system. Called on the task runner returned by
110 // DownloadPathReservationTracker::GetTaskRunner().
IsFileNameInUse(const base::FilePath & path)111 bool IsFileNameInUse(const base::FilePath& path) {
112 #if defined(OS_ANDROID)
113 // If there is a reservation, then the path is in use.
114 if (IsPathReserved(path.BaseName()))
115 return true;
116
117 if (DownloadCollectionBridge::FileNameExists(path.BaseName()))
118 return true;
119 #endif
120 return false;
121 }
122
123 // Returns true if the given path is in use by a path reservation,
124 // and has a different key then the item arg. Called on the task runner returned
125 // by DownloadPathReservationTracker::GetTaskRunner().
IsAdditionalPathReserved(const base::FilePath & path,DownloadItem * item)126 bool IsAdditionalPathReserved(const base::FilePath& path, DownloadItem* item) {
127 if (!g_reservation_map)
128 return false;
129
130 for (ReservationMap::const_iterator iter = g_reservation_map->begin();
131 iter != g_reservation_map->end(); ++iter) {
132 if (iter->first != item && base::FilePath::CompareEqualIgnoreCase(
133 iter->second.value(), path.value()))
134 return true;
135 }
136 return false;
137 }
138
139 // Returns true if the given path is in use by any path reservation or the
140 // file system. Called on the task runner returned by
141 // DownloadPathReservationTracker::GetTaskRunner().
IsPathInUse(const base::FilePath & path)142 bool IsPathInUse(const base::FilePath& path) {
143 // If there is a reservation, then the path is in use.
144 if (IsPathReserved(path))
145 return true;
146
147 // If the path exists in the file system, then the path is in use.
148 if (base::PathExists(path))
149 return true;
150
151 return false;
152 }
153
154 // Create a unique filename by appending a uniquifier. Modifies |path| in place
155 // if successful and returns true. Otherwise |path| is left unmodified and
156 // returns false. If |check_file_name_only| is true, this method will only check
157 // the name of the file to guarantee that it is unique.
CreateUniqueFilename(int max_path_component_length,const base::Time & download_start_time,base::FilePath * path,bool check_file_name_only)158 bool CreateUniqueFilename(int max_path_component_length,
159 const base::Time& download_start_time,
160 base::FilePath* path,
161 bool check_file_name_only) {
162 // Try every numeric uniquifier. Then make one attempt with the timestamp.
163 for (int uniquifier = 1;
164 uniquifier <= DownloadPathReservationTracker::kMaxUniqueFiles + 1;
165 ++uniquifier) {
166 // Append uniquifier.
167 std::string suffix(base::StringPrintf(" (%d)", uniquifier));
168
169 // After we've tried all the unique numeric indices, make one attempt using
170 // the timestamp.
171 if (uniquifier > DownloadPathReservationTracker::kMaxUniqueFiles) {
172 // Generate an ISO8601 compliant local timestamp suffix that avoids
173 // reserved characters that are forbidden on some OSes like Windows.
174 base::Time::Exploded exploded;
175 download_start_time.LocalExplode(&exploded);
176 suffix = base::StringPrintf(
177 " - %04d-%02d-%02dT%02d%02d%02d.%03d", exploded.year, exploded.month,
178 exploded.day_of_month, exploded.hour, exploded.minute,
179 exploded.second, exploded.millisecond);
180 }
181
182 base::FilePath path_to_check(*path);
183 // If the name length limit is available (max_length != -1), and the
184 // the current name exceeds the limit, truncate.
185 if (max_path_component_length != -1) {
186 #if defined(OS_WIN)
187 int limit =
188 max_path_component_length -
189 std::max(kIntermediateNameSuffixLength, kZoneIdentifierLength) -
190 suffix.size();
191 #else
192 int limit = max_path_component_length - kIntermediateNameSuffixLength -
193 suffix.size();
194 #endif // defined(OS_WIN)
195 // If truncation failed, give up uniquification.
196 if (limit <= 0 ||
197 !filename_generation::TruncateFilename(&path_to_check, limit))
198 break;
199 }
200 path_to_check = path_to_check.InsertBeforeExtensionASCII(suffix);
201
202 if ((check_file_name_only && !IsFileNameInUse(path_to_check)) ||
203 (!check_file_name_only && !IsPathInUse(path_to_check))) {
204 *path = path_to_check;
205 return true;
206 }
207 }
208
209 return false;
210 }
211
212 struct CreateReservationInfo {
213 ReservationKey key;
214 base::FilePath source_path;
215 base::FilePath suggested_path;
216 base::FilePath default_download_path;
217 base::FilePath temporary_path;
218 base::FilePath fallback_directory; // directory to use when target path
219 // cannot be used.
220 bool create_target_directory;
221 base::Time start_time;
222 DownloadPathReservationTracker::FilenameConflictAction conflict_action;
223 };
224
225 // Check if |target_path| is writable.
IsPathWritable(const CreateReservationInfo & info,const base::FilePath & target_path)226 bool IsPathWritable(const CreateReservationInfo& info,
227 const base::FilePath& target_path) {
228 if (base::PathIsWritable(target_path.DirName()))
229 return true;
230 // If a temporary file is already created under the same dir as |target_path|,
231 // return true. This is to avoid the windows network share issue. See
232 // http://crbug.com/383765.
233 return !info.temporary_path.empty() &&
234 info.temporary_path.DirName() == target_path.DirName();
235 }
236
237 // Called when reservation conflicts happen. Returns the result on whether the
238 // conflict can be resolved, and uniquifying the file name if necessary. If
239 // |check_file_name_only| is true, this method will only check the name of the
240 // file to guarantee that it is unique.
ResolveReservationConflicts(const CreateReservationInfo & info,int max_path_component_length,base::FilePath * target_path,bool check_file_name_only)241 PathValidationResult ResolveReservationConflicts(
242 const CreateReservationInfo& info,
243 int max_path_component_length,
244 base::FilePath* target_path,
245 bool check_file_name_only) {
246 switch (info.conflict_action) {
247 case DownloadPathReservationTracker::UNIQUIFY:
248 return CreateUniqueFilename(max_path_component_length, info.start_time,
249 target_path, check_file_name_only)
250 ? PathValidationResult::SUCCESS
251 : PathValidationResult::CONFLICT;
252
253 case DownloadPathReservationTracker::OVERWRITE:
254 if (IsPathReserved(*target_path))
255 return PathValidationResult::CONFLICT;
256 return PathValidationResult::SUCCESS;
257
258 case DownloadPathReservationTracker::PROMPT:
259 return PathValidationResult::CONFLICT;
260 }
261 NOTREACHED();
262 return PathValidationResult::SUCCESS;
263 }
264
265 // Verify that |target_path| can be written to and also resolve any conflicts if
266 // necessary by uniquifying the filename.
ValidatePathAndResolveConflicts(const CreateReservationInfo & info,base::FilePath * target_path)267 PathValidationResult ValidatePathAndResolveConflicts(
268 const CreateReservationInfo& info,
269 base::FilePath* target_path) {
270 // Check writability of the suggested path. If we can't write to it, use
271 // the |default_download_path| if it is not empty or |fallback_directory|.
272 // We'll prompt them in this case. No further amendments are made to the
273 // filename since the user is going to be prompted.
274 if (!IsPathWritable(info, *target_path)) {
275 DVLOG(1) << "Unable to write to path \"" << target_path->value() << "\"";
276 if (!info.default_download_path.empty() &&
277 target_path->DirName() != info.default_download_path) {
278 *target_path = info.default_download_path.Append(target_path->BaseName());
279 } else {
280 *target_path = info.fallback_directory.Append(target_path->BaseName());
281 }
282 return PathValidationResult::PATH_NOT_WRITABLE;
283 }
284
285 int max_path_component_length =
286 base::GetMaximumPathComponentLength(target_path->DirName());
287 // Check the limit of file name length if it could be obtained. When the
288 // suggested name exceeds the limit, truncate or prompt the user.
289 if (max_path_component_length != -1) {
290 #if defined(OS_WIN)
291 int limit = max_path_component_length -
292 std::max(kIntermediateNameSuffixLength, kZoneIdentifierLength);
293 #else
294 int limit = max_path_component_length - kIntermediateNameSuffixLength;
295 #endif // defined(OS_WIN)
296 if (limit <= 0 ||
297 !filename_generation::TruncateFilename(target_path, limit))
298 return PathValidationResult::NAME_TOO_LONG;
299 }
300
301 // Disallow downloading a file onto itself. Assume that downloading a file
302 // onto another file that differs only by case is not enough of a legitimate
303 // edge case to justify determining the case sensitivity of the underlying
304 // filesystem.
305 if (*target_path == info.source_path)
306 return PathValidationResult::SAME_AS_SOURCE;
307
308 if (!IsPathInUse(*target_path))
309 return PathValidationResult::SUCCESS;
310
311 return ResolveReservationConflicts(info, max_path_component_length,
312 target_path, false);
313 }
314
315 // Called on the task runner returned by
316 // DownloadPathReservationTracker::GetTaskRunner() to reserve a download path.
317 // This method:
318 // - Creates directory |default_download_path| if it doesn't exist.
319 // - Verifies that the parent directory of |suggested_path| exists and is
320 // writeable.
321 // - Truncates the suggested name if it exceeds the filesystem's limit.
322 // - Uniquifies |suggested_path| if |should_uniquify_path| is true.
323 // - Schedules |callback| on the UI thread with the reserved path and a flag
324 // indicating whether the returned path has been successfully verified.
325 // - Returns the result of creating the path reservation.
CreateReservation(const CreateReservationInfo & info,base::FilePath * reserved_path)326 PathValidationResult CreateReservation(const CreateReservationInfo& info,
327 base::FilePath* reserved_path) {
328 // Create a reservation map if one doesn't exist. It will be automatically
329 // deleted when all the reservations are revoked.
330 if (g_reservation_map == NULL)
331 g_reservation_map = new ReservationMap;
332
333 // Erase the reservation if it already exists. This can happen during
334 // automatic resumption where a new target determination request may be issued
335 // for a DownloadItem without an intervening transition to INTERRUPTED.
336 //
337 // Revoking and re-acquiring the reservation forces us to re-verify the claims
338 // we are making about the path.
339 g_reservation_map->erase(info.key);
340
341 base::FilePath target_path(info.suggested_path.NormalizePathSeparators());
342 base::FilePath target_dir = target_path.DirName();
343 base::FilePath filename = target_path.BaseName();
344
345 #if defined(OS_ANDROID)
346 if (DownloadCollectionBridge::ShouldPublishDownload(target_path)) {
347 // If the download is written to a content URI, put file name in the
348 // reservation map as content URIs will always be different.
349 PathValidationResult result = PathValidationResult::SUCCESS;
350 if (IsFileNameInUse(filename)) {
351 int max_path_component_length =
352 base::GetMaximumPathComponentLength(target_path.DirName());
353 result = ResolveReservationConflicts(info, max_path_component_length,
354 &target_path, true);
355 }
356 (*g_reservation_map)[info.key] = target_path.BaseName();
357 *reserved_path = target_path;
358 return result;
359 }
360 #endif
361 // Create target_dir if necessary and appropriate. target_dir may be the last
362 // directory that the user selected in a FilePicker; if that directory has
363 // since been removed, do NOT automatically re-create it. Only automatically
364 // create the directory if it is the default Downloads directory or if the
365 // caller explicitly requested automatic directory creation.
366 if (!base::DirectoryExists(target_dir) &&
367 (info.create_target_directory ||
368 (!info.default_download_path.empty() &&
369 (info.default_download_path == target_dir)))) {
370 base::CreateDirectory(target_dir);
371 }
372
373 PathValidationResult result =
374 ValidatePathAndResolveConflicts(info, &target_path);
375 (*g_reservation_map)[info.key] = target_path;
376 *reserved_path = target_path;
377 return result;
378 }
379
380 // Called on a background thread to update the path of the reservation
381 // associated with |key| to |new_path|.
UpdateReservation(ReservationKey key,const base::FilePath & new_path)382 void UpdateReservation(ReservationKey key, const base::FilePath& new_path) {
383 DCHECK(g_reservation_map != NULL);
384 auto iter = g_reservation_map->find(key);
385 if (iter != g_reservation_map->end()) {
386 iter->second = new_path;
387 } else {
388 // This would happen if an UpdateReservation() notification was scheduled on
389 // the SequencedTaskRunner before ReserveInternal(), or after a Revoke()
390 // call. Neither should happen.
391 NOTREACHED();
392 }
393 }
394
395 // Called on the FILE thread to remove the path reservation associated with
396 // |key|.
RevokeReservation(ReservationKey key)397 void RevokeReservation(ReservationKey key) {
398 DCHECK(g_reservation_map != NULL);
399 DCHECK(base::Contains(*g_reservation_map, key));
400 g_reservation_map->erase(key);
401 if (g_reservation_map->size() == 0) {
402 // No more reservations. Delete map.
403 delete g_reservation_map;
404 g_reservation_map = NULL;
405 }
406 }
407
RunGetReservedPathCallback(DownloadPathReservationTracker::ReservedPathCallback callback,const base::FilePath * reserved_path,PathValidationResult result)408 void RunGetReservedPathCallback(
409 DownloadPathReservationTracker::ReservedPathCallback callback,
410 const base::FilePath* reserved_path,
411 PathValidationResult result) {
412 std::move(callback).Run(result, *reserved_path);
413 }
414
415 // Gets the path reserved in the global |g_reservation_map|. For content Uri,
416 // file name instead of file path is used.
GetReservationPath(DownloadItem * download_item)417 base::FilePath GetReservationPath(DownloadItem* download_item) {
418 #if defined(OS_ANDROID)
419 if (download_item->GetTargetFilePath().IsContentUri())
420 return download_item->GetFileNameToReportUser();
421 #endif
422 return download_item->GetTargetFilePath();
423 }
424
DownloadItemObserver(DownloadItem * download_item)425 DownloadItemObserver::DownloadItemObserver(DownloadItem* download_item)
426 : download_item_(download_item),
427 last_target_path_(GetReservationPath(download_item)) {
428 download_item_->AddObserver(this);
429 download_item_->SetUserData(&kUserDataKey, base::WrapUnique(this));
430 }
431
~DownloadItemObserver()432 DownloadItemObserver::~DownloadItemObserver() {
433 download_item_->RemoveObserver(this);
434 // DownloadItemObserver is owned by DownloadItem. It should only be getting
435 // destroyed because it's being removed from the UserData pool. No need to
436 // call DownloadItem::RemoveUserData().
437 }
438
OnDownloadUpdated(DownloadItem * download)439 void DownloadItemObserver::OnDownloadUpdated(DownloadItem* download) {
440 switch (download->GetState()) {
441 case DownloadItem::IN_PROGRESS: {
442 // Update the reservation.
443 base::FilePath new_target_path = GetReservationPath(download);
444 if (new_target_path != last_target_path_) {
445 DownloadPathReservationTracker::GetTaskRunner()->PostTask(
446 FROM_HERE,
447 base::BindOnce(&UpdateReservation, download, new_target_path));
448 last_target_path_ = new_target_path;
449 }
450 break;
451 }
452
453 case DownloadItem::COMPLETE:
454 // If the download is complete, then it has already been renamed to the
455 // final name. The existence of the file on disk is sufficient to prevent
456 // conflicts from now on.
457
458 case DownloadItem::CANCELLED:
459 // We no longer need the reservation if the download is being removed.
460
461 case DownloadItem::INTERRUPTED:
462 // The download filename will need to be re-generated when the download is
463 // restarted. Holding on to the reservation now would prevent the name
464 // from being used for a subsequent retry attempt.
465 DownloadPathReservationTracker::GetTaskRunner()->PostTask(
466 FROM_HERE, base::BindOnce(&RevokeReservation, download));
467 download->RemoveUserData(&kUserDataKey);
468 break;
469
470 case DownloadItem::MAX_DOWNLOAD_STATE:
471 // Compiler appeasement.
472 NOTREACHED();
473 }
474 }
475
OnDownloadDestroyed(DownloadItem * download)476 void DownloadItemObserver::OnDownloadDestroyed(DownloadItem* download) {
477 // Items should be COMPLETE/INTERRUPTED/CANCELLED before being destroyed.
478 NOTREACHED();
479 DownloadPathReservationTracker::GetTaskRunner()->PostTask(
480 FROM_HERE, base::BindOnce(&RevokeReservation, download));
481 }
482
483 // static
484 const int DownloadItemObserver::kUserDataKey = 0;
485
486 } // namespace
487
488 // static
GetReservedPath(DownloadItem * download_item,const base::FilePath & target_path,const base::FilePath & default_path,const base::FilePath & fallback_directory,bool create_directory,FilenameConflictAction conflict_action,ReservedPathCallback callback)489 void DownloadPathReservationTracker::GetReservedPath(
490 DownloadItem* download_item,
491 const base::FilePath& target_path,
492 const base::FilePath& default_path,
493 const base::FilePath& fallback_directory,
494 bool create_directory,
495 FilenameConflictAction conflict_action,
496 ReservedPathCallback callback) {
497 // Attach an observer to the download item so that we know when the target
498 // path changes and/or the download is no longer active.
499 new DownloadItemObserver(download_item);
500 // DownloadItemObserver deletes itself.
501
502 base::FilePath* reserved_path = new base::FilePath;
503 base::FilePath source_path;
504 if (download_item->GetURL().SchemeIsFile())
505 net::FileURLToFilePath(download_item->GetURL(), &source_path);
506 CreateReservationInfo info = {static_cast<ReservationKey>(download_item),
507 source_path,
508 target_path,
509 default_path,
510 download_item->GetTemporaryFilePath(),
511 fallback_directory,
512 create_directory,
513 download_item->GetStartTime(),
514 conflict_action};
515
516 base::PostTaskAndReplyWithResult(
517 GetTaskRunner().get(), FROM_HERE,
518 base::BindOnce(&CreateReservation, info, reserved_path),
519 base::BindOnce(&RunGetReservedPathCallback, std::move(callback),
520 base::Owned(reserved_path)));
521 }
522
523 // static
IsPathInUseForTesting(const base::FilePath & path)524 bool DownloadPathReservationTracker::IsPathInUseForTesting(
525 const base::FilePath& path) {
526 return IsPathInUse(path);
527 }
528
529 // static
530 scoped_refptr<base::SequencedTaskRunner>
GetTaskRunner()531 DownloadPathReservationTracker::GetTaskRunner() {
532 return g_sequenced_task_runner.Get();
533 }
534
535 // static
CheckDownloadPathForExistingDownload(const base::FilePath & target_path,DownloadItem * download_item,CheckDownloadPathCallback callback)536 void DownloadPathReservationTracker::CheckDownloadPathForExistingDownload(
537 const base::FilePath& target_path,
538 DownloadItem* download_item,
539 CheckDownloadPathCallback callback) {
540 base::PostTaskAndReplyWithResult(
541 GetTaskRunner().get(), FROM_HERE,
542 base::BindOnce(&IsAdditionalPathReserved, target_path, download_item),
543 std::move(callback));
544 }
545
546 } // namespace download
547