Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: CPL - Common Portability Library
4 : * Purpose: Implement VSI large file api for Google Cloud Storage
5 : * Author: Even Rouault, even.rouault at spatialys.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2010-2018, Even Rouault <even.rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "cpl_port.h"
14 : #include "cpl_http.h"
15 : #include "cpl_minixml.h"
16 : #include "cpl_json.h"
17 : #include "cpl_vsil_curl_priv.h"
18 : #include "cpl_vsil_curl_class.h"
19 :
20 : #include <errno.h>
21 :
22 : #include <algorithm>
23 : #include <set>
24 : #include <map>
25 : #include <memory>
26 :
27 : #include "cpl_google_cloud.h"
28 :
29 : // To avoid aliasing to GetDiskFreeSpace to GetDiskFreeSpaceA on Windows
30 : #ifdef GetDiskFreeSpace
31 : #undef GetDiskFreeSpace
32 : #endif
33 :
34 : #ifndef HAVE_CURL
35 :
36 : void VSIInstallGSFileHandler(void)
37 : {
38 : // Not supported.
39 : }
40 :
41 : #else
42 :
43 : //! @cond Doxygen_Suppress
44 : #ifndef DOXYGEN_SKIP
45 :
46 : #define ENABLE_DEBUG 0
47 :
48 : #define unchecked_curl_easy_setopt(handle, opt, param) \
49 : CPL_IGNORE_RET_VAL(curl_easy_setopt(handle, opt, param))
50 :
51 : namespace cpl
52 : {
53 :
54 : /************************************************************************/
55 : /* VSIGSFSHandler */
56 : /************************************************************************/
57 :
58 : class VSIGSFSHandler final : public IVSIS3LikeFSHandlerWithMultipartUpload
59 : {
60 : CPL_DISALLOW_COPY_ASSIGN(VSIGSFSHandler)
61 : const std::string m_osPrefix;
62 :
63 : protected:
64 : VSICurlHandle *CreateFileHandle(const char *pszFilename) override;
65 :
66 57 : const char *GetDebugKey() const override
67 : {
68 57 : return "GS";
69 : }
70 :
71 18681 : std::string GetFSPrefix() const override
72 : {
73 18681 : return m_osPrefix;
74 : }
75 :
76 : std::string
77 : GetURLFromFilename(const std::string &osFilename) const override;
78 :
79 : IVSIS3LikeHandleHelper *CreateHandleHelper(const char *pszURI,
80 : bool bAllowNoObject) override;
81 :
82 : void ClearCache() override;
83 :
84 0 : bool IsAllowedHeaderForObjectCreation(const char *pszHeaderName) override
85 : {
86 0 : return STARTS_WITH(pszHeaderName, "x-goog-");
87 : }
88 :
89 : VSIVirtualHandleUniquePtr
90 : CreateWriteHandle(const char *pszFilename,
91 : CSLConstList papszOptions) override;
92 :
93 0 : GIntBig GetDiskFreeSpace(const char * /* pszDirname */) override
94 : {
95 : // There is no limit per bucket, but a 5 TiB limit per object.
96 0 : return static_cast<GIntBig>(5) * 1024 * 1024 * 1024 * 1024;
97 : }
98 :
99 : public:
100 1808 : explicit VSIGSFSHandler(const char *pszPrefix) : m_osPrefix(pszPrefix)
101 : {
102 1808 : }
103 :
104 : ~VSIGSFSHandler() override;
105 :
106 : const char *GetOptions() override;
107 :
108 : char *GetSignedURL(const char *pszFilename,
109 : CSLConstList papszOptions) override;
110 :
111 : char **GetFileMetadata(const char *pszFilename, const char *pszDomain,
112 : CSLConstList papszOptions) override;
113 :
114 : bool SetFileMetadata(const char *pszFilename, CSLConstList papszMetadata,
115 : const char *pszDomain,
116 : CSLConstList papszOptions) override;
117 :
118 : int *UnlinkBatch(CSLConstList papszFiles) override;
119 : int RmdirRecursive(const char *pszDirname) override;
120 :
121 : std::string
122 : GetStreamingFilename(const std::string &osFilename) const override;
123 :
124 0 : VSIFilesystemHandler *Duplicate(const char *pszPrefix) override
125 : {
126 0 : return new VSIGSFSHandler(pszPrefix);
127 : }
128 :
129 1 : bool SupportsMultipartAbort() const override
130 : {
131 1 : return true;
132 : }
133 :
134 : std::string
135 5978 : GetHintForPotentiallyRecognizedPath(const std::string &osPath) override
136 : {
137 11952 : if (!cpl::starts_with(osPath, m_osPrefix) &&
138 11952 : !cpl::starts_with(osPath, GetStreamingFilename(m_osPrefix)))
139 : {
140 17916 : for (const char *pszPrefix : {"gs://", "gcs://"})
141 : {
142 11944 : if (cpl::starts_with(osPath, pszPrefix))
143 : {
144 2 : return GetFSPrefix() + osPath.substr(strlen(pszPrefix));
145 : }
146 : }
147 : }
148 5979 : return std::string();
149 : }
150 : };
151 :
152 : /************************************************************************/
153 : /* VSIGSHandle */
154 : /************************************************************************/
155 :
156 : class VSIGSHandle final : public IVSIS3LikeHandle
157 : {
158 : CPL_DISALLOW_COPY_ASSIGN(VSIGSHandle)
159 :
160 : VSIGSHandleHelper *m_poHandleHelper = nullptr;
161 :
162 : protected:
163 : struct curl_slist *GetCurlHeaders(const std::string &osVerb,
164 : struct curl_slist *psHeaders) override;
165 :
166 : public:
167 : VSIGSHandle(VSIGSFSHandler *poFS, const char *pszFilename,
168 : VSIGSHandleHelper *poHandleHelper);
169 : ~VSIGSHandle() override;
170 : };
171 :
172 : /************************************************************************/
173 : /* ~VSIGSFSHandler() */
174 : /************************************************************************/
175 :
176 1131 : VSIGSFSHandler::~VSIGSFSHandler()
177 : {
178 1131 : VSICurlFilesystemHandlerBase::ClearCache();
179 1131 : }
180 :
181 : /************************************************************************/
182 : /* ClearCache() */
183 : /************************************************************************/
184 :
185 367 : void VSIGSFSHandler::ClearCache()
186 : {
187 367 : VSICurlFilesystemHandlerBase::ClearCache();
188 :
189 367 : VSIGSHandleHelper::ClearCache();
190 367 : }
191 :
192 : /************************************************************************/
193 : /* CreateFileHandle() */
194 : /************************************************************************/
195 :
196 43 : VSICurlHandle *VSIGSFSHandler::CreateFileHandle(const char *pszFilename)
197 : {
198 86 : VSIGSHandleHelper *poHandleHelper = VSIGSHandleHelper::BuildFromURI(
199 129 : pszFilename + GetFSPrefix().size(), GetFSPrefix().c_str());
200 43 : if (poHandleHelper == nullptr)
201 9 : return nullptr;
202 34 : return new VSIGSHandle(this, pszFilename, poHandleHelper);
203 : }
204 :
205 : /************************************************************************/
206 : /* GetOptions() */
207 : /************************************************************************/
208 :
209 2 : const char *VSIGSFSHandler::GetOptions()
210 : {
211 : static std::string osOptions(
212 2 : std::string("<Options>")
213 : .append(
214 : " <Option name='GS_SECRET_ACCESS_KEY' type='string' "
215 : "description='Secret access key. To use with "
216 : "GS_ACCESS_KEY_ID'/>"
217 : " <Option name='GS_ACCESS_KEY_ID' type='string' "
218 : "description='Access key id'/>"
219 : " <Option name='GS_NO_SIGN_REQUEST' type='boolean' "
220 : "description='Whether to disable signing of requests' "
221 : "default='NO'/>"
222 : " <Option name='GS_OAUTH2_REFRESH_TOKEN' type='string' "
223 : "description='OAuth2 refresh token. For OAuth2 client "
224 : "authentication. "
225 : "To use with GS_OAUTH2_CLIENT_ID and GS_OAUTH2_CLIENT_SECRET'/>"
226 : " <Option name='GS_OAUTH2_CLIENT_ID' type='string' "
227 : "description='OAuth2 client id for OAuth2 client "
228 : "authentication'/>"
229 : " <Option name='GS_OAUTH2_CLIENT_SECRET' type='string' "
230 : "description='OAuth2 client secret for OAuth2 client "
231 : "authentication'/>"
232 : " <Option name='GS_OAUTH2_PRIVATE_KEY' type='string' "
233 : "description='Private key for OAuth2 service account "
234 : "authentication. "
235 : "To use with GS_OAUTH2_CLIENT_EMAIL'/>"
236 : " <Option name='GS_OAUTH2_PRIVATE_KEY_FILE' type='string' "
237 : "description='Filename that contains private key for OAuth2 "
238 : "service "
239 : "account authentication. "
240 : "To use with GS_OAUTH2_CLIENT_EMAIL'/>"
241 : " <Option name='GS_OAUTH2_CLIENT_EMAIL' type='string' "
242 : "description='Client email to use with OAuth2 service account "
243 : "authentication'/>"
244 : " <Option name='GS_OAUTH2_SCOPE' type='string' "
245 : "description='OAuth2 authorization scope' "
246 : "default='https://www.googleapis.com/auth/"
247 : "devstorage.read_write'/>"
248 : " <Option name='CPL_MACHINE_IS_GCE' type='boolean' "
249 : "description='Whether the current machine is a Google Compute "
250 : "Engine "
251 : "instance' default='NO'/>"
252 : " <Option name='CPL_GCE_CHECK_LOCAL_FILES' type='boolean' "
253 : "description='Whether to check system logs to determine "
254 : "if current machine is a GCE instance' default='YES'/>"
255 : "description='Filename that contains AWS configuration' "
256 : "default='~/.aws/config'/>"
257 : " <Option name='CPL_GS_CREDENTIALS_FILE' type='string' "
258 : "description='Filename that contains Google Storage "
259 : "credentials' "
260 : "default='~/.boto'/>"
261 : " <Option name='VSIGS_CHUNK_SIZE' type='int' "
262 : "description='Size in MiB for chunks of files that are "
263 : "uploaded. The"
264 1 : "default value allows for files up to ")
265 1 : .append(CPLSPrintf("%d", GetDefaultPartSizeInMiB() *
266 1 : GetMaximumPartCount() / 1024))
267 1 : .append("GiB each' default='")
268 1 : .append(CPLSPrintf("%d", GetDefaultPartSizeInMiB()))
269 1 : .append("' min='")
270 1 : .append(CPLSPrintf("%d", GetMinimumPartSizeInMiB()))
271 1 : .append("' max='")
272 1 : .append(CPLSPrintf("%d", GetMaximumPartSizeInMiB()))
273 1 : .append("'/>")
274 1 : .append(VSICurlFilesystemHandlerBase::GetOptionsStatic())
275 3 : .append("</Options>"));
276 2 : return osOptions.c_str();
277 : }
278 :
279 : /************************************************************************/
280 : /* GetSignedURL() */
281 : /************************************************************************/
282 :
283 6 : char *VSIGSFSHandler::GetSignedURL(const char *pszFilename,
284 : CSLConstList papszOptions)
285 : {
286 6 : if (!STARTS_WITH_CI(pszFilename, GetFSPrefix().c_str()))
287 0 : return nullptr;
288 :
289 12 : VSIGSHandleHelper *poHandleHelper = VSIGSHandleHelper::BuildFromURI(
290 18 : pszFilename + GetFSPrefix().size(), GetFSPrefix().c_str(), nullptr,
291 : papszOptions);
292 6 : if (poHandleHelper == nullptr)
293 : {
294 1 : return nullptr;
295 : }
296 :
297 5 : std::string osRet(poHandleHelper->GetSignedURL(papszOptions));
298 :
299 5 : delete poHandleHelper;
300 5 : return osRet.empty() ? nullptr : CPLStrdup(osRet.c_str());
301 : }
302 :
303 : /************************************************************************/
304 : /* GetURLFromFilename() */
305 : /************************************************************************/
306 :
307 : std::string
308 14 : VSIGSFSHandler::GetURLFromFilename(const std::string &osFilename) const
309 : {
310 : const std::string osFilenameWithoutPrefix =
311 28 : osFilename.substr(GetFSPrefix().size());
312 : auto poHandleHelper =
313 : std::unique_ptr<VSIGSHandleHelper>(VSIGSHandleHelper::BuildFromURI(
314 28 : osFilenameWithoutPrefix.c_str(), GetFSPrefix().c_str()));
315 14 : if (poHandleHelper == nullptr)
316 4 : return std::string();
317 10 : return poHandleHelper->GetURL();
318 : }
319 :
320 : /************************************************************************/
321 : /* CreateHandleHelper() */
322 : /************************************************************************/
323 :
324 15 : IVSIS3LikeHandleHelper *VSIGSFSHandler::CreateHandleHelper(const char *pszURI,
325 : bool)
326 : {
327 15 : return VSIGSHandleHelper::BuildFromURI(pszURI, GetFSPrefix().c_str());
328 : }
329 :
330 : /************************************************************************/
331 : /* CreateWriteHandle() */
332 : /************************************************************************/
333 :
334 : VSIVirtualHandleUniquePtr
335 1 : VSIGSFSHandler::CreateWriteHandle(const char *pszFilename,
336 : CSLConstList papszOptions)
337 : {
338 : auto poHandleHelper =
339 1 : CreateHandleHelper(pszFilename + GetFSPrefix().size(), false);
340 1 : if (poHandleHelper == nullptr)
341 0 : return nullptr;
342 : auto poHandle = std::make_unique<VSIMultipartWriteHandle>(
343 2 : this, pszFilename, poHandleHelper, papszOptions);
344 1 : if (!poHandle->IsOK())
345 : {
346 0 : return nullptr;
347 : }
348 1 : return VSIVirtualHandleUniquePtr(poHandle.release());
349 : }
350 :
351 : /************************************************************************/
352 : /* GetFileMetadata() */
353 : /************************************************************************/
354 :
355 7 : char **VSIGSFSHandler::GetFileMetadata(const char *pszFilename,
356 : const char *pszDomain,
357 : CSLConstList papszOptions)
358 : {
359 7 : if (!STARTS_WITH_CI(pszFilename, GetFSPrefix().c_str()))
360 0 : return nullptr;
361 :
362 7 : if (pszDomain == nullptr)
363 : {
364 : // Handle case of requesting GetFileMetadata() on the bucket root
365 3 : std::string osFilename(pszFilename);
366 3 : if (osFilename.back() == '/')
367 3 : osFilename.pop_back();
368 3 : if (osFilename.find('/', GetFSPrefix().size()) == std::string::npos)
369 : {
370 : const std::string osBucket =
371 6 : osFilename.substr(GetFSPrefix().size());
372 : const std::string osResource =
373 9 : std::string("storage/v1/b/").append(osBucket);
374 :
375 : auto poHandleHelper = std::unique_ptr<VSIGSHandleHelper>(
376 : VSIGSHandleHelper::BuildFromURI(osResource.c_str(),
377 3 : GetFSPrefix().c_str(),
378 9 : osBucket.c_str()));
379 3 : if (!poHandleHelper)
380 0 : return nullptr;
381 :
382 : // Check if OAuth2 is used externally and a bearer token is passed
383 : // as a header in path-specific options
384 : const CPLStringList aosHTTPOptions(
385 6 : CPLHTTPGetOptionsFromEnv(pszFilename));
386 3 : bool bUsingBearerToken = false;
387 3 : const char *pszHeaders = aosHTTPOptions.FetchNameValue("HEADERS");
388 3 : if (pszHeaders && strstr(pszHeaders, "Authorization: Bearer "))
389 1 : bUsingBearerToken = true;
390 :
391 : // The JSON API cannot be used with HMAC keys
392 3 : if (poHandleHelper->UsesHMACKey() && !bUsingBearerToken)
393 : {
394 1 : CPLDebug(GetDebugKey(),
395 : "GetFileMetadata() on bucket "
396 : "only available for OAuth2 authentication");
397 1 : return VSICurlFilesystemHandlerBase::GetFileMetadata(
398 1 : pszFilename, pszDomain, papszOptions);
399 : }
400 :
401 4 : NetworkStatisticsFileSystem oContextFS(GetFSPrefix().c_str());
402 4 : NetworkStatisticsAction oContextAction("GetFileMetadata");
403 :
404 4 : const CPLHTTPRetryParameters oRetryParameters(aosHTTPOptions);
405 4 : CPLHTTPRetryContext oRetryContext(oRetryParameters);
406 :
407 : bool bRetry;
408 4 : CPLStringList aosResult;
409 2 : do
410 : {
411 2 : bRetry = false;
412 2 : CURL *hCurlHandle = curl_easy_init();
413 :
414 : struct curl_slist *headers =
415 4 : static_cast<struct curl_slist *>(CPLHTTPSetOptions(
416 2 : hCurlHandle, poHandleHelper->GetURL().c_str(),
417 : aosHTTPOptions.List()));
418 2 : headers = poHandleHelper->GetCurlHeaders("GET", headers);
419 :
420 4 : CurlRequestHelper requestHelper;
421 2 : const long response_code = requestHelper.perform(
422 2 : hCurlHandle, headers, this, poHandleHelper.get());
423 :
424 2 : NetworkStatisticsLogger::LogGET(
425 : requestHelper.sWriteFuncData.nSize);
426 :
427 2 : if (response_code != 200 ||
428 2 : requestHelper.sWriteFuncData.pBuffer == nullptr)
429 : {
430 : // Look if we should attempt a retry
431 0 : if (oRetryContext.CanRetry(
432 : static_cast<int>(response_code),
433 0 : requestHelper.sWriteFuncHeaderData.pBuffer,
434 : requestHelper.szCurlErrBuf))
435 : {
436 0 : CPLError(CE_Warning, CPLE_AppDefined,
437 : "HTTP error code: %d - %s. "
438 : "Retrying again in %.1f secs",
439 : static_cast<int>(response_code),
440 0 : poHandleHelper->GetURL().c_str(),
441 : oRetryContext.GetCurrentDelay());
442 0 : CPLSleep(oRetryContext.GetCurrentDelay());
443 0 : bRetry = true;
444 : }
445 : else
446 : {
447 0 : CPLDebug(GetDebugKey(), "%s",
448 0 : requestHelper.sWriteFuncData.pBuffer
449 : ? requestHelper.sWriteFuncData.pBuffer
450 : : "(null)");
451 0 : CPLError(CE_Failure, CPLE_AppDefined,
452 : "GetFileMetadata failed");
453 : }
454 : }
455 : else
456 : {
457 4 : CPLJSONDocument oDoc;
458 6 : if (oDoc.LoadMemory(
459 : reinterpret_cast<const GByte *>(
460 2 : requestHelper.sWriteFuncData.pBuffer),
461 : static_cast<int>(
462 4 : requestHelper.sWriteFuncData.nSize)) &&
463 4 : oDoc.GetRoot().GetType() == CPLJSONObject::Type::Object)
464 : {
465 4 : for (const auto &oObj : oDoc.GetRoot().GetChildren())
466 : {
467 4 : aosResult.SetNameValue(oObj.GetName().c_str(),
468 6 : oObj.ToString().c_str());
469 : }
470 : }
471 : else
472 : {
473 : // Shouldn't happen normally
474 : aosResult.SetNameValue(
475 0 : "DATA", requestHelper.sWriteFuncData.pBuffer);
476 : }
477 : }
478 :
479 2 : curl_easy_cleanup(hCurlHandle);
480 : } while (bRetry);
481 :
482 2 : return aosResult.StealList();
483 : }
484 : }
485 :
486 4 : if (pszDomain == nullptr || !EQUAL(pszDomain, "ACL"))
487 : {
488 2 : return VSICurlFilesystemHandlerBase::GetFileMetadata(
489 2 : pszFilename, pszDomain, papszOptions);
490 : }
491 :
492 : auto poHandleHelper =
493 4 : std::unique_ptr<IVSIS3LikeHandleHelper>(VSIGSHandleHelper::BuildFromURI(
494 10 : pszFilename + GetFSPrefix().size(), GetFSPrefix().c_str()));
495 2 : if (!poHandleHelper)
496 0 : return nullptr;
497 :
498 4 : NetworkStatisticsFileSystem oContextFS(GetFSPrefix().c_str());
499 4 : NetworkStatisticsAction oContextAction("GetFileMetadata");
500 :
501 4 : const CPLStringList aosHTTPOptions(CPLHTTPGetOptionsFromEnv(pszFilename));
502 4 : const CPLHTTPRetryParameters oRetryParameters(aosHTTPOptions);
503 4 : CPLHTTPRetryContext oRetryContext(oRetryParameters);
504 :
505 : bool bRetry;
506 4 : CPLStringList aosResult;
507 2 : do
508 : {
509 2 : bRetry = false;
510 2 : CURL *hCurlHandle = curl_easy_init();
511 2 : poHandleHelper->AddQueryParameter("acl", "");
512 :
513 : struct curl_slist *headers = static_cast<struct curl_slist *>(
514 2 : CPLHTTPSetOptions(hCurlHandle, poHandleHelper->GetURL().c_str(),
515 : aosHTTPOptions.List()));
516 2 : headers = poHandleHelper->GetCurlHeaders("GET", headers);
517 :
518 4 : CurlRequestHelper requestHelper;
519 2 : const long response_code = requestHelper.perform(
520 : hCurlHandle, headers, this, poHandleHelper.get());
521 :
522 2 : NetworkStatisticsLogger::LogGET(requestHelper.sWriteFuncData.nSize);
523 :
524 2 : if (response_code != 200 ||
525 1 : requestHelper.sWriteFuncData.pBuffer == nullptr)
526 : {
527 : // Look if we should attempt a retry
528 1 : if (oRetryContext.CanRetry(
529 : static_cast<int>(response_code),
530 1 : requestHelper.sWriteFuncHeaderData.pBuffer,
531 : requestHelper.szCurlErrBuf))
532 : {
533 0 : CPLError(CE_Warning, CPLE_AppDefined,
534 : "HTTP error code: %d - %s. "
535 : "Retrying again in %.1f secs",
536 : static_cast<int>(response_code),
537 0 : poHandleHelper->GetURL().c_str(),
538 : oRetryContext.GetCurrentDelay());
539 0 : CPLSleep(oRetryContext.GetCurrentDelay());
540 0 : bRetry = true;
541 : }
542 : else
543 : {
544 1 : CPLDebug(GetDebugKey(), "%s",
545 1 : requestHelper.sWriteFuncData.pBuffer
546 : ? requestHelper.sWriteFuncData.pBuffer
547 : : "(null)");
548 1 : CPLError(CE_Failure, CPLE_AppDefined, "GetFileMetadata failed");
549 : }
550 : }
551 : else
552 : {
553 1 : aosResult.SetNameValue("XML", requestHelper.sWriteFuncData.pBuffer);
554 : }
555 :
556 2 : curl_easy_cleanup(hCurlHandle);
557 : } while (bRetry);
558 2 : return aosResult.StealList();
559 : }
560 :
561 : /************************************************************************/
562 : /* SetFileMetadata() */
563 : /************************************************************************/
564 :
565 5 : bool VSIGSFSHandler::SetFileMetadata(const char *pszFilename,
566 : CSLConstList papszMetadata,
567 : const char *pszDomain,
568 : CSLConstList /* papszOptions */)
569 : {
570 5 : if (!STARTS_WITH_CI(pszFilename, GetFSPrefix().c_str()))
571 0 : return false;
572 :
573 5 : if (pszDomain == nullptr ||
574 5 : !(EQUAL(pszDomain, "HEADERS") || EQUAL(pszDomain, "ACL")))
575 : {
576 1 : CPLError(CE_Failure, CPLE_NotSupported,
577 : "Only HEADERS and ACL domain are supported");
578 1 : return false;
579 : }
580 :
581 4 : if (EQUAL(pszDomain, "HEADERS"))
582 : {
583 1 : return CopyObject(pszFilename, pszFilename, papszMetadata) == 0;
584 : }
585 :
586 3 : const char *pszXML = CSLFetchNameValue(papszMetadata, "XML");
587 3 : if (pszXML == nullptr)
588 : {
589 1 : CPLError(CE_Failure, CPLE_AppDefined, "XML key is missing in metadata");
590 1 : return false;
591 : }
592 :
593 : auto poHandleHelper =
594 4 : std::unique_ptr<IVSIS3LikeHandleHelper>(VSIGSHandleHelper::BuildFromURI(
595 10 : pszFilename + GetFSPrefix().size(), GetFSPrefix().c_str()));
596 2 : if (!poHandleHelper)
597 0 : return false;
598 :
599 4 : NetworkStatisticsFileSystem oContextFS(GetFSPrefix().c_str());
600 4 : NetworkStatisticsAction oContextAction("SetFileMetadata");
601 :
602 : bool bRetry;
603 2 : bool bRet = false;
604 :
605 4 : const CPLStringList aosHTTPOptions(CPLHTTPGetOptionsFromEnv(pszFilename));
606 4 : const CPLHTTPRetryParameters oRetryParameters(aosHTTPOptions);
607 2 : CPLHTTPRetryContext oRetryContext(oRetryParameters);
608 :
609 2 : do
610 : {
611 2 : bRetry = false;
612 2 : CURL *hCurlHandle = curl_easy_init();
613 2 : poHandleHelper->AddQueryParameter("acl", "");
614 2 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_CUSTOMREQUEST, "PUT");
615 2 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_POSTFIELDS, pszXML);
616 :
617 : struct curl_slist *headers = static_cast<struct curl_slist *>(
618 2 : CPLHTTPSetOptions(hCurlHandle, poHandleHelper->GetURL().c_str(),
619 : aosHTTPOptions.List()));
620 2 : headers = curl_slist_append(headers, "Content-Type: application/xml");
621 4 : headers = poHandleHelper->GetCurlHeaders("PUT", headers, pszXML,
622 2 : strlen(pszXML));
623 2 : NetworkStatisticsLogger::LogPUT(strlen(pszXML));
624 :
625 4 : CurlRequestHelper requestHelper;
626 2 : const long response_code = requestHelper.perform(
627 : hCurlHandle, headers, this, poHandleHelper.get());
628 :
629 2 : if (response_code != 200)
630 : {
631 : // Look if we should attempt a retry
632 1 : if (oRetryContext.CanRetry(
633 : static_cast<int>(response_code),
634 1 : requestHelper.sWriteFuncHeaderData.pBuffer,
635 : requestHelper.szCurlErrBuf))
636 : {
637 0 : CPLError(CE_Warning, CPLE_AppDefined,
638 : "HTTP error code: %d - %s. "
639 : "Retrying again in %.1f secs",
640 : static_cast<int>(response_code),
641 0 : poHandleHelper->GetURL().c_str(),
642 : oRetryContext.GetCurrentDelay());
643 0 : CPLSleep(oRetryContext.GetCurrentDelay());
644 0 : bRetry = true;
645 : }
646 : else
647 : {
648 1 : CPLDebug(GetDebugKey(), "%s",
649 1 : requestHelper.sWriteFuncData.pBuffer
650 : ? requestHelper.sWriteFuncData.pBuffer
651 : : "(null)");
652 1 : CPLError(CE_Failure, CPLE_AppDefined, "SetFileMetadata failed");
653 : }
654 : }
655 : else
656 : {
657 1 : bRet = true;
658 : }
659 :
660 2 : curl_easy_cleanup(hCurlHandle);
661 : } while (bRetry);
662 2 : return bRet;
663 : }
664 :
665 : /************************************************************************/
666 : /* UnlinkBatch() */
667 : /************************************************************************/
668 :
669 2 : int *VSIGSFSHandler::UnlinkBatch(CSLConstList papszFiles)
670 : {
671 : // Implemented using
672 : // https://cloud.google.com/storage/docs/json_api/v1/how-tos/batch
673 :
674 2 : const char *pszFirstFilename =
675 2 : papszFiles && papszFiles[0] ? papszFiles[0] : nullptr;
676 :
677 2 : bool bUsingBearerToken = false;
678 2 : if (pszFirstFilename)
679 : {
680 : const CPLStringList aosHTTPOptions(
681 4 : CPLHTTPGetOptionsFromEnv(pszFirstFilename));
682 2 : const char *pszHeaders = aosHTTPOptions.FetchNameValue("HEADERS");
683 2 : if (pszHeaders && strstr(pszHeaders, "Authorization: Bearer "))
684 1 : bUsingBearerToken = true;
685 : }
686 :
687 : auto poHandleHelper =
688 : std::unique_ptr<VSIGSHandleHelper>(VSIGSHandleHelper::BuildFromURI(
689 2 : "batch/storage/v1", GetFSPrefix().c_str(),
690 6 : pszFirstFilename &&
691 4 : STARTS_WITH(pszFirstFilename, GetFSPrefix().c_str())
692 6 : ? pszFirstFilename + GetFSPrefix().size()
693 8 : : nullptr));
694 :
695 : // The JSON API cannot be used with HMAC keys
696 2 : if ((poHandleHelper && poHandleHelper->UsesHMACKey()) && !bUsingBearerToken)
697 : {
698 0 : CPLDebug(GetDebugKey(), "UnlinkBatch() has an efficient implementation "
699 : "only for OAuth2 authentication");
700 0 : return VSICurlFilesystemHandlerBase::UnlinkBatch(papszFiles);
701 : }
702 :
703 : int *panRet =
704 2 : static_cast<int *>(CPLCalloc(sizeof(int), CSLCount(papszFiles)));
705 :
706 2 : if (!poHandleHelper || pszFirstFilename == nullptr)
707 0 : return panRet;
708 :
709 4 : NetworkStatisticsFileSystem oContextFS(GetFSPrefix().c_str());
710 4 : NetworkStatisticsAction oContextAction("UnlinkBatch");
711 :
712 : // For debug / testing only
713 : const int nBatchSize =
714 2 : std::max(1, std::min(100, atoi(CPLGetConfigOption(
715 2 : "CPL_VSIGS_UNLINK_BATCH_SIZE", "100"))));
716 4 : std::string osPOSTContent;
717 :
718 : const CPLStringList aosHTTPOptions(
719 4 : CPLHTTPGetOptionsFromEnv(pszFirstFilename));
720 4 : const CPLHTTPRetryParameters oRetryParameters(aosHTTPOptions);
721 4 : CPLHTTPRetryContext oRetryContext(oRetryParameters);
722 :
723 6 : for (int i = 0; papszFiles && papszFiles[i]; i++)
724 : {
725 4 : CPLAssert(STARTS_WITH_CI(papszFiles[i], GetFSPrefix().c_str()));
726 : const char *pszFilenameWithoutPrefix =
727 4 : papszFiles[i] + GetFSPrefix().size();
728 4 : const char *pszSlash = strchr(pszFilenameWithoutPrefix, '/');
729 4 : if (!pszSlash)
730 0 : return panRet;
731 8 : std::string osBucket;
732 : osBucket.assign(pszFilenameWithoutPrefix,
733 4 : pszSlash - pszFilenameWithoutPrefix);
734 :
735 8 : std::string osResource = "storage/v1/b/";
736 4 : osResource += osBucket;
737 4 : osResource += "/o/";
738 4 : osResource += CPLAWSURLEncode(pszSlash + 1, true);
739 :
740 : #ifdef ADD_AUTH_TO_NESTED_REQUEST
741 : std::string osAuthorization;
742 : std::string osDate;
743 : {
744 : auto poTmpHandleHelper = std::unique_ptr<IVSIS3LikeHandleHelper>(
745 : VSIGSHandleHelper::BuildFromURI(osResource.c_str(),
746 : GetFSPrefix().c_str()));
747 : CURL *hCurlHandle = curl_easy_init();
748 : struct curl_slist *subrequest_headers =
749 : static_cast<struct curl_slist *>(CPLHTTPSetOptions(
750 : hCurlHandle, poTmpHandleHelper->GetURL().c_str(),
751 : aosHTTPOptions.List()));
752 : subrequest_headers = poTmpHandleHelper->GetCurlHeaders(
753 : "DELETE", subrequest_headers, nullptr, 0);
754 : for (struct curl_slist *iter = subrequest_headers; iter;
755 : iter = iter->next)
756 : {
757 : if (STARTS_WITH_CI(iter->data, "Authorization: "))
758 : {
759 : osAuthorization = iter->data;
760 : }
761 : else if (STARTS_WITH_CI(iter->data, "Date: "))
762 : {
763 : osDate = iter->data;
764 : }
765 : }
766 : curl_slist_free_all(subrequest_headers);
767 : curl_easy_cleanup(hCurlHandle);
768 : }
769 : #endif
770 :
771 4 : osPOSTContent += "--===============7330845974216740156==\r\n";
772 4 : osPOSTContent += "Content-Type: application/http\r\n";
773 4 : osPOSTContent += CPLSPrintf("Content-ID: <%d>\r\n", i + 1);
774 4 : osPOSTContent += "\r\n\r\n";
775 4 : osPOSTContent += "DELETE /";
776 4 : osPOSTContent += osResource;
777 4 : osPOSTContent += " HTTP/1.1\r\n";
778 : #ifdef ADD_AUTH_TO_NESTED_REQUEST
779 : if (!osAuthorization.empty())
780 : {
781 : osPOSTContent += osAuthorization;
782 : osPOSTContent += "\r\n";
783 : }
784 : if (!osDate.empty())
785 : {
786 : osPOSTContent += osDate;
787 : osPOSTContent += "\r\n";
788 : }
789 : #endif
790 4 : osPOSTContent += "\r\n\r\n";
791 :
792 4 : if (((i + 1) % nBatchSize) == 0 || papszFiles[i + 1] == nullptr)
793 : {
794 3 : osPOSTContent += "--===============7330845974216740156==--\r\n";
795 :
796 : #ifdef DEBUG_VERBOSE
797 : CPLDebug(GetDebugKey(), "%s", osPOSTContent.c_str());
798 : #endif
799 :
800 : // Run request
801 : bool bRetry;
802 6 : std::string osResponse;
803 3 : do
804 : {
805 3 : bRetry = false;
806 3 : CURL *hCurlHandle = curl_easy_init();
807 :
808 3 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_CUSTOMREQUEST,
809 : "POST");
810 3 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_POSTFIELDS,
811 : osPOSTContent.c_str());
812 :
813 : struct curl_slist *headers =
814 6 : static_cast<struct curl_slist *>(CPLHTTPSetOptions(
815 3 : hCurlHandle, poHandleHelper->GetURL().c_str(),
816 : aosHTTPOptions.List()));
817 3 : headers = curl_slist_append(
818 : headers,
819 : "Content-Type: multipart/mixed; "
820 : "boundary=\"===============7330845974216740156==\"");
821 6 : headers = poHandleHelper->GetCurlHeaders("POST", headers,
822 3 : osPOSTContent.c_str(),
823 : osPOSTContent.size());
824 :
825 6 : CurlRequestHelper requestHelper;
826 3 : const long response_code = requestHelper.perform(
827 3 : hCurlHandle, headers, this, poHandleHelper.get());
828 :
829 3 : NetworkStatisticsLogger::LogPOST(
830 : osPOSTContent.size(), requestHelper.sWriteFuncData.nSize);
831 :
832 3 : if (response_code != 200 ||
833 3 : requestHelper.sWriteFuncData.pBuffer == nullptr)
834 : {
835 : // Look if we should attempt a retry
836 0 : if (oRetryContext.CanRetry(
837 : static_cast<int>(response_code),
838 0 : requestHelper.sWriteFuncHeaderData.pBuffer,
839 : requestHelper.szCurlErrBuf))
840 : {
841 0 : CPLError(CE_Warning, CPLE_AppDefined,
842 : "HTTP error code: %d - %s. "
843 : "Retrying again in %.1f secs",
844 : static_cast<int>(response_code),
845 0 : poHandleHelper->GetURL().c_str(),
846 : oRetryContext.GetCurrentDelay());
847 0 : CPLSleep(oRetryContext.GetCurrentDelay());
848 0 : bRetry = true;
849 : }
850 : else
851 : {
852 0 : CPLDebug(GetDebugKey(), "%s",
853 0 : requestHelper.sWriteFuncData.pBuffer
854 : ? requestHelper.sWriteFuncData.pBuffer
855 : : "(null)");
856 0 : CPLError(CE_Failure, CPLE_AppDefined,
857 : "DeleteObjects failed");
858 : }
859 : }
860 : else
861 : {
862 : #ifdef DEBUG_VERBOSE
863 : CPLDebug(GetDebugKey(), "%s",
864 : requestHelper.sWriteFuncData.pBuffer);
865 : #endif
866 3 : osResponse = requestHelper.sWriteFuncData.pBuffer;
867 : }
868 :
869 3 : curl_easy_cleanup(hCurlHandle);
870 : } while (bRetry);
871 :
872 : // Mark deleted files
873 107 : for (int j = i + 1 - nBatchSize; j <= i; j++)
874 : {
875 104 : auto nPos = osResponse.find(
876 : CPLSPrintf("Content-ID: <response-%d>", j + 1));
877 104 : if (nPos != std::string::npos)
878 : {
879 4 : nPos = osResponse.find("HTTP/1.1 ", nPos);
880 4 : if (nPos != std::string::npos)
881 : {
882 : const char *pszHTTPCode =
883 4 : osResponse.c_str() + nPos + strlen("HTTP/1.1 ");
884 4 : panRet[j] = (atoi(pszHTTPCode) == 204) ? 1 : 0;
885 : }
886 : }
887 : }
888 :
889 3 : osPOSTContent.clear();
890 : }
891 : }
892 2 : return panRet;
893 : }
894 :
895 : /************************************************************************/
896 : /* RmdirRecursive() */
897 : /************************************************************************/
898 :
899 1 : int VSIGSFSHandler::RmdirRecursive(const char *pszDirname)
900 : {
901 : // For debug / testing only
902 : const int nBatchSize = std::min(
903 1 : 100, atoi(CPLGetConfigOption("CPL_VSIGS_UNLINK_BATCH_SIZE", "100")));
904 :
905 1 : return RmdirRecursiveInternal(pszDirname, nBatchSize);
906 : }
907 :
908 : /************************************************************************/
909 : /* GetStreamingFilename() */
910 : /************************************************************************/
911 :
912 : std::string
913 5977 : VSIGSFSHandler::GetStreamingFilename(const std::string &osFilename) const
914 : {
915 5977 : if (STARTS_WITH(osFilename.c_str(), GetFSPrefix().c_str()))
916 11950 : return "/vsigs_streaming/" + osFilename.substr(GetFSPrefix().size());
917 0 : return osFilename;
918 : }
919 :
920 : /************************************************************************/
921 : /* VSIGSHandle() */
922 : /************************************************************************/
923 :
924 34 : VSIGSHandle::VSIGSHandle(VSIGSFSHandler *poFSIn, const char *pszFilename,
925 34 : VSIGSHandleHelper *poHandleHelper)
926 34 : : IVSIS3LikeHandle(poFSIn, pszFilename, poHandleHelper->GetURL().c_str()),
927 34 : m_poHandleHelper(poHandleHelper)
928 : {
929 34 : }
930 :
931 : /************************************************************************/
932 : /* ~VSIGSHandle() */
933 : /************************************************************************/
934 :
935 68 : VSIGSHandle::~VSIGSHandle()
936 : {
937 34 : delete m_poHandleHelper;
938 68 : }
939 :
940 : /************************************************************************/
941 : /* GetCurlHeaders() */
942 : /************************************************************************/
943 :
944 28 : struct curl_slist *VSIGSHandle::GetCurlHeaders(const std::string &osVerb,
945 : struct curl_slist *psHeaders)
946 : {
947 28 : return m_poHandleHelper->GetCurlHeaders(osVerb, psHeaders);
948 : }
949 :
950 : } /* end of namespace cpl */
951 :
952 : #endif // DOXYGEN_SKIP
953 : //! @endcond
954 :
955 : /************************************************************************/
956 : /* VSIInstallGSFileHandler() */
957 : /************************************************************************/
958 :
959 : /*!
960 : \brief Install /vsigs/ Google Cloud Storage file system handler
961 : (requires libcurl)
962 :
963 : \verbatim embed:rst
964 : See :ref:`/vsigs/ documentation <vsigs>`
965 : \endverbatim
966 :
967 : */
968 :
969 1808 : void VSIInstallGSFileHandler(void)
970 : {
971 1808 : VSIFileManager::InstallHandler(
972 3616 : "/vsigs/", std::make_shared<cpl::VSIGSFSHandler>("/vsigs/"));
973 1808 : }
974 :
975 : #endif /* HAVE_CURL */
|