Coverage Report

Created: 2025-11-15 08:43

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