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