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