Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: WMS Client Driver
4 : * Purpose: Implementation of Dataset and RasterBand classes for WMS
5 : * and other similar services.
6 : * Author: Adam Nowacki, nowak@xpam.de
7 : *
8 : ******************************************************************************
9 : * Copyright (c) 2007, Adam Nowacki
10 : * Copyright (c) 2017, Dmitry Baryshnikov, <polimax@mail.ru>
11 : * Copyright (c) 2017, NextGIS, <info@nextgis.com>
12 : *
13 : * SPDX-License-Identifier: MIT
14 : ****************************************************************************/
15 :
16 : #include "cpl_md5.h"
17 : #include "wmsdriver.h"
18 :
19 14 : static void CleanCacheThread(void *pData)
20 : {
21 14 : GDALWMSCache *pCache = static_cast<GDALWMSCache *>(pData);
22 14 : pCache->Clean();
23 14 : }
24 :
25 : //------------------------------------------------------------------------------
26 : // GDALWMSFileCache
27 : //------------------------------------------------------------------------------
28 : class GDALWMSFileCache : public GDALWMSCacheImpl
29 : {
30 : public:
31 217 : GDALWMSFileCache(const CPLString &soPath, CPLXMLNode *pConfig)
32 217 : : GDALWMSCacheImpl(soPath, pConfig), m_osPostfix(""), m_nDepth(2),
33 : m_nExpires(604800), // 7 days
34 : m_nMaxSize(67108864), // 64 Mb
35 217 : m_nCleanThreadRunTimeout(120) // 3 min
36 : {
37 217 : const char *pszCacheDepth = CPLGetXMLValue(pConfig, "Depth", "2");
38 217 : if (pszCacheDepth != nullptr)
39 217 : m_nDepth = atoi(pszCacheDepth);
40 :
41 : const char *pszCacheExtension =
42 217 : CPLGetXMLValue(pConfig, "Extension", nullptr);
43 217 : if (pszCacheExtension != nullptr)
44 0 : m_osPostfix = pszCacheExtension;
45 :
46 : const char *pszCacheExpires =
47 217 : CPLGetXMLValue(pConfig, "Expires", nullptr);
48 217 : if (pszCacheExpires != nullptr)
49 : {
50 1 : m_nExpires = atoi(pszCacheExpires);
51 1 : CPLDebug("WMS", "Cache expires in %d sec", m_nExpires);
52 : }
53 :
54 : const char *pszCacheMaxSize =
55 217 : CPLGetXMLValue(pConfig, "MaxSize", nullptr);
56 217 : if (pszCacheMaxSize != nullptr)
57 0 : m_nMaxSize = atol(pszCacheMaxSize);
58 :
59 : const char *pszCleanThreadRunTimeout =
60 217 : CPLGetXMLValue(pConfig, "CleanTimeout", nullptr);
61 217 : if (pszCleanThreadRunTimeout != nullptr)
62 : {
63 0 : m_nCleanThreadRunTimeout = atoi(pszCleanThreadRunTimeout);
64 0 : CPLDebug("WMS", "Clean Thread Run Timeout is %d sec",
65 : m_nCleanThreadRunTimeout);
66 : }
67 217 : }
68 :
69 : virtual int GetCleanThreadRunTimeout() override;
70 :
71 22 : virtual CPLErr Insert(const char *pszKey,
72 : const CPLString &osFileName) override
73 : {
74 : // Warns if it fails to write, but returns success
75 44 : CPLString soFilePath = GetFilePath(pszKey);
76 22 : MakeDirs(CPLGetDirnameSafe(soFilePath).c_str());
77 22 : if (CPLCopyFile(soFilePath, osFileName) == CE_None)
78 21 : return CE_None;
79 : // Warn if it fails after folder creation
80 1 : CPLError(CE_Warning, CPLE_FileIO, "Error writing to WMS cache %s",
81 : m_soPath.c_str());
82 1 : return CE_None;
83 : }
84 :
85 : virtual enum GDALWMSCacheItemStatus
86 36 : GetItemStatus(const char *pszKey) const override
87 : {
88 : VSIStatBufL sStatBuf;
89 36 : if (VSIStatL(GetFilePath(pszKey), &sStatBuf) == 0)
90 : {
91 16 : long seconds = static_cast<long>(time(nullptr) - sStatBuf.st_mtime);
92 16 : return seconds < m_nExpires ? CACHE_ITEM_OK : CACHE_ITEM_EXPIRED;
93 : }
94 20 : return CACHE_ITEM_NOT_FOUND;
95 : }
96 :
97 14 : virtual GDALDataset *GetDataset(const char *pszKey,
98 : char **papszOpenOptions) const override
99 : {
100 14 : return GDALDataset::FromHandle(GDALOpenEx(
101 28 : GetFilePath(pszKey),
102 : GDAL_OF_RASTER | GDAL_OF_READONLY | GDAL_OF_VERBOSE_ERROR, nullptr,
103 28 : papszOpenOptions, nullptr));
104 : }
105 :
106 14 : virtual void Clean() override
107 : {
108 14 : char **papszList = VSIReadDirRecursive(m_soPath);
109 14 : if (papszList == nullptr)
110 : {
111 1 : return;
112 : }
113 :
114 13 : int counter = 0;
115 26 : std::vector<int> toDelete;
116 13 : long nSize = 0;
117 13 : time_t nTime = time(nullptr);
118 64 : while (papszList[counter] != nullptr)
119 : {
120 : const std::string osPath =
121 51 : CPLFormFilenameSafe(m_soPath, papszList[counter], nullptr);
122 : VSIStatBufL sStatBuf;
123 51 : if (VSIStatL(osPath.c_str(), &sStatBuf) == 0)
124 : {
125 51 : if (!VSI_ISDIR(sStatBuf.st_mode))
126 : {
127 17 : long seconds = static_cast<long>(nTime - sStatBuf.st_mtime);
128 17 : if (seconds > m_nExpires)
129 : {
130 3 : toDelete.push_back(counter);
131 : }
132 :
133 17 : nSize += static_cast<long>(sStatBuf.st_size);
134 : }
135 : }
136 51 : counter++;
137 : }
138 :
139 13 : if (nSize > m_nMaxSize)
140 : {
141 0 : CPLDebug("WMS", "Delete %u items from cache",
142 0 : static_cast<unsigned int>(toDelete.size()));
143 0 : for (size_t i = 0; i < toDelete.size(); ++i)
144 : {
145 : const std::string osPath = CPLFormFilenameSafe(
146 0 : m_soPath, papszList[toDelete[i]], nullptr);
147 0 : VSIUnlink(osPath.c_str());
148 : }
149 : }
150 :
151 13 : CSLDestroy(papszList);
152 : }
153 :
154 : private:
155 72 : CPLString GetFilePath(const char *pszKey) const
156 : {
157 144 : CPLString soHash(CPLMD5String(pszKey));
158 72 : CPLString soCacheFile(m_soPath);
159 :
160 72 : if (!soCacheFile.empty() && soCacheFile.back() != '/')
161 : {
162 72 : soCacheFile.append(1, '/');
163 : }
164 :
165 216 : for (int i = 0; i < m_nDepth; ++i)
166 : {
167 144 : soCacheFile.append(1, soHash[i]);
168 144 : soCacheFile.append(1, '/');
169 : }
170 72 : soCacheFile.append(soHash);
171 72 : soCacheFile.append(m_osPostfix);
172 144 : return soCacheFile;
173 : }
174 :
175 74 : static void MakeDirs(const char *pszPath)
176 : {
177 74 : if (IsPathExists(pszPath))
178 : {
179 22 : return;
180 : }
181 : // Recursive makedirs, ignoring errors
182 52 : MakeDirs(CPLGetDirnameSafe(pszPath).c_str());
183 :
184 52 : VSIMkdir(pszPath, 0744);
185 : }
186 :
187 74 : static bool IsPathExists(const char *pszPath)
188 : {
189 : VSIStatBufL sbuf;
190 74 : return VSIStatL(pszPath, &sbuf) == 0;
191 : }
192 :
193 : private:
194 : CPLString m_osPostfix;
195 : int m_nDepth;
196 : int m_nExpires;
197 : long m_nMaxSize;
198 : int m_nCleanThreadRunTimeout;
199 : };
200 :
201 22 : int GDALWMSFileCache::GetCleanThreadRunTimeout()
202 : {
203 22 : return m_nCleanThreadRunTimeout;
204 : }
205 :
206 : //------------------------------------------------------------------------------
207 : // GDALWMSCache
208 : //------------------------------------------------------------------------------
209 :
210 : GDALWMSCache::GDALWMSCache() = default;
211 :
212 217 : GDALWMSCache::~GDALWMSCache()
213 : {
214 217 : if (m_hThread)
215 14 : CPLJoinThread(m_hThread);
216 217 : delete m_poCache;
217 217 : }
218 :
219 217 : CPLErr GDALWMSCache::Initialize(const char *pszUrl, CPLXMLNode *pConfig)
220 : {
221 362 : const auto NullifyIfEmpty = [](const char *pszStr)
222 362 : { return pszStr && pszStr[0] != 0 ? pszStr : nullptr; };
223 :
224 217 : if (const char *pszXmlCachePath =
225 217 : NullifyIfEmpty(CPLGetXMLValue(pConfig, "Path", nullptr)))
226 : {
227 112 : m_osCachePath = pszXmlCachePath;
228 : }
229 105 : else if (const char *pszUserCachePath = NullifyIfEmpty(
230 : CPLGetConfigOption("GDAL_DEFAULT_WMS_CACHE_PATH", nullptr)))
231 : {
232 94 : m_osCachePath = pszUserCachePath;
233 : }
234 11 : else if (const char *pszXDG_CACHE_HOME =
235 11 : NullifyIfEmpty(CPLGetConfigOption("XDG_CACHE_HOME", nullptr)))
236 : {
237 : m_osCachePath =
238 1 : CPLFormFilenameSafe(pszXDG_CACHE_HOME, "gdalwmscache", nullptr);
239 : }
240 : else
241 : {
242 : #ifdef _WIN32
243 : const char *pszHome =
244 : NullifyIfEmpty(CPLGetConfigOption("USERPROFILE", nullptr));
245 : #else
246 : const char *pszHome =
247 10 : NullifyIfEmpty(CPLGetConfigOption("HOME", nullptr));
248 : #endif
249 10 : if (pszHome)
250 : {
251 5 : m_osCachePath = CPLFormFilenameSafe(
252 10 : CPLFormFilenameSafe(pszHome, ".cache", nullptr).c_str(),
253 5 : "gdalwmscache", nullptr);
254 : }
255 : else
256 : {
257 : const char *pszDir =
258 5 : NullifyIfEmpty(CPLGetConfigOption("CPL_TMPDIR", nullptr));
259 :
260 5 : if (!pszDir)
261 3 : pszDir = NullifyIfEmpty(CPLGetConfigOption("TMPDIR", nullptr));
262 :
263 5 : if (!pszDir)
264 2 : pszDir = NullifyIfEmpty(CPLGetConfigOption("TEMP", nullptr));
265 :
266 5 : if (!pszDir)
267 1 : pszDir = ".";
268 :
269 : const char *pszUsername =
270 5 : NullifyIfEmpty(CPLGetConfigOption("USERNAME", nullptr));
271 5 : if (!pszUsername)
272 : pszUsername =
273 4 : NullifyIfEmpty(CPLGetConfigOption("USER", nullptr));
274 :
275 5 : if (pszUsername)
276 : {
277 8 : m_osCachePath = CPLFormFilenameSafe(
278 : pszDir, CPLSPrintf("gdalwmscache_%s", pszUsername),
279 4 : nullptr);
280 : }
281 : else
282 : {
283 2 : m_osCachePath = CPLFormFilenameSafe(
284 : pszDir,
285 : CPLSPrintf("gdalwmscache_%s",
286 : CPLMD5String(pszUrl ? pszUrl : "")),
287 1 : nullptr);
288 : }
289 : }
290 : }
291 :
292 : // Separate folder for each unique dataset url
293 217 : if (CPLTestBool(CPLGetXMLValue(pConfig, "Unique", "True")))
294 : {
295 218 : m_osCachePath = CPLFormFilenameSafe(
296 109 : m_osCachePath, CPLMD5String(pszUrl ? pszUrl : ""), nullptr);
297 : }
298 217 : CPLDebug("WMS", "Using %s for cache", m_osCachePath.c_str());
299 :
300 : // TODO: Add sqlite db cache type
301 217 : const char *pszType = CPLGetXMLValue(pConfig, "Type", "file");
302 217 : if (EQUAL(pszType, "file"))
303 : {
304 217 : m_poCache = new GDALWMSFileCache(m_osCachePath, pConfig);
305 : }
306 :
307 217 : return CE_None;
308 : }
309 :
310 22 : CPLErr GDALWMSCache::Insert(const char *pszKey, const CPLString &soFileName)
311 : {
312 22 : if (m_poCache != nullptr && pszKey != nullptr)
313 : {
314 : // Add file to cache
315 22 : CPLErr result = m_poCache->Insert(pszKey, soFileName);
316 22 : if (result == CE_None)
317 : {
318 : // Start clean thread
319 22 : int cleanThreadRunTimeout = m_poCache->GetCleanThreadRunTimeout();
320 44 : if (cleanThreadRunTimeout > 0 && !m_bIsCleanThreadRunning &&
321 22 : time(nullptr) - m_nCleanThreadLastRunTime >
322 22 : cleanThreadRunTimeout)
323 : {
324 14 : if (m_hThread)
325 0 : CPLJoinThread(m_hThread);
326 14 : m_bIsCleanThreadRunning = true;
327 14 : m_hThread = CPLCreateJoinableThread(CleanCacheThread, this);
328 : }
329 : }
330 22 : return result;
331 : }
332 :
333 0 : return CE_Failure;
334 : }
335 :
336 : enum GDALWMSCacheItemStatus
337 36 : GDALWMSCache::GetItemStatus(const char *pszKey) const
338 : {
339 36 : if (m_poCache != nullptr)
340 : {
341 36 : return m_poCache->GetItemStatus(pszKey);
342 : }
343 0 : return CACHE_ITEM_NOT_FOUND;
344 : }
345 :
346 14 : GDALDataset *GDALWMSCache::GetDataset(const char *pszKey,
347 : char **papszOpenOptions) const
348 : {
349 14 : if (m_poCache != nullptr)
350 : {
351 14 : return m_poCache->GetDataset(pszKey, papszOpenOptions);
352 : }
353 0 : return nullptr;
354 : }
355 :
356 14 : void GDALWMSCache::Clean()
357 : {
358 14 : if (m_poCache != nullptr)
359 : {
360 14 : CPLDebug("WMS", "Clean cache");
361 14 : m_poCache->Clean();
362 : }
363 :
364 14 : m_nCleanThreadLastRunTime = time(nullptr);
365 14 : m_bIsCleanThreadRunning = false;
366 14 : }
|