1 /*
2 * This file is part of RawTherapee.
3 *
4 * Copyright (c) 2004-2010 Gabor Horvath <hgabor@rawtherapee.com>
5 *
6 * RawTherapee is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * RawTherapee is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with RawTherapee. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20 #include <memory>
21 #include <iostream>
22
23 #include <dirent.h>
24 #include <giomm.h>
25 #include <glib/gstdio.h>
26
27 #ifdef WIN32
28 #include <fileapi.h>
29 #endif
30
31 #include "cachemanager.h"
32
33 #include "guiutils.h"
34 #include "options.h"
35 #include "thumbnail.h"
36 #include "procparamchangers.h"
37
38 namespace
39 {
40
41 constexpr int cacheDirMode = 0777;
42 constexpr const char* cacheDirs[] = { "profiles", "images", "aehistograms", "embprofiles", "data" };
43
44 }
45
getInstance()46 CacheManager* CacheManager::getInstance ()
47 {
48 static CacheManager instance;
49 return &instance;
50 }
51
init()52 void CacheManager::init ()
53 {
54 MyMutex::MyLock lock (mutex);
55
56 openEntries.clear ();
57 baseDir = options.cacheBaseDir;
58
59 auto error = g_mkdir_with_parents (baseDir.c_str(), cacheDirMode);
60
61 for (const auto& cacheDir : cacheDirs) {
62 if (strncmp(cacheDir, "aehistograms", 12)) { // don't create aehistograms folder.
63 error |= g_mkdir_with_parents (Glib::build_filename (baseDir, cacheDir).c_str(), cacheDirMode);
64 }
65 }
66
67 if (error != 0 && rtengine::settings->verbose) {
68 std::cerr << "Failed to create all cache directories: " << g_strerror(errno) << std::endl;
69 }
70 }
71
getEntry(const Glib::ustring & fname)72 Thumbnail* CacheManager::getEntry (const Glib::ustring& fname)
73 {
74 std::unique_ptr<Thumbnail> thumbnail;
75
76 // take manager lock and search for entry,
77 // if found return it,
78 // else release lock and create it
79 {
80 MyMutex::MyLock lock (mutex);
81
82 // if it is open, return it
83 const auto iterator = openEntries.find (fname);
84 if (iterator != openEntries.end ()) {
85
86 auto cachedThumbnail = iterator->second;
87
88 cachedThumbnail->increaseRef ();
89 return cachedThumbnail;
90 }
91 }
92
93 // build path name
94 const auto md5 = getMD5 (fname);
95
96 if (md5.empty ()) {
97 return nullptr;
98 }
99
100 const auto cacheName = getCacheFileName ("data", fname, ".txt", md5);
101
102 // let's see if we have it in the cache
103 {
104 CacheImageData imageData;
105
106 const auto error = imageData.load (cacheName);
107 if (error == 0 && imageData.supported) {
108
109 thumbnail.reset (new Thumbnail (this, fname, &imageData));
110 if (!thumbnail->isSupported ()) {
111 thumbnail.reset ();
112 }
113 }
114 }
115
116 // if not, create a new one
117 if (!thumbnail) {
118
119 thumbnail.reset (new Thumbnail (this, fname, md5));
120 if (!thumbnail->isSupported ()) {
121 thumbnail.reset ();
122 }
123 }
124
125 // retake the lock and see if it was added while we we're unlocked, if it
126 // was use it over our version. if not added we create the cache entry
127 if (thumbnail) {
128 MyMutex::MyLock lock (mutex);
129
130 const auto iterator = openEntries.find (fname);
131 if (iterator != openEntries.end ()) {
132
133 auto cachedThumbnail = iterator->second;
134
135 cachedThumbnail->increaseRef ();
136 return cachedThumbnail;
137 }
138
139 // it wasn't, create a new entry
140 openEntries.emplace (fname, thumbnail.get ());
141 }
142
143 return thumbnail.release ();
144 }
145
146
deleteEntry(const Glib::ustring & fname)147 void CacheManager::deleteEntry (const Glib::ustring& fname)
148 {
149 MyMutex::MyLock lock (mutex);
150
151 // check if it is opened
152 auto iterator = openEntries.find (fname);
153 if (iterator == openEntries.end ()) {
154 deleteFiles (fname, getMD5 (fname), true, true);
155 return;
156 }
157
158 auto thumbnail = iterator->second;
159
160 // decrease reference count;
161 // this will call back into CacheManager,
162 // so we release the lock for it
163 {
164 lock.release ();
165 thumbnail->decreaseRef ();
166 lock.acquire ();
167 }
168
169 // check again if in the editor,
170 // the thumbnail still exists,
171 // if not, delete it
172 if (openEntries.count (fname) == 0) {
173 deleteFiles (fname, thumbnail->getMD5 (), true, true);
174 }
175 }
176
clearFromCache(const Glib::ustring & fname,bool purge) const177 void CacheManager::clearFromCache (const Glib::ustring& fname, bool purge) const
178 {
179 deleteFiles (fname, getMD5 (fname), true, purge);
180 }
181
renameEntry(const std::string & oldfilename,const std::string & oldmd5,const std::string & newfilename)182 void CacheManager::renameEntry (const std::string& oldfilename, const std::string& oldmd5, const std::string& newfilename)
183 {
184 MyMutex::MyLock lock (mutex);
185
186 const auto newmd5 = getMD5 (newfilename);
187
188 auto error = g_rename (getCacheFileName ("profiles", oldfilename, paramFileExtension, oldmd5).c_str (), getCacheFileName ("profiles", newfilename, paramFileExtension, newmd5).c_str ());
189 error |= g_rename (getCacheFileName ("images", oldfilename, ".rtti", oldmd5).c_str (), getCacheFileName ("images", newfilename, ".rtti", newmd5).c_str ());
190 error |= g_rename (getCacheFileName ("aehistograms", oldfilename, "", oldmd5).c_str (), getCacheFileName ("aehistograms", newfilename, "", newmd5).c_str ());
191 error |= g_rename (getCacheFileName ("embprofiles", oldfilename, ".icc", oldmd5).c_str (), getCacheFileName ("embprofiles", newfilename, ".icc", newmd5).c_str ());
192 error |= g_rename (getCacheFileName ("data", oldfilename, ".txt", oldmd5).c_str (), getCacheFileName ("data", newfilename, ".txt", newmd5).c_str ());
193
194 if (error != 0 && rtengine::settings->verbose) {
195 std::cerr << "Failed to rename all files for cache entry '" << oldfilename << "': " << g_strerror(errno) << std::endl;
196 }
197
198 // check if it is opened
199 // if it is open, update md5
200 const auto iterator = openEntries.find (oldfilename);
201 if (iterator == openEntries.end ()) {
202 return;
203 }
204
205 auto thumbnail = iterator->second;
206 openEntries.erase (iterator);
207 openEntries.emplace (newfilename, thumbnail);
208
209 thumbnail->setFileName (newfilename);
210 thumbnail->updateCache ();
211 thumbnail->saveThumbnail ();
212 }
213
closeThumbnail(Thumbnail * thumbnail)214 void CacheManager::closeThumbnail (Thumbnail* thumbnail)
215 {
216 MyMutex::MyLock lock (mutex);
217
218 openEntries.erase (thumbnail->getFileName ());
219 delete thumbnail;
220 }
221
closeCache() const222 void CacheManager::closeCache () const
223 {
224 MyMutex::MyLock lock (mutex);
225
226 applyCacheSizeLimitation ();
227 }
228
clearAll() const229 void CacheManager::clearAll () const
230 {
231 MyMutex::MyLock lock (mutex);
232
233 for (const auto& cacheDir : cacheDirs) {
234 deleteDir (cacheDir);
235 }
236 }
237
clearImages() const238 void CacheManager::clearImages () const
239 {
240 MyMutex::MyLock lock (mutex);
241
242 deleteDir ("data");
243 deleteDir ("images");
244 deleteDir ("aehistograms");
245 deleteDir ("embprofiles");
246 }
247
clearProfiles() const248 void CacheManager::clearProfiles () const
249 {
250 MyMutex::MyLock lock (mutex);
251
252 deleteDir ("profiles");
253 }
254
deleteDir(const Glib::ustring & dirName) const255 void CacheManager::deleteDir (const Glib::ustring& dirName) const
256 {
257 try {
258
259 Glib::Dir dir (Glib::build_filename (baseDir, dirName));
260
261 auto error = 0;
262 for (auto entry = dir.begin (); entry != dir.end (); ++entry) {
263 error |= g_remove (Glib::build_filename (baseDir, dirName, *entry).c_str ());
264 }
265
266 if (error != 0 && rtengine::settings->verbose) {
267 std::cerr << "Failed to delete all entries in cache directory '" << dirName << "': " << g_strerror(errno) << std::endl;
268 }
269
270 } catch (Glib::Error&) {}
271 }
272
deleteFiles(const Glib::ustring & fname,const std::string & md5,bool purgeData,bool purgeProfile) const273 void CacheManager::deleteFiles (const Glib::ustring& fname, const std::string& md5, bool purgeData, bool purgeProfile) const
274 {
275 if (md5.empty ()) {
276 return;
277 }
278
279 auto error = g_remove (getCacheFileName ("images", fname, ".rtti", md5).c_str ());
280 error |= g_remove (getCacheFileName ("aehistograms", fname, "", md5).c_str ());
281 error |= g_remove (getCacheFileName ("embprofiles", fname, ".icc", md5).c_str ());
282
283 if (purgeData) {
284 error |= g_remove (getCacheFileName ("data", fname, ".txt", md5).c_str ());
285 }
286
287 if (purgeProfile) {
288 error |= g_remove (getCacheFileName ("profiles", fname, paramFileExtension, md5).c_str ());
289 }
290
291 if (error != 0 && rtengine::settings->verbose) {
292 std::cerr << "Failed to delete all files for cache entry '" << fname << "': " << g_strerror(errno) << std::endl;
293 }
294 }
295
getMD5(const Glib::ustring & fname)296 std::string CacheManager::getMD5 (const Glib::ustring& fname)
297 {
298
299 #ifdef WIN32
300
301 std::unique_ptr<wchar_t, GFreeFunc> wfname(reinterpret_cast<wchar_t*>(g_utf8_to_utf16 (fname.c_str (), -1, NULL, NULL, NULL)), g_free);
302
303 WIN32_FILE_ATTRIBUTE_DATA fileAttr;
304 if (GetFileAttributesExW(wfname.get(), GetFileExInfoStandard, &fileAttr)) {
305 // We use name, size and creation time to identify a file.
306 const auto identifier = Glib::ustring::compose("%1-%2-%3-%4", fileAttr.nFileSizeLow, fileAttr.ftCreationTime.dwHighDateTime, fileAttr.ftCreationTime.dwLowDateTime, fname);
307 return Glib::Checksum::compute_checksum(Glib::Checksum::CHECKSUM_MD5, identifier);
308 }
309
310 #else
311
312 const auto file = Gio::File::create_for_path(fname);
313 if (file) {
314
315 try
316 {
317 const auto info = file->query_info("standard::*");
318 if (info) {
319 // We only use name and size to identify a file.
320 const auto identifier = Glib::ustring::compose("%1%2", fname, info->get_size());
321 return Glib::Checksum::compute_checksum(Glib::Checksum::CHECKSUM_MD5, identifier);
322 }
323
324 } catch(Gio::Error&) {}
325 }
326
327 #endif
328
329 return {};
330 }
331
getCacheFileName(const Glib::ustring & subDir,const Glib::ustring & fname,const Glib::ustring & fext,const Glib::ustring & md5) const332 Glib::ustring CacheManager::getCacheFileName (const Glib::ustring& subDir,
333 const Glib::ustring& fname,
334 const Glib::ustring& fext,
335 const Glib::ustring& md5) const
336 {
337 const auto dirName = Glib::build_filename (baseDir, subDir);
338 const auto baseName = Glib::path_get_basename (fname) + "." + md5;
339 return Glib::build_filename (dirName, baseName + fext);
340 }
341
applyCacheSizeLimitation() const342 void CacheManager::applyCacheSizeLimitation () const
343 {
344 // first count files without fetching file name and timestamp.
345 auto cachedir = opendir(Glib::build_filename(baseDir, "data").c_str());
346 if (!cachedir) {
347 return;
348 }
349
350 std::size_t numFiles = 0;
351 while (readdir(cachedir)) {
352 ++numFiles;
353 }
354
355 closedir(cachedir);
356 if (numFiles > 2) {
357 numFiles -= 2; // because . and .. are counted
358 }
359
360 if (numFiles <= options.maxCacheEntries) {
361 return;
362 }
363
364 using FNameMTime = std::pair<Glib::ustring, Glib::TimeVal>;
365
366 std::vector<FNameMTime> files;
367 files.reserve(numFiles);
368
369 constexpr std::size_t md5_size = 32;
370 // get filenames and timestamps
371 try {
372 const auto dir = Gio::File::create_for_path(Glib::build_filename(baseDir, "data"));
373 const auto enumerator = dir->enumerate_children("standard::name,time::modified");
374
375 while (const auto file = enumerator->next_file()) {
376 const auto name = file->get_name();
377 if (name.size() >= md5_size + 5) {
378 files.emplace_back(name, file->modification_time());
379 }
380 }
381
382 } catch (Glib::Exception&) {}
383
384 if (files.size() <= options.maxCacheEntries) {
385 // limit not reached
386 return;
387 }
388
389 const std::size_t toDelete = files.size() - options.maxCacheEntries + options.maxCacheEntries * 5 / 100; // reserve 5% free cache space
390
391 std::nth_element(
392 files.begin(),
393 files.begin() + toDelete,
394 files.end(),
395 [](const FNameMTime& lhs, const FNameMTime& rhs) -> bool
396 {
397 return lhs.second < rhs.second;
398 }
399 );
400
401 for (std::vector<FNameMTime>::const_iterator entry = files.begin(), end = files.begin() + toDelete; entry != end; ++entry) {
402 const auto& name = entry->first;
403 const auto name_size = name.size() - md5_size;
404 const auto fname = name.substr(0, name_size - 5);
405 const auto md5 = name.substr(name_size - 4, md5_size);
406
407 deleteFiles(fname, md5, true, false);
408 }
409 }
410
411