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