Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: GDAL
4 : * Purpose: Icechunk driver
5 : * Author: Even Rouault <even dot rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2026, Even Rouault <even dot rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "icechunkdrivercore.h"
14 :
15 : #include "cpl_json.h"
16 : #include "cpl_time.h"
17 :
18 : #include "gdalalgorithm.h"
19 : #include "gdal_frmts.h"
20 : #include "gdal_priv.h"
21 :
22 : #include "ogr_p.h"
23 :
24 : #include "icechunkrepo.h"
25 : #include "icechunksnapshot.h"
26 : #include "icechunkutils.h"
27 :
28 : #ifndef _
29 : #define _(x) (x)
30 : #endif
31 :
32 : namespace gdal::icechunk
33 : {
34 : /************************************************************************/
35 : /* DatasetOpen() */
36 : /************************************************************************/
37 :
38 85 : static GDALDataset *DatasetOpen(GDALOpenInfo *poOpenInfo)
39 : {
40 85 : if (!IcechunkDriverIdentify(poOpenInfo) || poOpenInfo->eAccess == GA_Update)
41 0 : return nullptr;
42 :
43 85 : std::unique_ptr<GDALOpenInfo> poTmpOpenInfo; // keep in that scope
44 170 : std::string osBranchName;
45 170 : std::string osTagName;
46 : std::string osFilename = GetFilenameFromDatasetName(
47 255 : poOpenInfo->pszFilename, osBranchName, osTagName);
48 85 : if (osFilename.empty())
49 1 : return nullptr; // Error emitted by GetFilenameFromDatasetName
50 84 : if (osFilename != poOpenInfo->pszFilename)
51 : {
52 : poTmpOpenInfo =
53 7 : std::make_unique<GDALOpenInfo>(osFilename.c_str(), GA_ReadOnly);
54 7 : poTmpOpenInfo->nOpenFlags = poOpenInfo->nOpenFlags;
55 7 : poOpenInfo = poTmpOpenInfo.get();
56 : }
57 :
58 84 : auto repo = IcechunkRepo::Open(poOpenInfo->pszFilename,
59 84 : poOpenInfo->bIsDirectory ? nullptr
60 168 : : poOpenInfo->fpL);
61 84 : if (!repo)
62 13 : return nullptr;
63 :
64 : class DummyDataset : public GDALDataset
65 : {
66 : public:
67 6 : DummyDataset()
68 6 : {
69 6 : nRasterXSize = 0;
70 6 : nRasterYSize = 0;
71 6 : }
72 :
73 1 : std::shared_ptr<GDALGroup> GetRootGroup() const override
74 : {
75 : class DummyGroup : public GDALGroup
76 : {
77 : public:
78 1 : DummyGroup() : GDALGroup(std::string(), "/")
79 : {
80 1 : }
81 : };
82 :
83 1 : return std::make_shared<DummyGroup>();
84 : }
85 : };
86 :
87 : const auto ConcatBranchOrTagNames =
88 3 : [](const std::map<std::string, std::string> &mapNameToSnapshotId)
89 : {
90 3 : std::string s;
91 6 : for (const auto &[name, _] : mapNameToSnapshotId)
92 : {
93 3 : if (!s.empty())
94 0 : s += ", ";
95 3 : s += '"';
96 3 : s += name;
97 3 : s += '"';
98 : }
99 3 : return s;
100 : };
101 :
102 71 : std::unique_ptr<IcechunkSnapshot> snapshot;
103 71 : if (osTagName.empty())
104 : {
105 69 : if (osBranchName.empty())
106 : {
107 67 : const auto branches = repo->GetBranches();
108 67 : if (branches.empty())
109 : {
110 1 : return std::make_unique<DummyDataset>().release();
111 : }
112 66 : else if (branches.find("main") != branches.end())
113 : {
114 65 : osBranchName = "main";
115 : }
116 : else
117 : {
118 1 : CPLError(CE_Failure, CPLE_AppDefined,
119 : "You need to specify a branch name among %s",
120 2 : ConcatBranchOrTagNames(repo->GetBranches()).c_str());
121 1 : return nullptr;
122 : }
123 : }
124 :
125 67 : const auto nErrorCount = CPLGetErrorCounter();
126 67 : snapshot = repo->OpenSnapshotOnBranch(osBranchName, false);
127 67 : if (!snapshot)
128 : {
129 22 : if (nErrorCount == CPLGetErrorCounter())
130 : {
131 1 : CPLError(
132 : CE_Failure, CPLE_AppDefined,
133 : "Invalid branch name \"%s\". Valid branch names are: %s",
134 : osBranchName.c_str(),
135 2 : ConcatBranchOrTagNames(repo->GetBranches()).c_str());
136 : }
137 22 : return nullptr;
138 : }
139 : }
140 : else
141 : {
142 2 : const auto nErrorCount = CPLGetErrorCounter();
143 2 : snapshot = repo->OpenSnapshotOnTag(osTagName, false);
144 2 : if (!snapshot)
145 : {
146 1 : if (nErrorCount == CPLGetErrorCounter())
147 : {
148 1 : CPLError(CE_Failure, CPLE_AppDefined,
149 : "Invalid tag name \"%s\". Valid tag names are: %s",
150 : osTagName.c_str(),
151 2 : ConcatBranchOrTagNames(repo->GetTags()).c_str());
152 : }
153 1 : return nullptr;
154 : }
155 : }
156 :
157 46 : if (snapshot->GetNodeCount() <= 1)
158 : {
159 5 : return std::make_unique<DummyDataset>().release();
160 : }
161 :
162 41 : auto poZarrDriver = GetGDALDriverManager()->GetDriverByName("ZARR");
163 41 : if (!poZarrDriver)
164 : {
165 0 : CPLError(CE_Failure, CPLE_AppDefined,
166 : "Cannot open Icechunk dataset due to missing Zarr driver");
167 0 : return nullptr;
168 : }
169 41 : const auto pfnOpen = poZarrDriver->GetOpenCallback();
170 41 : if (!pfnOpen)
171 : {
172 : // Cannot happen if using official GDAL Zarr driver!
173 0 : CPLError(CE_Failure, CPLE_AppDefined,
174 : "Cannot open Icechunk dataset due to missing Open() method in "
175 : "Zarr driver");
176 0 : return nullptr;
177 : }
178 :
179 : const std::string osVSIIcechunkFilename =
180 123 : std::string("ZARR:\"/vsiicechunk/{").append(osFilename).append("}\"");
181 82 : GDALOpenInfo oOpenInfoZarr(osVSIIcechunkFilename.c_str(), GA_ReadOnly);
182 41 : oOpenInfoZarr.nOpenFlags = poOpenInfo->nOpenFlags;
183 41 : oOpenInfoZarr.papszOpenOptions = poOpenInfo->papszOpenOptions;
184 : // cppcheck-suppress returnDanglingLifetime
185 41 : return pfnOpen(&oOpenInfoZarr);
186 : }
187 :
188 : /************************************************************************/
189 : /* ClearCaches() */
190 : /************************************************************************/
191 :
192 1006 : static void ClearCaches(GDALDriver *)
193 : {
194 1006 : gdal::icechunk::IcechunkRepo::ClearCaches();
195 1006 : VSIIcechunkFileSystemClearCaches();
196 1006 : }
197 :
198 : /************************************************************************/
199 : /* TimestampInMicrosecToISO8211() */
200 : /************************************************************************/
201 :
202 3 : static std::string TimestampInMicrosecToISO8211(uint64_t nTimestamp)
203 : {
204 : struct tm brokendown;
205 3 : constexpr int MICROSECONDS_IN_SEC = 1000 * 1000;
206 3 : CPLUnixTimeToYMDHMS(nTimestamp / MICROSECONDS_IN_SEC, &brokendown);
207 : OGRField sField;
208 3 : sField.Date.Year = static_cast<GInt16>(brokendown.tm_year + 1900);
209 3 : sField.Date.Month = static_cast<GByte>(brokendown.tm_mon + 1);
210 3 : sField.Date.Day = static_cast<GByte>(brokendown.tm_mday);
211 3 : sField.Date.Hour = static_cast<GByte>(brokendown.tm_hour);
212 3 : sField.Date.Minute = static_cast<GByte>(brokendown.tm_min);
213 3 : sField.Date.Second = static_cast<float>(
214 3 : brokendown.tm_sec + (nTimestamp % MICROSECONDS_IN_SEC) /
215 : static_cast<float>(MICROSECONDS_IN_SEC));
216 3 : sField.Date.TZFlag = OGR_TZFLAG_UTC;
217 : std::unique_ptr<char, VSIFreeReleaser> pszDateTime(
218 3 : OGRGetXMLDateTime(&sField, /* bAlwaysMillisecond = */ false));
219 6 : return pszDateTime.get();
220 : }
221 :
222 : /************************************************************************/
223 : /* ListRefsAlgorithm */
224 : /************************************************************************/
225 :
226 282 : class ListRefsAlgorithm /* non-final */ : public GDALAlgorithm
227 : {
228 : public:
229 : ~ListRefsAlgorithm() override;
230 :
231 : protected:
232 282 : ListRefsAlgorithm(const std::string &osName,
233 : const std::string &osDescription,
234 : const std::string &osHelpURL)
235 282 : : GDALAlgorithm(osName, osDescription, osHelpURL)
236 : {
237 282 : AddInputDatasetArg(&m_dataset, GDAL_OF_MULTIDIM_RASTER);
238 282 : AddOutputStringArg(&m_outputString);
239 282 : }
240 :
241 : GDALArgDatasetValue m_dataset{};
242 : std::string m_outputString{};
243 : };
244 :
245 : ListRefsAlgorithm::~ListRefsAlgorithm() = default;
246 :
247 : /************************************************************************/
248 : /* ListBranchesAlgorithm */
249 : /************************************************************************/
250 :
251 : class ListBranchesAlgorithm final : public ListRefsAlgorithm
252 : {
253 : public:
254 : static constexpr const char *NAME = LIST_BRANCHES;
255 :
256 141 : ListBranchesAlgorithm()
257 141 : : ListRefsAlgorithm(
258 282 : NAME, std::string("List branches of an Icechunk repository"),
259 423 : "/programs/gdal_driver_icechunk_list_branches.html")
260 : {
261 141 : }
262 :
263 : protected:
264 : bool RunImpl(GDALProgressFunc, void *) override;
265 : };
266 :
267 3 : bool ListBranchesAlgorithm::RunImpl(GDALProgressFunc, void *)
268 : {
269 6 : std::string osBranchName;
270 6 : std::string osTagName;
271 : const std::string osFilename = GetFilenameFromDatasetName(
272 6 : m_dataset.GetName(), osBranchName, osTagName);
273 6 : auto repo = IcechunkRepo::Open(osFilename.c_str());
274 3 : if (!repo)
275 1 : return false;
276 :
277 2 : CPLJSONArray oArray;
278 4 : for (const auto &[branchName, _] : repo->GetBranches())
279 : {
280 4 : CPLJSONObject oCommit;
281 2 : oCommit.Set("name", branchName);
282 4 : auto snapshot = repo->OpenSnapshotOnBranch(branchName);
283 2 : if (snapshot)
284 : {
285 2 : oCommit.Set("commit_message", snapshot->GetCommitMessage());
286 2 : if (const uint64_t nTimestamp = snapshot->GetFlushTimestamp())
287 : {
288 2 : oCommit.Set("timestamp",
289 4 : TimestampInMicrosecToISO8211(nTimestamp));
290 : }
291 : }
292 2 : oArray.Add(oCommit);
293 : }
294 2 : m_outputString = oArray.ToString();
295 2 : m_outputString += '\n';
296 :
297 2 : return true;
298 : }
299 :
300 : /************************************************************************/
301 : /* ListTagsAlgorithm */
302 : /************************************************************************/
303 :
304 : class ListTagsAlgorithm final : public ListRefsAlgorithm
305 : {
306 : public:
307 : static constexpr const char *NAME = LIST_TAGS;
308 :
309 141 : ListTagsAlgorithm()
310 141 : : ListRefsAlgorithm(NAME,
311 282 : std::string("List tags of an Icechunk repository"),
312 423 : "/programs/gdal_driver_icechunk_list_tags.html")
313 : {
314 141 : }
315 :
316 : protected:
317 : bool RunImpl(GDALProgressFunc, void *) override;
318 : };
319 :
320 3 : bool ListTagsAlgorithm::RunImpl(GDALProgressFunc, void *)
321 : {
322 6 : std::string osBranchName;
323 6 : std::string osTagName;
324 : const std::string osFilename = GetFilenameFromDatasetName(
325 6 : m_dataset.GetName(), osBranchName, osTagName);
326 6 : auto repo = IcechunkRepo::Open(osFilename.c_str());
327 3 : if (!repo)
328 1 : return false;
329 :
330 2 : CPLJSONArray oArray;
331 3 : for (const auto &[tagName, _] : repo->GetTags())
332 : {
333 2 : CPLJSONObject oCommit;
334 1 : oCommit.Set("name", tagName);
335 2 : auto snapshot = repo->OpenSnapshotOnTag(tagName);
336 1 : if (snapshot)
337 : {
338 1 : oCommit.Set("commit_message", snapshot->GetCommitMessage());
339 1 : if (const uint64_t nTimestamp = snapshot->GetFlushTimestamp())
340 : {
341 1 : oCommit.Set("timestamp",
342 2 : TimestampInMicrosecToISO8211(nTimestamp));
343 : }
344 : }
345 1 : oArray.Add(oCommit);
346 : }
347 2 : m_outputString = oArray.ToString();
348 2 : m_outputString += '\n';
349 :
350 2 : return true;
351 : }
352 :
353 : /************************************************************************/
354 : /* InstantiateAlgorithm() */
355 : /************************************************************************/
356 :
357 : static GDALAlgorithm *
358 282 : InstantiateAlgorithm(const std::vector<std::string> &aosPath)
359 : {
360 282 : if (aosPath.size() == 1 && aosPath[0] == ListBranchesAlgorithm::NAME)
361 : {
362 141 : return std::make_unique<ListBranchesAlgorithm>().release();
363 : }
364 141 : else if (aosPath.size() == 1 && aosPath[0] == ListTagsAlgorithm::NAME)
365 : {
366 141 : return std::make_unique<ListTagsAlgorithm>().release();
367 : }
368 : else
369 : {
370 0 : return nullptr;
371 : }
372 : }
373 :
374 : } // namespace gdal::icechunk
375 :
376 : /************************************************************************/
377 : /* GDALRegister_Icechunk() */
378 : /************************************************************************/
379 :
380 2135 : void GDALRegister_Icechunk()
381 :
382 : {
383 2135 : if (GDALGetDriverByName(DRIVER_NAME) != nullptr)
384 263 : return;
385 :
386 1872 : gdal::icechunk::VSIInstallIcechunkFileSystem();
387 :
388 3744 : auto poDriver = std::make_unique<GDALDriver>();
389 1872 : IcechunkDriverSetCommonMetadata(poDriver.get());
390 :
391 1872 : poDriver->pfnOpen = gdal::icechunk::DatasetOpen;
392 1872 : poDriver->pfnClearCaches = gdal::icechunk::ClearCaches;
393 1872 : poDriver->pfnInstantiateAlgorithm = gdal::icechunk::InstantiateAlgorithm;
394 :
395 1872 : GetGDALDriverManager()->RegisterDriver(poDriver.release());
396 : }
|