Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: Earth Engine Data API Images driver
4 : * Purpose: Earth Engine Data API Images driver
5 : * Author: Even Rouault, even dot rouault at spatialys.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2017-2018, Planet Labs
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "cpl_http.h"
14 : #include "cpl_multiproc.h" // CPLSleep
15 : #include "eeda.h"
16 : #include "ogrlibjsonutils.h"
17 :
18 : #include <stdlib.h>
19 : #include <limits>
20 :
21 : std::vector<EEDAIBandDesc>
22 10 : BuildBandDescArray(json_object *poBands,
23 : std::map<CPLString, CPLString> &oMapCodeToWKT)
24 : {
25 10 : const auto nBandCount = json_object_array_length(poBands);
26 10 : std::vector<EEDAIBandDesc> aoBandDesc;
27 :
28 34 : for (auto i = decltype(nBandCount){0}; i < nBandCount; i++)
29 : {
30 24 : json_object *poBand = json_object_array_get_idx(poBands, i);
31 48 : if (poBand == nullptr ||
32 24 : json_object_get_type(poBand) != json_type_object)
33 0 : continue;
34 :
35 24 : json_object *poId = CPL_json_object_object_get(poBand, "id");
36 24 : const char *pszBandId = json_object_get_string(poId);
37 24 : if (pszBandId == nullptr)
38 0 : continue;
39 :
40 : json_object *poDataType =
41 24 : CPL_json_object_object_get(poBand, "dataType");
42 48 : if (poDataType == nullptr ||
43 24 : json_object_get_type(poDataType) != json_type_object)
44 : {
45 0 : continue;
46 : }
47 :
48 : json_object *poPrecision =
49 24 : CPL_json_object_object_get(poDataType, "precision");
50 24 : const char *pszPrecision = json_object_get_string(poPrecision);
51 24 : if (pszPrecision == nullptr)
52 0 : continue;
53 24 : GDALDataType eDT = GDT_Byte;
54 24 : if (EQUAL(pszPrecision, "INT"))
55 : {
56 : json_object *poRange =
57 24 : CPL_json_object_object_get(poDataType, "range");
58 24 : if (poRange && json_object_get_type(poRange) == json_type_object)
59 : {
60 24 : int nMin = 0;
61 24 : int nMax = 0;
62 24 : json_object *poMin = CPL_json_object_object_get(poRange, "min");
63 24 : if (poMin)
64 : {
65 0 : nMin = json_object_get_int(poMin);
66 : }
67 24 : json_object *poMax = CPL_json_object_object_get(poRange, "max");
68 24 : if (poMax)
69 : {
70 24 : nMax = json_object_get_int(poMax);
71 : }
72 :
73 24 : if (nMin == -128 && nMax == 127)
74 : {
75 0 : eDT = GDT_Int8;
76 : }
77 24 : else if (nMin < std::numeric_limits<GInt16>::min())
78 : {
79 0 : eDT = GDT_Int32;
80 : }
81 24 : else if (nMax > std::numeric_limits<GUInt16>::max())
82 : {
83 0 : eDT = GDT_UInt32;
84 : }
85 24 : else if (nMin < 0)
86 : {
87 0 : eDT = GDT_Int16;
88 : }
89 24 : else if (nMax > std::numeric_limits<GByte>::max())
90 : {
91 19 : eDT = GDT_UInt16;
92 : }
93 : }
94 : }
95 0 : else if (EQUAL(pszPrecision, "FLOAT"))
96 : {
97 0 : eDT = GDT_Float32;
98 : }
99 0 : else if (EQUAL(pszPrecision, "DOUBLE"))
100 : {
101 0 : eDT = GDT_Float64;
102 : }
103 : else
104 : {
105 0 : CPLError(CE_Warning, CPLE_NotSupported,
106 : "Unhandled dataType %s for band %s", pszPrecision,
107 : pszBandId);
108 0 : continue;
109 : }
110 :
111 24 : json_object *poGrid = CPL_json_object_object_get(poBand, "grid");
112 48 : if (poGrid == nullptr ||
113 24 : json_object_get_type(poGrid) != json_type_object)
114 : {
115 0 : continue;
116 : }
117 :
118 24 : CPLString osWKT;
119 : // Cf https://developers.google.com/earth-engine/reference/rest/v1alpha/PixelGrid
120 24 : json_object *poCrs = CPL_json_object_object_get(poGrid, "crsCode");
121 24 : if (poCrs == nullptr)
122 0 : poCrs = CPL_json_object_object_get(poGrid, "crsWkt");
123 24 : if (poCrs ==
124 : nullptr) // "wkt" must come from a preliminary version of the API
125 0 : poCrs = CPL_json_object_object_get(poGrid, "wkt");
126 24 : OGRSpatialReference oSRS;
127 24 : if (poCrs)
128 : {
129 24 : const char *pszStr = json_object_get_string(poCrs);
130 24 : if (pszStr == nullptr)
131 0 : continue;
132 24 : if (STARTS_WITH(pszStr, "SR-ORG:"))
133 : {
134 : // For EEDA:MCD12Q1 for example
135 : pszStr =
136 0 : CPLSPrintf("http://spatialreference.org/ref/sr-org/%s/",
137 : pszStr + strlen("SR-ORG:"));
138 : }
139 :
140 : std::map<CPLString, CPLString>::const_iterator oIter =
141 24 : oMapCodeToWKT.find(pszStr);
142 24 : if (oIter != oMapCodeToWKT.end())
143 : {
144 15 : osWKT = oIter->second;
145 : }
146 9 : else if (oSRS.SetFromUserInput(pszStr) != OGRERR_NONE)
147 : {
148 0 : CPLError(CE_Warning, CPLE_AppDefined, "Unrecognized crs: %s",
149 : pszStr);
150 0 : oMapCodeToWKT[pszStr] = "";
151 : }
152 : else
153 : {
154 9 : char *pszWKT = nullptr;
155 9 : oSRS.exportToWkt(&pszWKT);
156 9 : if (pszWKT != nullptr)
157 9 : osWKT = pszWKT;
158 9 : CPLFree(pszWKT);
159 9 : oMapCodeToWKT[pszStr] = osWKT;
160 : }
161 : }
162 :
163 : json_object *poAT =
164 24 : CPL_json_object_object_get(poGrid, "affineTransform");
165 24 : if (poAT == nullptr || json_object_get_type(poAT) != json_type_object)
166 : {
167 0 : continue;
168 : }
169 : GDALGeoTransform gt{
170 : json_object_get_double(
171 24 : CPL_json_object_object_get(poAT, "translateX")),
172 48 : json_object_get_double(CPL_json_object_object_get(poAT, "scaleX")),
173 48 : json_object_get_double(CPL_json_object_object_get(poAT, "shearX")),
174 : json_object_get_double(
175 48 : CPL_json_object_object_get(poAT, "translateY")),
176 48 : json_object_get_double(CPL_json_object_object_get(poAT, "shearY")),
177 48 : json_object_get_double(CPL_json_object_object_get(poAT, "scaleY")),
178 24 : };
179 :
180 : json_object *poDimensions =
181 24 : CPL_json_object_object_get(poGrid, "dimensions");
182 48 : if (poDimensions == nullptr ||
183 24 : json_object_get_type(poDimensions) != json_type_object)
184 : {
185 0 : continue;
186 : }
187 : json_object *poWidth =
188 24 : CPL_json_object_object_get(poDimensions, "width");
189 24 : int nWidth = json_object_get_int(poWidth);
190 : json_object *poHeight =
191 24 : CPL_json_object_object_get(poDimensions, "height");
192 24 : int nHeight = json_object_get_int(poHeight);
193 :
194 : #if 0
195 : if( poWidth == nullptr && poHeight == nullptr && poX == nullptr && poY == nullptr &&
196 : dfResX == 1.0 && dfResY == 1.0 )
197 : {
198 : // e.g. EEDAI:LT5_L1T_8DAY_EVI/19840109
199 : const char* pszAuthorityName = oSRS.GetAuthorityName(nullptr);
200 : const char* pszAuthorityCode = oSRS.GetAuthorityCode(nullptr);
201 : if( pszAuthorityName && pszAuthorityCode &&
202 : EQUAL(pszAuthorityName, "EPSG") &&
203 : EQUAL(pszAuthorityCode, "4326") )
204 : {
205 : dfX = -180;
206 : dfY = 90;
207 : nWidth = 1 << 30;
208 : nHeight = 1 << 29;
209 : dfResX = 360.0 / nWidth;
210 : dfResY = -dfResX;
211 : }
212 : }
213 : #endif
214 :
215 24 : if (nWidth <= 0 || nHeight <= 0)
216 : {
217 0 : CPLError(CE_Warning, CPLE_AppDefined,
218 : "Invalid width/height for band %s", pszBandId);
219 0 : continue;
220 : }
221 :
222 48 : EEDAIBandDesc oDesc;
223 24 : oDesc.osName = pszBandId;
224 24 : oDesc.osWKT = std::move(osWKT);
225 24 : oDesc.eDT = eDT;
226 24 : oDesc.gt = std::move(gt);
227 24 : oDesc.nWidth = nWidth;
228 24 : oDesc.nHeight = nHeight;
229 24 : aoBandDesc.emplace_back(std::move(oDesc));
230 : }
231 10 : return aoBandDesc;
232 : }
233 :
234 : /************************************************************************/
235 : /* GDALEEDABaseDataset() */
236 : /************************************************************************/
237 :
238 43 : GDALEEDABaseDataset::GDALEEDABaseDataset()
239 43 : : m_bMustCleanPersistent(false), m_nExpirationTime(0)
240 : {
241 43 : }
242 :
243 : /************************************************************************/
244 : /* ~GDALEEDABaseDataset() */
245 : /************************************************************************/
246 :
247 43 : GDALEEDABaseDataset::~GDALEEDABaseDataset()
248 : {
249 43 : if (m_bMustCleanPersistent)
250 : {
251 15 : char **papszOptions = CSLSetNameValue(nullptr, "CLOSE_PERSISTENT",
252 : CPLSPrintf("EEDAI:%p", this));
253 15 : CPLHTTPDestroyResult(CPLHTTPFetch(m_osBaseURL, papszOptions));
254 15 : CSLDestroy(papszOptions);
255 : }
256 43 : }
257 :
258 : /************************************************************************/
259 : /* ConvertPathToName() */
260 : /************************************************************************/
261 :
262 16 : CPLString GDALEEDABaseDataset::ConvertPathToName(const CPLString &path)
263 : {
264 16 : size_t end = path.find('/');
265 32 : CPLString folder = path.substr(0, end);
266 :
267 16 : if (folder == "users")
268 : {
269 2 : return "projects/earthengine-legacy/assets/" + path;
270 : }
271 15 : else if (folder != "projects")
272 : {
273 24 : return "projects/earthengine-public/assets/" + path;
274 : }
275 :
276 : // Find the start and end positions of the third segment, if it exists.
277 3 : int segment = 1;
278 3 : size_t start = 0;
279 8 : while (end != std::string::npos && segment < 3)
280 : {
281 5 : segment++;
282 5 : start = end + 1;
283 5 : end = path.find('/', start);
284 : }
285 :
286 3 : end = (end == std::string::npos) ? path.size() : end;
287 : // segment is 3 if path has at least 3 segments.
288 3 : if (folder == "projects" && segment == 3)
289 : {
290 : // If the first segment is "projects" and the third segment is "assets",
291 : // path is a name, so return as-is.
292 2 : if (path.substr(start, end - start) == "assets")
293 : {
294 1 : return path;
295 : }
296 : }
297 4 : return "projects/earthengine-legacy/assets/" + path;
298 : }
299 :
300 : /************************************************************************/
301 : /* GetBaseHTTPOptions() */
302 : /************************************************************************/
303 :
304 27 : char **GDALEEDABaseDataset::GetBaseHTTPOptions()
305 : {
306 27 : m_bMustCleanPersistent = true;
307 :
308 27 : char **papszOptions = nullptr;
309 : papszOptions =
310 27 : CSLAddString(papszOptions, CPLSPrintf("PERSISTENT=EEDAI:%p", this));
311 :
312 : // Strategy to get the Bearer Authorization value:
313 : // - if it is specified in the EEDA_BEARER config option, use it
314 : // - otherwise if EEDA_BEARER_FILE is specified, read it and use its content
315 : // - otherwise if GOOGLE_APPLICATION_CREDENTIALS is specified, read the
316 : // corresponding file to get the private key and client_email, to get a
317 : // bearer using OAuth2ServiceAccount method
318 : // - otherwise if EEDA_PRIVATE_KEY and EEDA_CLIENT_EMAIL are set, use them
319 : // to get a bearer using OAuth2ServiceAccount method
320 : // - otherwise if EEDA_PRIVATE_KEY_FILE and EEDA_CLIENT_EMAIL are set, use
321 : // them to get a bearer
322 :
323 54 : CPLString osBearer(CPLGetConfigOption("EEDA_BEARER", m_osBearer));
324 50 : if (osBearer.empty() ||
325 23 : (!m_osBearer.empty() && time(nullptr) > m_nExpirationTime))
326 : {
327 4 : CPLString osBearerFile(CPLGetConfigOption("EEDA_BEARER_FILE", ""));
328 4 : if (!osBearerFile.empty())
329 : {
330 0 : VSILFILE *fp = VSIFOpenL(osBearerFile, "rb");
331 0 : if (fp == nullptr)
332 : {
333 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
334 : osBearerFile.c_str());
335 : }
336 : else
337 : {
338 : char abyBuffer[512];
339 0 : size_t nRead = VSIFReadL(abyBuffer, 1, sizeof(abyBuffer), fp);
340 0 : osBearer.assign(abyBuffer, nRead);
341 0 : VSIFCloseL(fp);
342 : }
343 : }
344 : else
345 : {
346 4 : CPLString osPrivateKey(CPLGetConfigOption("EEDA_PRIVATE_KEY", ""));
347 : CPLString osClientEmail(
348 4 : CPLGetConfigOption("EEDA_CLIENT_EMAIL", ""));
349 :
350 4 : if (osPrivateKey.empty())
351 : {
352 : CPLString osPrivateKeyFile(
353 6 : CPLGetConfigOption("EEDA_PRIVATE_KEY_FILE", ""));
354 3 : if (!osPrivateKeyFile.empty())
355 : {
356 0 : VSILFILE *fp = VSIFOpenL(osPrivateKeyFile, "rb");
357 0 : if (fp == nullptr)
358 : {
359 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
360 : osPrivateKeyFile.c_str());
361 : }
362 : else
363 : {
364 : char *pabyBuffer =
365 0 : static_cast<char *>(CPLMalloc(32768));
366 0 : size_t nRead = VSIFReadL(pabyBuffer, 1, 32768, fp);
367 0 : osPrivateKey.assign(pabyBuffer, nRead);
368 0 : VSIFCloseL(fp);
369 0 : CPLFree(pabyBuffer);
370 : }
371 : }
372 : }
373 :
374 4 : CPLString osServiceAccountJson;
375 : const char *pszVSIPath =
376 4 : CSLFetchNameValue(papszOpenOptions, "VSI_PATH_FOR_AUTH");
377 4 : if (pszVSIPath)
378 : osServiceAccountJson = VSIGetPathSpecificOption(
379 0 : pszVSIPath, "GOOGLE_APPLICATION_CREDENTIALS", "");
380 4 : if (osServiceAccountJson.empty())
381 : osServiceAccountJson =
382 4 : CPLGetConfigOption("GOOGLE_APPLICATION_CREDENTIALS", "");
383 4 : if (!osServiceAccountJson.empty())
384 : {
385 1 : CPLJSONDocument oDoc;
386 1 : if (!oDoc.Load(osServiceAccountJson))
387 : {
388 0 : CSLDestroy(papszOptions);
389 0 : return nullptr;
390 : }
391 :
392 1 : osPrivateKey = oDoc.GetRoot().GetString("private_key");
393 1 : osPrivateKey.replaceAll("\\n", "\n");
394 1 : osClientEmail = oDoc.GetRoot().GetString("client_email");
395 : }
396 :
397 4 : char **papszMD = nullptr;
398 4 : if (!osPrivateKey.empty() && !osClientEmail.empty())
399 : {
400 2 : CPLDebug("EEDA", "Requesting Bearer token");
401 2 : osPrivateKey.replaceAll("\\n", "\n");
402 : // CPLDebug("EEDA", "Private key: %s", osPrivateKey.c_str());
403 2 : papszMD = GOA2GetAccessTokenFromServiceAccount(
404 : osPrivateKey, osClientEmail,
405 : "https://www.googleapis.com/auth/earthengine.readonly",
406 : nullptr, nullptr);
407 2 : if (papszMD == nullptr)
408 : {
409 0 : CSLDestroy(papszOptions);
410 0 : return nullptr;
411 : }
412 : }
413 : // Some Travis-CI workers are GCE machines, and for some tests, we
414 : // don't want this code path to be taken. And on AppVeyor/Window, we
415 : // would also attempt a network access
416 4 : else if (!CPLTestBool(CPLGetConfigOption("CPL_GCE_SKIP", "NO")) &&
417 2 : CPLIsMachinePotentiallyGCEInstance())
418 : {
419 1 : papszMD = GOA2GetAccessTokenFromCloudEngineVM(nullptr);
420 : }
421 :
422 4 : if (papszMD)
423 : {
424 3 : osBearer = CSLFetchNameValueDef(papszMD, "access_token", "");
425 3 : m_osBearer = osBearer;
426 3 : m_nExpirationTime = CPLAtoGIntBig(
427 : CSLFetchNameValueDef(papszMD, "expires_in", "0"));
428 3 : if (m_nExpirationTime != 0)
429 3 : m_nExpirationTime += time(nullptr) - 10;
430 3 : CSLDestroy(papszMD);
431 : }
432 : else
433 : {
434 1 : CPLError(CE_Failure, CPLE_AppDefined,
435 : "Missing EEDA_BEARER, EEDA_BEARER_FILE or "
436 : "GOOGLE_APPLICATION_CREDENTIALS or "
437 : "EEDA_PRIVATE_KEY/EEDA_PRIVATE_KEY_FILE + "
438 : "EEDA_CLIENT_EMAIL config option or "
439 : "VSI_PATH_FOR_AUTH open option");
440 1 : CSLDestroy(papszOptions);
441 1 : return nullptr;
442 : }
443 : }
444 : }
445 26 : papszOptions = CSLAddString(
446 : papszOptions,
447 : CPLSPrintf("HEADERS=Authorization: Bearer %s", osBearer.c_str()));
448 :
449 26 : return papszOptions;
450 : }
451 :
452 : /* Add a small amount of random jitter to avoid cyclic server stampedes */
453 0 : static double EEDABackoffFactor(double base)
454 : {
455 : // We don't need cryptographic quality randomness...
456 : return base
457 : #ifndef __COVERITY__
458 0 : + rand() * 0.5 / RAND_MAX
459 : #endif
460 : ;
461 : }
462 :
463 : /************************************************************************/
464 : /* EEDAHTTPFetch() */
465 : /************************************************************************/
466 :
467 26 : CPLHTTPResult *EEDAHTTPFetch(const char *pszURL, char **papszOptions)
468 : {
469 : CPLHTTPResult *psResult;
470 26 : const int RETRY_COUNT = 4;
471 26 : double dfRetryDelay = 1.0;
472 26 : for (int i = 0; i <= RETRY_COUNT; i++)
473 : {
474 26 : psResult = CPLHTTPFetch(pszURL, papszOptions);
475 :
476 26 : if (psResult == nullptr)
477 0 : break;
478 26 : if (psResult->nDataLen != 0 && psResult->nStatus == 0 &&
479 26 : psResult->pszErrBuf == nullptr)
480 : {
481 : /* got a valid response */
482 26 : CPLErrorReset();
483 26 : break;
484 : }
485 : else
486 : {
487 0 : const char *pszErrorText =
488 0 : psResult->pszErrBuf ? psResult->pszErrBuf : "(null)";
489 :
490 : /* Get HTTP status code */
491 0 : int nHTTPStatus = -1;
492 0 : if (psResult->pszErrBuf != nullptr &&
493 0 : EQUALN(psResult->pszErrBuf,
494 : "HTTP error code : ", strlen("HTTP error code : ")))
495 : {
496 : nHTTPStatus =
497 0 : atoi(psResult->pszErrBuf + strlen("HTTP error code : "));
498 0 : if (psResult->pabyData)
499 0 : pszErrorText =
500 : reinterpret_cast<const char *>(psResult->pabyData);
501 : }
502 :
503 0 : if ((nHTTPStatus == 429 || nHTTPStatus == 500 ||
504 0 : (nHTTPStatus >= 502 && nHTTPStatus <= 504)) &&
505 : i < RETRY_COUNT)
506 : {
507 0 : CPLError(CE_Warning, CPLE_FileIO,
508 : "GET error when downloading %s, HTTP status=%d, "
509 : "retrying in %.2fs : %s",
510 : pszURL, nHTTPStatus, dfRetryDelay, pszErrorText);
511 0 : CPLHTTPDestroyResult(psResult);
512 0 : psResult = nullptr;
513 :
514 0 : CPLSleep(dfRetryDelay);
515 0 : dfRetryDelay *= EEDABackoffFactor(4);
516 : }
517 : else
518 : {
519 : break;
520 : }
521 : }
522 : }
523 :
524 26 : return psResult;
525 : }
|