Coverage Report

Created: 2026-08-14 09:29

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