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