1/*
2 *  Copyright 2004 The WebRTC Project Authors. All rights reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#include <time.h>
12
13#if defined(WEBRTC_WIN)
14#include "webrtc/base/win32.h"
15#endif
16
17#include <algorithm>
18#include "webrtc/base/arraysize.h"
19#include "webrtc/base/common.h"
20#include "webrtc/base/diskcache.h"
21#include "webrtc/base/fileutils.h"
22#include "webrtc/base/pathutils.h"
23#include "webrtc/base/stream.h"
24#include "webrtc/base/stringencode.h"
25#include "webrtc/base/stringutils.h"
26
27#if !defined(NDEBUG)
28#define TRANSPARENT_CACHE_NAMES 1
29#else
30#define TRANSPARENT_CACHE_NAMES 0
31#endif
32
33namespace rtc {
34
35class DiskCache;
36
37///////////////////////////////////////////////////////////////////////////////
38// DiskCacheAdapter
39///////////////////////////////////////////////////////////////////////////////
40
41class DiskCacheAdapter : public StreamAdapterInterface {
42public:
43  DiskCacheAdapter(const DiskCache* cache, const std::string& id, size_t index,
44                   StreamInterface* stream)
45  : StreamAdapterInterface(stream), cache_(cache), id_(id), index_(index)
46  { }
47  ~DiskCacheAdapter() override {
48    Close();
49    cache_->ReleaseResource(id_, index_);
50  }
51
52private:
53  const DiskCache* cache_;
54  std::string id_;
55  size_t index_;
56};
57
58///////////////////////////////////////////////////////////////////////////////
59// DiskCache
60///////////////////////////////////////////////////////////////////////////////
61
62DiskCache::DiskCache() : max_cache_(0), total_size_(0), total_accessors_(0) {
63}
64
65DiskCache::~DiskCache() {
66  ASSERT(0 == total_accessors_);
67}
68
69bool DiskCache::Initialize(const std::string& folder, size_t size) {
70  if (!folder_.empty() || !Filesystem::CreateFolder(folder))
71    return false;
72
73  folder_ = folder;
74  max_cache_ = size;
75  ASSERT(0 == total_size_);
76
77  if (!InitializeEntries())
78    return false;
79
80  return CheckLimit();
81}
82
83bool DiskCache::Purge() {
84  if (folder_.empty())
85    return false;
86
87  if (total_accessors_ > 0) {
88    LOG_F(LS_WARNING) << "Cache files open";
89    return false;
90  }
91
92  if (!PurgeFiles())
93    return false;
94
95  map_.clear();
96  return true;
97}
98
99bool DiskCache::LockResource(const std::string& id) {
100  Entry* entry = GetOrCreateEntry(id, true);
101  if (LS_LOCKED == entry->lock_state)
102    return false;
103  if ((LS_UNLOCKED == entry->lock_state) && (entry->accessors > 0))
104    return false;
105  if ((total_size_ > max_cache_) && !CheckLimit()) {
106    LOG_F(LS_WARNING) << "Cache overfull";
107    return false;
108  }
109  entry->lock_state = LS_LOCKED;
110  return true;
111}
112
113StreamInterface* DiskCache::WriteResource(const std::string& id, size_t index) {
114  Entry* entry = GetOrCreateEntry(id, false);
115  if (LS_LOCKED != entry->lock_state)
116    return NULL;
117
118  size_t previous_size = 0;
119  std::string filename(IdToFilename(id, index));
120  FileStream::GetSize(filename, &previous_size);
121  ASSERT(previous_size <= entry->size);
122  if (previous_size > entry->size) {
123    previous_size = entry->size;
124  }
125
126  scoped_ptr<FileStream> file(new FileStream);
127  if (!file->Open(filename, "wb", NULL)) {
128    LOG_F(LS_ERROR) << "Couldn't create cache file";
129    return NULL;
130  }
131
132  entry->streams = std::max(entry->streams, index + 1);
133  entry->size -= previous_size;
134  total_size_ -= previous_size;
135
136  entry->accessors += 1;
137  total_accessors_ += 1;
138  return new DiskCacheAdapter(this, id, index, file.release());
139}
140
141bool DiskCache::UnlockResource(const std::string& id) {
142  Entry* entry = GetOrCreateEntry(id, false);
143  if (LS_LOCKED != entry->lock_state)
144    return false;
145
146  if (entry->accessors > 0) {
147    entry->lock_state = LS_UNLOCKING;
148  } else {
149    entry->lock_state = LS_UNLOCKED;
150    entry->last_modified = time(0);
151    CheckLimit();
152  }
153  return true;
154}
155
156StreamInterface* DiskCache::ReadResource(const std::string& id,
157                                         size_t index) const {
158  const Entry* entry = GetEntry(id);
159  if (LS_UNLOCKED != entry->lock_state)
160    return NULL;
161  if (index >= entry->streams)
162    return NULL;
163
164  scoped_ptr<FileStream> file(new FileStream);
165  if (!file->Open(IdToFilename(id, index), "rb", NULL))
166    return NULL;
167
168  entry->accessors += 1;
169  total_accessors_ += 1;
170  return new DiskCacheAdapter(this, id, index, file.release());
171}
172
173bool DiskCache::HasResource(const std::string& id) const {
174  const Entry* entry = GetEntry(id);
175  return (NULL != entry) && (entry->streams > 0);
176}
177
178bool DiskCache::HasResourceStream(const std::string& id, size_t index) const {
179  const Entry* entry = GetEntry(id);
180  if ((NULL == entry) || (index >= entry->streams))
181    return false;
182
183  std::string filename = IdToFilename(id, index);
184
185  return FileExists(filename);
186}
187
188bool DiskCache::DeleteResource(const std::string& id) {
189  Entry* entry = GetOrCreateEntry(id, false);
190  if (!entry)
191    return true;
192
193  if ((LS_UNLOCKED != entry->lock_state) || (entry->accessors > 0))
194    return false;
195
196  bool success = true;
197  for (size_t index = 0; index < entry->streams; ++index) {
198    std::string filename = IdToFilename(id, index);
199
200    if (!FileExists(filename))
201      continue;
202
203    if (!DeleteFile(filename)) {
204      LOG_F(LS_ERROR) << "Couldn't remove cache file: " << filename;
205      success = false;
206    }
207  }
208
209  total_size_ -= entry->size;
210  map_.erase(id);
211  return success;
212}
213
214bool DiskCache::CheckLimit() {
215#if !defined(NDEBUG)
216  // Temporary check to make sure everything is working correctly.
217  size_t cache_size = 0;
218  for (EntryMap::iterator it = map_.begin(); it != map_.end(); ++it) {
219    cache_size += it->second.size;
220  }
221  ASSERT(cache_size == total_size_);
222#endif
223
224  // TODO: Replace this with a non-brain-dead algorithm for clearing out the
225  // oldest resources... something that isn't O(n^2)
226  while (total_size_ > max_cache_) {
227    EntryMap::iterator oldest = map_.end();
228    for (EntryMap::iterator it = map_.begin(); it != map_.end(); ++it) {
229      if ((LS_UNLOCKED != it->second.lock_state) || (it->second.accessors > 0))
230        continue;
231      oldest = it;
232      break;
233    }
234    if (oldest == map_.end()) {
235      LOG_F(LS_WARNING) << "All resources are locked!";
236      return false;
237    }
238    for (EntryMap::iterator it = oldest++; it != map_.end(); ++it) {
239      if (it->second.last_modified < oldest->second.last_modified) {
240        oldest = it;
241      }
242    }
243    if (!DeleteResource(oldest->first)) {
244      LOG_F(LS_ERROR) << "Couldn't delete from cache!";
245      return false;
246    }
247  }
248  return true;
249}
250
251std::string DiskCache::IdToFilename(const std::string& id, size_t index) const {
252#ifdef TRANSPARENT_CACHE_NAMES
253  // This escapes colons and other filesystem characters, so the user can't open
254  // special devices (like "COM1:"), or access other directories.
255  size_t buffer_size = id.length()*3 + 1;
256  char* buffer = new char[buffer_size];
257  encode(buffer, buffer_size, id.data(), id.length(),
258         unsafe_filename_characters(), '%');
259  // TODO: ASSERT(strlen(buffer) < FileSystem::MaxBasenameLength());
260#else  // !TRANSPARENT_CACHE_NAMES
261  // We might want to just use a hash of the filename at some point, both for
262  // obfuscation, and to avoid both filename length and escaping issues.
263  ASSERT(false);
264#endif  // !TRANSPARENT_CACHE_NAMES
265
266  char extension[32];
267  sprintfn(extension, arraysize(extension), ".%u", index);
268
269  Pathname pathname;
270  pathname.SetFolder(folder_);
271  pathname.SetBasename(buffer);
272  pathname.SetExtension(extension);
273
274#ifdef TRANSPARENT_CACHE_NAMES
275  delete [] buffer;
276#endif  // TRANSPARENT_CACHE_NAMES
277
278  return pathname.pathname();
279}
280
281bool DiskCache::FilenameToId(const std::string& filename, std::string* id,
282                             size_t* index) const {
283  Pathname pathname(filename);
284  unsigned tempdex;
285  if (1 != sscanf(pathname.extension().c_str(), ".%u", &tempdex))
286    return false;
287
288  *index = static_cast<size_t>(tempdex);
289
290  size_t buffer_size = pathname.basename().length() + 1;
291  char* buffer = new char[buffer_size];
292  decode(buffer, buffer_size, pathname.basename().data(),
293         pathname.basename().length(), '%');
294  id->assign(buffer);
295  delete [] buffer;
296  return true;
297}
298
299DiskCache::Entry* DiskCache::GetOrCreateEntry(const std::string& id,
300                                              bool create) {
301  EntryMap::iterator it = map_.find(id);
302  if (it != map_.end())
303    return &it->second;
304  if (!create)
305    return NULL;
306  Entry e;
307  e.lock_state = LS_UNLOCKED;
308  e.accessors = 0;
309  e.size = 0;
310  e.streams = 0;
311  e.last_modified = time(0);
312  it = map_.insert(EntryMap::value_type(id, e)).first;
313  return &it->second;
314}
315
316void DiskCache::ReleaseResource(const std::string& id, size_t index) const {
317  const Entry* entry = GetEntry(id);
318  if (!entry) {
319    LOG_F(LS_WARNING) << "Missing cache entry";
320    ASSERT(false);
321    return;
322  }
323
324  entry->accessors -= 1;
325  total_accessors_ -= 1;
326
327  if (LS_UNLOCKED != entry->lock_state) {
328    // This is safe, because locked resources only issue WriteResource, which
329    // is non-const.  Think about a better way to handle it.
330    DiskCache* this2 = const_cast<DiskCache*>(this);
331    Entry* entry2 = this2->GetOrCreateEntry(id, false);
332
333    size_t new_size = 0;
334    std::string filename(IdToFilename(id, index));
335    FileStream::GetSize(filename, &new_size);
336    entry2->size += new_size;
337    this2->total_size_ += new_size;
338
339    if ((LS_UNLOCKING == entry->lock_state) && (0 == entry->accessors)) {
340      entry2->last_modified = time(0);
341      entry2->lock_state = LS_UNLOCKED;
342      this2->CheckLimit();
343    }
344  }
345}
346
347///////////////////////////////////////////////////////////////////////////////
348
349} // namespace rtc
350