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