LCOV - code coverage report
Current view: top level - frmts/wms - gdalwmscache.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 145 158 91.8 %
Date: 2025-07-09 17:50:03 Functions: 17 17 100.0 %

          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 : }

Generated by: LCOV version 1.14