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(CPLGetDirname(soFilePath));
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 char *pszPath =
124 45 : CPLFormFilename(m_soPath, papszList[counter], nullptr);
125 : VSIStatBufL sStatBuf;
126 45 : if (VSIStatL(pszPath, &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 0 : 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 char *pszPath =
149 0 : CPLFormFilename(m_soPath, papszList[toDelete[i]], nullptr);
150 0 : VSIUnlink(pszPath);
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 : const char *pszDirPath = CPLGetDirname(pszPath);
186 42 : MakeDirs(pszDirPath);
187 :
188 42 : VSIMkdir(pszPath, 0744);
189 : }
190 :
191 60 : static bool IsPathExists(const char *pszPath)
192 : {
193 : VSIStatBufL sbuf;
194 60 : return VSIStatL(pszPath, &sbuf) == 0;
195 : }
196 :
197 : private:
198 : CPLString m_osPostfix;
199 : int m_nDepth;
200 : int m_nExpires;
201 : long m_nMaxSize;
202 : int m_nCleanThreadRunTimeout;
203 : };
204 :
205 : //------------------------------------------------------------------------------
206 : // GDALWMSCache
207 : //------------------------------------------------------------------------------
208 :
209 : GDALWMSCache::GDALWMSCache() = default;
210 :
211 213 : GDALWMSCache::~GDALWMSCache()
212 : {
213 213 : if (m_hThread)
214 12 : CPLJoinThread(m_hThread);
215 213 : delete m_poCache;
216 213 : }
217 :
218 213 : CPLErr GDALWMSCache::Initialize(const char *pszUrl, CPLXMLNode *pConfig)
219 : {
220 354 : const auto NullifyIfEmpty = [](const char *pszStr)
221 354 : { return pszStr && pszStr[0] != 0 ? pszStr : nullptr; };
222 :
223 213 : if (const char *pszXmlCachePath =
224 213 : NullifyIfEmpty(CPLGetXMLValue(pConfig, "Path", nullptr)))
225 : {
226 112 : m_osCachePath = pszXmlCachePath;
227 : }
228 101 : else if (const char *pszUserCachePath = NullifyIfEmpty(
229 : CPLGetConfigOption("GDAL_DEFAULT_WMS_CACHE_PATH", nullptr)))
230 : {
231 90 : m_osCachePath = pszUserCachePath;
232 : }
233 11 : else if (const char *pszXDG_CACHE_HOME =
234 11 : NullifyIfEmpty(CPLGetConfigOption("XDG_CACHE_HOME", nullptr)))
235 : {
236 : m_osCachePath =
237 1 : CPLFormFilename(pszXDG_CACHE_HOME, "gdalwmscache", nullptr);
238 : }
239 : else
240 : {
241 : #ifdef _WIN32
242 : const char *pszHome =
243 : NullifyIfEmpty(CPLGetConfigOption("USERPROFILE", nullptr));
244 : #else
245 : const char *pszHome =
246 10 : NullifyIfEmpty(CPLGetConfigOption("HOME", nullptr));
247 : #endif
248 10 : if (pszHome)
249 : {
250 : m_osCachePath =
251 : CPLFormFilename(CPLFormFilename(pszHome, ".cache", nullptr),
252 5 : "gdalwmscache", nullptr);
253 : }
254 : else
255 : {
256 : const char *pszDir =
257 5 : NullifyIfEmpty(CPLGetConfigOption("CPL_TMPDIR", nullptr));
258 :
259 5 : if (!pszDir)
260 3 : pszDir = NullifyIfEmpty(CPLGetConfigOption("TMPDIR", nullptr));
261 :
262 5 : if (!pszDir)
263 2 : pszDir = NullifyIfEmpty(CPLGetConfigOption("TEMP", nullptr));
264 :
265 5 : if (!pszDir)
266 1 : pszDir = ".";
267 :
268 : const char *pszUsername =
269 5 : NullifyIfEmpty(CPLGetConfigOption("USERNAME", nullptr));
270 5 : if (!pszUsername)
271 : pszUsername =
272 4 : NullifyIfEmpty(CPLGetConfigOption("USER", nullptr));
273 :
274 5 : if (pszUsername)
275 : {
276 : m_osCachePath = CPLFormFilename(
277 : pszDir, CPLSPrintf("gdalwmscache_%s", pszUsername),
278 4 : nullptr);
279 : }
280 : else
281 : {
282 : m_osCachePath = CPLFormFilename(
283 : pszDir,
284 : CPLSPrintf("gdalwmscache_%s",
285 : CPLMD5String(pszUrl ? pszUrl : "")),
286 1 : nullptr);
287 : }
288 : }
289 : }
290 :
291 : // Separate folder for each unique dataset url
292 213 : if (CPLTestBool(CPLGetXMLValue(pConfig, "Unique", "True")))
293 : {
294 : m_osCachePath = CPLFormFilename(
295 105 : m_osCachePath, CPLMD5String(pszUrl ? pszUrl : ""), nullptr);
296 : }
297 213 : CPLDebug("WMS", "Using %s for cache", m_osCachePath.c_str());
298 :
299 : // TODO: Add sqlite db cache type
300 213 : const char *pszType = CPLGetXMLValue(pConfig, "Type", "file");
301 213 : if (EQUAL(pszType, "file"))
302 : {
303 213 : m_poCache = new GDALWMSFileCache(m_osCachePath, pConfig);
304 : }
305 :
306 213 : return CE_None;
307 : }
308 :
309 18 : CPLErr GDALWMSCache::Insert(const char *pszKey, const CPLString &soFileName)
310 : {
311 18 : if (m_poCache != nullptr && pszKey != nullptr)
312 : {
313 : // Add file to cache
314 18 : CPLErr result = m_poCache->Insert(pszKey, soFileName);
315 18 : if (result == CE_None)
316 : {
317 : // Start clean thread
318 18 : int cleanThreadRunTimeout = m_poCache->GetCleanThreadRunTimeout();
319 36 : if (cleanThreadRunTimeout > 0 && !m_bIsCleanThreadRunning &&
320 18 : time(nullptr) - m_nCleanThreadLastRunTime >
321 18 : cleanThreadRunTimeout)
322 : {
323 12 : if (m_hThread)
324 0 : CPLJoinThread(m_hThread);
325 12 : m_bIsCleanThreadRunning = true;
326 12 : m_hThread = CPLCreateJoinableThread(CleanCacheThread, this);
327 : }
328 : }
329 18 : return result;
330 : }
331 :
332 0 : return CE_Failure;
333 : }
334 :
335 : enum GDALWMSCacheItemStatus
336 32 : GDALWMSCache::GetItemStatus(const char *pszKey) const
337 : {
338 32 : if (m_poCache != nullptr)
339 : {
340 32 : return m_poCache->GetItemStatus(pszKey);
341 : }
342 0 : return CACHE_ITEM_NOT_FOUND;
343 : }
344 :
345 14 : GDALDataset *GDALWMSCache::GetDataset(const char *pszKey,
346 : char **papszOpenOptions) const
347 : {
348 14 : if (m_poCache != nullptr)
349 : {
350 14 : return m_poCache->GetDataset(pszKey, papszOpenOptions);
351 : }
352 0 : return nullptr;
353 : }
354 :
355 12 : void GDALWMSCache::Clean()
356 : {
357 12 : if (m_poCache != nullptr)
358 : {
359 12 : CPLDebug("WMS", "Clean cache");
360 12 : m_poCache->Clean();
361 : }
362 :
363 12 : m_nCleanThreadLastRunTime = time(nullptr);
364 12 : m_bIsCleanThreadRunning = false;
365 12 : }
|