summaryrefslogtreecommitdiffstats
path: root/chrome/browser/download/download_path_reservation_tracker.cc
blob: b5912aee76e3f05460547fa1afaa39ed058f8ab5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/download/download_path_reservation_tracker.h"

#include <map>

#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/third_party/icu/icu_utf.h"
#include "chrome/common/chrome_paths.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item.h"

using content::BrowserThread;
using content::DownloadItem;

namespace {

typedef DownloadItem* ReservationKey;
typedef std::map<ReservationKey, base::FilePath> ReservationMap;

// The lower bound for file name truncation. If the truncation results in a name
// shorter than this limit, we give up automatic truncation and prompt the user.
static const size_t kTruncatedNameLengthLowerbound = 5;

// The length of the suffix string we append for an intermediate file name.
// In the file name truncation, we keep the margin to append the suffix.
// TODO(kinaba): remove the margin. The user should be able to set maximum
// possible filename.
static const size_t kIntermediateNameSuffixLength = sizeof(".crdownload") - 1;

// Map of download path reservations. Each reserved path is associated with a
// ReservationKey=DownloadItem*. This object is destroyed in |Revoke()| when
// there are no more reservations.
//
// It is not an error, although undesirable, to have multiple DownloadItem*s
// that are mapped to the same path. This can happen if a reservation is created
// that is supposed to overwrite an existing reservation.
ReservationMap* g_reservation_map = NULL;

// Observes a DownloadItem for changes to its target path and state. Updates or
// revokes associated download path reservations as necessary. Created, invoked
// and destroyed on the UI thread.
class DownloadItemObserver : public DownloadItem::Observer {
 public:
  explicit DownloadItemObserver(DownloadItem* download_item);

 private:
  virtual ~DownloadItemObserver();

  // DownloadItem::Observer
  virtual void OnDownloadUpdated(DownloadItem* download) OVERRIDE;
  virtual void OnDownloadDestroyed(DownloadItem* download) OVERRIDE;

  DownloadItem* download_item_;

  // Last known target path for the download.
  base::FilePath last_target_path_;

  DISALLOW_COPY_AND_ASSIGN(DownloadItemObserver);
};

// Returns true if the given path is in use by a path reservation.
bool IsPathReserved(const base::FilePath& path) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  // No reservation map => no reservations.
  if (g_reservation_map == NULL)
    return false;
  // Unfortunately path normalization doesn't work reliably for non-existant
  // files. So given a FilePath, we can't derive a normalized key that we can
  // use for lookups. We only expect a small number of concurrent downloads at
  // any given time, so going through all of them shouldn't be too slow.
  for (ReservationMap::const_iterator iter = g_reservation_map->begin();
       iter != g_reservation_map->end(); ++iter) {
    if (iter->second == path)
      return true;
  }
  return false;
}

// Returns true if the given path is in use by any path reservation or the
// file system. Called on the FILE thread.
bool IsPathInUse(const base::FilePath& path) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  // If there is a reservation, then the path is in use.
  if (IsPathReserved(path))
    return true;

  // If the path exists in the file system, then the path is in use.
  if (base::PathExists(path))
    return true;

  return false;
}

// Truncates path->BaseName() to make path->BaseName().value().size() <= limit.
// - It keeps the extension as is. Only truncates the body part.
// - It secures the base filename length to be more than or equals to
//   kTruncatedNameLengthLowerbound.
// If it was unable to shorten the name, returns false.
bool TruncateFileName(base::FilePath* path, size_t limit) {
  base::FilePath basename(path->BaseName());
  // It is already short enough.
  if (basename.value().size() <= limit)
    return true;

  base::FilePath dir(path->DirName());
  base::FilePath::StringType ext(basename.Extension());
  base::FilePath::StringType name(basename.RemoveExtension().value());

  // Impossible to satisfy the limit.
  if (limit < kTruncatedNameLengthLowerbound + ext.size())
    return false;
  limit -= ext.size();

  // Encoding specific truncation logic.
  base::FilePath::StringType truncated;
#if defined(OS_CHROMEOS) || defined(OS_MACOSX)
  // UTF-8.
  base::TruncateUTF8ToByteSize(name, limit, &truncated);
#elif defined(OS_WIN)
  // UTF-16.
  DCHECK(name.size() > limit);
  truncated = name.substr(0, CBU16_IS_TRAIL(name[limit]) ? limit - 1 : limit);
#else
  // We cannot generally assume that the file name encoding is in UTF-8 (see
  // the comment for FilePath::AsUTF8Unsafe), hence no safe way to truncate.
#endif

  if (truncated.size() < kTruncatedNameLengthLowerbound)
    return false;
  *path = dir.Append(truncated + ext);
  return true;
}

// Called on the FILE thread to reserve a download path. This method:
// - Creates directory |default_download_path| if it doesn't exist.
// - Verifies that the parent directory of |suggested_path| exists and is
//   writeable.
// - Truncates the suggested name if it exceeds the filesystem's limit.
// - Uniquifies |suggested_path| if |should_uniquify_path| is true.
// - Returns true if |reserved_path| has been successfully verified.
bool CreateReservation(
    ReservationKey key,
    const base::FilePath& suggested_path,
    const base::FilePath& default_download_path,
    bool create_directory,
    DownloadPathReservationTracker::FilenameConflictAction conflict_action,
    base::FilePath* reserved_path) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  DCHECK(suggested_path.IsAbsolute());

  // Create a reservation map if one doesn't exist. It will be automatically
  // deleted when all the reservations are revoked.
  if (g_reservation_map == NULL)
    g_reservation_map = new ReservationMap;

  ReservationMap& reservations = *g_reservation_map;
  DCHECK(!ContainsKey(reservations, key));

  base::FilePath target_path(suggested_path.NormalizePathSeparators());
  base::FilePath target_dir = target_path.DirName();
  base::FilePath filename = target_path.BaseName();
  bool is_path_writeable = true;
  bool has_conflicts = false;
  bool name_too_long = false;

  // Create target_dir if necessary and appropriate. target_dir may be the last
  // directory that the user selected in a FilePicker; if that directory has
  // since been removed, do NOT automatically re-create it. Only automatically
  // create the directory if it is the default Downloads directory or if the
  // caller explicitly requested automatic directory creation.
  if (!base::DirectoryExists(target_dir) &&
      (create_directory ||
       (!default_download_path.empty() &&
        (default_download_path == target_dir)))) {
    base::CreateDirectory(target_dir);
  }

  // Check writability of the suggested path. If we can't write to it, default
  // to the user's "My Documents" directory. We'll prompt them in this case.
  if (!base::PathIsWritable(target_dir)) {
    DVLOG(1) << "Unable to write to directory \"" << target_dir.value() << "\"";
    is_path_writeable = false;
    PathService::Get(chrome::DIR_USER_DOCUMENTS, &target_dir);
    target_path = target_dir.Append(filename);
  }

  if (is_path_writeable) {
    // Check the limit of file name length if it could be obtained. When the
    // suggested name exceeds the limit, truncate or prompt the user.
    int max_length = base::GetMaximumPathComponentLength(target_dir);
    if (max_length != -1) {
      int limit = max_length - kIntermediateNameSuffixLength;
      if (limit <= 0 || !TruncateFileName(&target_path, limit))
        name_too_long = true;
    }

    // Uniquify the name, if it already exists.
    if (!name_too_long && IsPathInUse(target_path)) {
      has_conflicts = true;
      if (conflict_action == DownloadPathReservationTracker::OVERWRITE) {
        has_conflicts = false;
      }
      // If ...PROMPT, then |has_conflicts| will remain true, |verified| will be
      // false, and CDMD will prompt.
      if (conflict_action == DownloadPathReservationTracker::UNIQUIFY) {
        for (int uniquifier = 1;
            uniquifier <= DownloadPathReservationTracker::kMaxUniqueFiles;
            ++uniquifier) {
          // Append uniquifier.
          std::string suffix(base::StringPrintf(" (%d)", uniquifier));
          base::FilePath path_to_check(target_path);
          // If the name length limit is available (max_length != -1), and the
          // the current name exceeds the limit, truncate.
          if (max_length != -1) {
            int limit =
                max_length - kIntermediateNameSuffixLength - suffix.size();
            // If truncation failed, give up uniquification.
            if (limit <= 0 || !TruncateFileName(&path_to_check, limit))
              break;
          }
          path_to_check = path_to_check.InsertBeforeExtensionASCII(suffix);

          if (!IsPathInUse(path_to_check)) {
            target_path = path_to_check;
            has_conflicts = false;
            break;
          }
        }
      }
    }
  }

  reservations[key] = target_path;
  bool verified = (is_path_writeable && !has_conflicts && !name_too_long);
  *reserved_path = target_path;
  return verified;
}

// Called on the FILE thread to update the path of the reservation associated
// with |key| to |new_path|.
void UpdateReservation(ReservationKey key, const base::FilePath& new_path) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  DCHECK(g_reservation_map != NULL);
  ReservationMap::iterator iter = g_reservation_map->find(key);
  if (iter != g_reservation_map->end()) {
    iter->second = new_path;
  } else {
    // This would happen if an UpdateReservation() notification was scheduled on
    // the FILE thread before ReserveInternal(), or after a Revoke()
    // call. Neither should happen.
    NOTREACHED();
  }
}

// Called on the FILE thread to remove the path reservation associated with
// |key|.
void RevokeReservation(ReservationKey key) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  DCHECK(g_reservation_map != NULL);
  DCHECK(ContainsKey(*g_reservation_map, key));
  g_reservation_map->erase(key);
  if (g_reservation_map->size() == 0) {
    // No more reservations. Delete map.
    delete g_reservation_map;
    g_reservation_map = NULL;
  }
}

void RunGetReservedPathCallback(
    const DownloadPathReservationTracker::ReservedPathCallback& callback,
    const base::FilePath* reserved_path,
    bool verified) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  callback.Run(*reserved_path, verified);
}

DownloadItemObserver::DownloadItemObserver(DownloadItem* download_item)
    : download_item_(download_item),
      last_target_path_(download_item->GetTargetFilePath()) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  download_item_->AddObserver(this);
}

DownloadItemObserver::~DownloadItemObserver() {
  download_item_->RemoveObserver(this);
}

void DownloadItemObserver::OnDownloadUpdated(DownloadItem* download) {
  switch (download->GetState()) {
    case DownloadItem::IN_PROGRESS: {
      // Update the reservation.
      base::FilePath new_target_path = download->GetTargetFilePath();
      if (new_target_path != last_target_path_) {
        BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind(
            &UpdateReservation, download, new_target_path));
        last_target_path_ = new_target_path;
      }
      break;
    }

    case DownloadItem::COMPLETE:
      // If the download is complete, then it has already been renamed to the
      // final name. The existence of the file on disk is sufficient to prevent
      // conflicts from now on.

    case DownloadItem::CANCELLED:
      // We no longer need the reservation if the download is being removed.

    case DownloadItem::INTERRUPTED:
      // The download filename will need to be re-generated when the download is
      // restarted. Holding on to the reservation now would prevent the name
      // from being used for a subsequent retry attempt.

      BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind(
          &RevokeReservation, download));
      delete this;
      break;

    case DownloadItem::MAX_DOWNLOAD_STATE:
      // Compiler appeasement.
      NOTREACHED();
  }
}

void DownloadItemObserver::OnDownloadDestroyed(DownloadItem* download) {
  // Items should be COMPLETE/INTERRUPTED/CANCELLED before being destroyed.
  NOTREACHED();
  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind(
      &RevokeReservation, download));
  delete this;
}

}  // namespace

// static
void DownloadPathReservationTracker::GetReservedPath(
    DownloadItem* download_item,
    const base::FilePath& target_path,
    const base::FilePath& default_path,
    bool create_directory,
    FilenameConflictAction conflict_action,
    const ReservedPathCallback& callback) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  // Attach an observer to the download item so that we know when the target
  // path changes and/or the download is no longer active.
  new DownloadItemObserver(download_item);
  // DownloadItemObserver deletes itself.

  base::FilePath* reserved_path = new base::FilePath;
  BrowserThread::PostTaskAndReplyWithResult(
      BrowserThread::FILE,
      FROM_HERE,
      base::Bind(&CreateReservation,
                 download_item,
                 target_path,
                 default_path,
                 create_directory,
                 conflict_action,
                 reserved_path),
      base::Bind(&RunGetReservedPathCallback,
                 callback,
                 base::Owned(reserved_path)));
}

// static
bool DownloadPathReservationTracker::IsPathInUseForTesting(
    const base::FilePath& path) {
  return IsPathInUse(path);
}