Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: CPL - Common Portability Library
4 : * Purpose: Implement a write-only file handle using PUT chunked writing
5 : * Author: Even Rouault, even.rouault at spatialys.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2024, Even Rouault <even.rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "cpl_vsil_curl_class.h"
14 :
15 : #ifdef HAVE_CURL
16 :
17 : //! @cond Doxygen_Suppress
18 :
19 : #define unchecked_curl_easy_setopt(handle, opt, param) \
20 : CPL_IGNORE_RET_VAL(curl_easy_setopt(handle, opt, param))
21 :
22 : namespace cpl
23 : {
24 :
25 : /************************************************************************/
26 : /* VSIChunkedWriteHandle() */
27 : /************************************************************************/
28 :
29 3 : VSIChunkedWriteHandle::VSIChunkedWriteHandle(
30 : IVSIS3LikeFSHandler *poFS, const char *pszFilename,
31 3 : IVSIS3LikeHandleHelper *poS3HandleHelper, CSLConstList papszOptions)
32 : : m_poFS(poFS), m_osFilename(pszFilename),
33 : m_poS3HandleHelper(poS3HandleHelper), m_aosOptions(papszOptions),
34 : m_aosHTTPOptions(CPLHTTPGetOptionsFromEnv(pszFilename)),
35 3 : m_oRetryParameters(m_aosHTTPOptions)
36 : {
37 3 : }
38 :
39 : /************************************************************************/
40 : /* ~VSIChunkedWriteHandle() */
41 : /************************************************************************/
42 :
43 6 : VSIChunkedWriteHandle::~VSIChunkedWriteHandle()
44 : {
45 3 : VSIChunkedWriteHandle::Close();
46 3 : delete m_poS3HandleHelper;
47 :
48 3 : if (m_hCurlMulti)
49 : {
50 1 : if (m_hCurl)
51 : {
52 1 : curl_multi_remove_handle(m_hCurlMulti, m_hCurl);
53 1 : curl_easy_cleanup(m_hCurl);
54 : }
55 1 : VSICURLMultiCleanup(m_hCurlMulti);
56 : }
57 3 : CPLFree(m_sWriteFuncHeaderData.pBuffer);
58 6 : }
59 :
60 : /************************************************************************/
61 : /* Close() */
62 : /************************************************************************/
63 :
64 6 : int VSIChunkedWriteHandle::Close()
65 : {
66 6 : int nRet = 0;
67 6 : if (!m_bClosed)
68 : {
69 3 : m_bClosed = true;
70 3 : if (m_hCurlMulti != nullptr)
71 : {
72 1 : nRet = FinishChunkedTransfer();
73 : }
74 : else
75 : {
76 2 : if (!m_bError && !DoEmptyPUT())
77 0 : nRet = -1;
78 : }
79 : }
80 6 : return nRet;
81 : }
82 :
83 : /************************************************************************/
84 : /* InvalidateParentDirectory() */
85 : /************************************************************************/
86 :
87 3 : void VSIChunkedWriteHandle::InvalidateParentDirectory()
88 : {
89 3 : m_poFS->InvalidateCachedData(m_poS3HandleHelper->GetURL().c_str());
90 :
91 3 : std::string osFilenameWithoutSlash(m_osFilename);
92 3 : if (!osFilenameWithoutSlash.empty() && osFilenameWithoutSlash.back() == '/')
93 1 : osFilenameWithoutSlash.pop_back();
94 3 : m_poFS->InvalidateDirContent(
95 6 : CPLGetDirnameSafe(osFilenameWithoutSlash.c_str()));
96 3 : }
97 :
98 : /************************************************************************/
99 : /* Seek() */
100 : /************************************************************************/
101 :
102 0 : int VSIChunkedWriteHandle::Seek(vsi_l_offset nOffset, int nWhence)
103 : {
104 0 : if (!((nWhence == SEEK_SET && nOffset == m_nCurOffset) ||
105 0 : (nWhence == SEEK_CUR && nOffset == 0) ||
106 0 : (nWhence == SEEK_END && nOffset == 0)))
107 : {
108 0 : CPLError(CE_Failure, CPLE_NotSupported,
109 : "Seek not supported on writable %s files",
110 0 : m_poFS->GetFSPrefix().c_str());
111 0 : m_bError = true;
112 0 : return -1;
113 : }
114 0 : return 0;
115 : }
116 :
117 : /************************************************************************/
118 : /* Tell() */
119 : /************************************************************************/
120 :
121 0 : vsi_l_offset VSIChunkedWriteHandle::Tell()
122 : {
123 0 : return m_nCurOffset;
124 : }
125 :
126 : /************************************************************************/
127 : /* Read() */
128 : /************************************************************************/
129 :
130 0 : size_t VSIChunkedWriteHandle::Read(void * /* pBuffer */, size_t /* nSize */,
131 : size_t /* nMemb */)
132 : {
133 0 : CPLError(CE_Failure, CPLE_NotSupported,
134 : "Read not supported on writable %s files",
135 0 : m_poFS->GetFSPrefix().c_str());
136 0 : m_bError = true;
137 0 : return 0;
138 : }
139 :
140 : /************************************************************************/
141 : /* ReadCallBackBufferChunked() */
142 : /************************************************************************/
143 :
144 3 : size_t VSIChunkedWriteHandle::ReadCallBackBufferChunked(char *buffer,
145 : size_t size,
146 : size_t nitems,
147 : void *instream)
148 : {
149 3 : VSIChunkedWriteHandle *poThis =
150 : static_cast<VSIChunkedWriteHandle *>(instream);
151 3 : if (poThis->m_nChunkedBufferSize == 0)
152 : {
153 : // CPLDebug("VSIChunkedWriteHandle", "Writing 0 byte (finish)");
154 1 : return 0;
155 : }
156 2 : const size_t nSizeMax = size * nitems;
157 2 : size_t nSizeToWrite = nSizeMax;
158 2 : size_t nChunkedBufferRemainingSize =
159 2 : poThis->m_nChunkedBufferSize - poThis->m_nChunkedBufferOff;
160 2 : if (nChunkedBufferRemainingSize < nSizeToWrite)
161 2 : nSizeToWrite = nChunkedBufferRemainingSize;
162 2 : memcpy(buffer,
163 2 : static_cast<const GByte *>(poThis->m_pBuffer) +
164 2 : poThis->m_nChunkedBufferOff,
165 : nSizeToWrite);
166 2 : poThis->m_nChunkedBufferOff += nSizeToWrite;
167 : // CPLDebug("VSIChunkedWriteHandle", "Writing %d bytes", nSizeToWrite);
168 2 : return nSizeToWrite;
169 : }
170 :
171 : /************************************************************************/
172 : /* Write() */
173 : /************************************************************************/
174 :
175 2 : size_t VSIChunkedWriteHandle::Write(const void *pBuffer, size_t nSize,
176 : size_t nMemb)
177 : {
178 2 : if (m_bError)
179 0 : return 0;
180 :
181 2 : const size_t nBytesToWrite = nSize * nMemb;
182 2 : if (nBytesToWrite == 0)
183 0 : return 0;
184 :
185 2 : if (m_hCurlMulti == nullptr)
186 : {
187 1 : m_hCurlMulti = curl_multi_init();
188 : }
189 :
190 2 : WriteFuncStruct sWriteFuncData;
191 4 : CPLHTTPRetryContext oRetryContext(m_oRetryParameters);
192 : // We can only easily retry at the first chunk of a transfer
193 2 : bool bCanRetry = (m_hCurl == nullptr);
194 : bool bRetry;
195 2 : do
196 : {
197 2 : bRetry = false;
198 2 : struct curl_slist *headers = nullptr;
199 2 : if (m_hCurl == nullptr)
200 : {
201 1 : CURL *hCurlHandle = curl_easy_init();
202 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_UPLOAD, 1L);
203 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_READFUNCTION,
204 : ReadCallBackBufferChunked);
205 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_READDATA, this);
206 :
207 1 : VSICURLInitWriteFuncStruct(&sWriteFuncData, nullptr, nullptr,
208 : nullptr);
209 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA,
210 : &sWriteFuncData);
211 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION,
212 : VSICurlHandleWriteFunc);
213 :
214 1 : VSICURLInitWriteFuncStruct(&m_sWriteFuncHeaderData, nullptr,
215 : nullptr, nullptr);
216 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA,
217 : &m_sWriteFuncHeaderData);
218 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION,
219 : VSICurlHandleWriteFunc);
220 :
221 1 : headers = static_cast<struct curl_slist *>(CPLHTTPSetOptions(
222 1 : hCurlHandle, m_poS3HandleHelper->GetURL().c_str(),
223 1 : m_aosHTTPOptions.List()));
224 2 : headers = VSICurlSetCreationHeadersFromOptions(
225 1 : headers, m_aosOptions.List(), m_osFilename.c_str());
226 1 : headers = VSICurlMergeHeaders(
227 1 : headers, m_poS3HandleHelper->GetCurlHeaders("PUT", headers));
228 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_HTTPHEADER,
229 : headers);
230 :
231 1 : m_osCurlErrBuf.resize(CURL_ERROR_SIZE + 1);
232 1 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_ERRORBUFFER,
233 : &m_osCurlErrBuf[0]);
234 :
235 1 : curl_multi_add_handle(m_hCurlMulti, hCurlHandle);
236 1 : m_hCurl = hCurlHandle;
237 : }
238 :
239 2 : m_pBuffer = pBuffer;
240 2 : m_nChunkedBufferOff = 0;
241 2 : m_nChunkedBufferSize = nBytesToWrite;
242 :
243 2 : int repeats = 0;
244 : // cppcheck-suppress knownConditionTrueFalse
245 3 : while (m_nChunkedBufferOff < m_nChunkedBufferSize && !bRetry)
246 : {
247 : int still_running;
248 :
249 3 : memset(&m_osCurlErrBuf[0], 0, m_osCurlErrBuf.size());
250 :
251 3 : while (curl_multi_perform(m_hCurlMulti, &still_running) ==
252 3 : CURLM_CALL_MULTI_PERFORM &&
253 : // cppcheck-suppress knownConditionTrueFalse
254 0 : m_nChunkedBufferOff < m_nChunkedBufferSize)
255 : {
256 : // loop
257 : }
258 : // cppcheck-suppress knownConditionTrueFalse
259 3 : if (!still_running || m_nChunkedBufferOff == m_nChunkedBufferSize)
260 : break;
261 :
262 : CURLMsg *msg;
263 0 : do
264 : {
265 1 : int msgq = 0;
266 1 : msg = curl_multi_info_read(m_hCurlMulti, &msgq);
267 1 : if (msg && (msg->msg == CURLMSG_DONE))
268 : {
269 0 : CURL *e = msg->easy_handle;
270 0 : if (e == m_hCurl)
271 : {
272 : long response_code;
273 0 : curl_easy_getinfo(m_hCurl, CURLINFO_RESPONSE_CODE,
274 : &response_code);
275 0 : if (response_code != 200 && response_code != 201)
276 : {
277 : // Look if we should attempt a retry
278 0 : if (bCanRetry &&
279 0 : oRetryContext.CanRetry(
280 : static_cast<int>(response_code),
281 0 : m_sWriteFuncHeaderData.pBuffer,
282 : m_osCurlErrBuf.c_str()))
283 : {
284 0 : CPLError(CE_Warning, CPLE_AppDefined,
285 : "HTTP error code: %d - %s. "
286 : "Retrying again in %.1f secs",
287 : static_cast<int>(response_code),
288 0 : m_poS3HandleHelper->GetURL().c_str(),
289 : oRetryContext.GetCurrentDelay());
290 0 : CPLSleep(oRetryContext.GetCurrentDelay());
291 0 : bRetry = true;
292 : }
293 0 : else if (sWriteFuncData.pBuffer != nullptr &&
294 0 : m_poS3HandleHelper->CanRestartOnError(
295 0 : sWriteFuncData.pBuffer,
296 0 : m_sWriteFuncHeaderData.pBuffer, false))
297 : {
298 0 : bRetry = true;
299 : }
300 : else
301 : {
302 0 : CPLError(CE_Failure, CPLE_AppDefined,
303 : "Error %d: %s",
304 : static_cast<int>(response_code),
305 : m_osCurlErrBuf.c_str());
306 :
307 0 : curl_slist_free_all(headers);
308 0 : bRetry = false;
309 : }
310 :
311 0 : curl_multi_remove_handle(m_hCurlMulti, m_hCurl);
312 0 : curl_easy_cleanup(m_hCurl);
313 :
314 0 : CPLFree(sWriteFuncData.pBuffer);
315 0 : CPLFree(m_sWriteFuncHeaderData.pBuffer);
316 :
317 0 : m_hCurl = nullptr;
318 0 : sWriteFuncData.pBuffer = nullptr;
319 0 : m_sWriteFuncHeaderData.pBuffer = nullptr;
320 0 : if (!bRetry)
321 0 : return 0;
322 : }
323 : }
324 : }
325 1 : } while (msg);
326 :
327 1 : CPLMultiPerformWait(m_hCurlMulti, repeats);
328 : }
329 :
330 2 : m_nWrittenInPUT += nBytesToWrite;
331 :
332 2 : curl_slist_free_all(headers);
333 :
334 2 : m_pBuffer = nullptr;
335 :
336 2 : if (!bRetry)
337 : {
338 : long response_code;
339 2 : curl_easy_getinfo(m_hCurl, CURLINFO_RESPONSE_CODE, &response_code);
340 2 : if (response_code != 100)
341 : {
342 : // Look if we should attempt a retry
343 0 : if (bCanRetry &&
344 0 : oRetryContext.CanRetry(static_cast<int>(response_code),
345 0 : m_sWriteFuncHeaderData.pBuffer,
346 : m_osCurlErrBuf.c_str()))
347 : {
348 0 : CPLError(CE_Warning, CPLE_AppDefined,
349 : "HTTP error code: %d - %s. "
350 : "Retrying again in %.1f secs",
351 : static_cast<int>(response_code),
352 0 : m_poS3HandleHelper->GetURL().c_str(),
353 : oRetryContext.GetCurrentDelay());
354 0 : CPLSleep(oRetryContext.GetCurrentDelay());
355 0 : bRetry = true;
356 : }
357 0 : else if (sWriteFuncData.pBuffer != nullptr &&
358 0 : m_poS3HandleHelper->CanRestartOnError(
359 0 : sWriteFuncData.pBuffer,
360 0 : m_sWriteFuncHeaderData.pBuffer, false))
361 : {
362 0 : bRetry = true;
363 : }
364 : else
365 : {
366 0 : CPLError(CE_Failure, CPLE_AppDefined, "Error %d: %s",
367 : static_cast<int>(response_code),
368 : m_osCurlErrBuf.c_str());
369 0 : bRetry = false;
370 0 : nMemb = 0;
371 : }
372 :
373 0 : curl_multi_remove_handle(m_hCurlMulti, m_hCurl);
374 0 : curl_easy_cleanup(m_hCurl);
375 :
376 0 : CPLFree(sWriteFuncData.pBuffer);
377 0 : CPLFree(m_sWriteFuncHeaderData.pBuffer);
378 :
379 0 : m_hCurl = nullptr;
380 0 : sWriteFuncData.pBuffer = nullptr;
381 0 : m_sWriteFuncHeaderData.pBuffer = nullptr;
382 : }
383 : }
384 : } while (bRetry);
385 :
386 2 : m_nCurOffset += nBytesToWrite;
387 :
388 2 : return nMemb;
389 : }
390 :
391 : /************************************************************************/
392 : /* FinishChunkedTransfer() */
393 : /************************************************************************/
394 :
395 1 : int VSIChunkedWriteHandle::FinishChunkedTransfer()
396 : {
397 1 : if (m_hCurl == nullptr)
398 0 : return -1;
399 :
400 2 : NetworkStatisticsFileSystem oContextFS(m_poFS->GetFSPrefix().c_str());
401 2 : NetworkStatisticsFile oContextFile(m_osFilename.c_str());
402 2 : NetworkStatisticsAction oContextAction("Write");
403 :
404 1 : NetworkStatisticsLogger::LogPUT(m_nWrittenInPUT);
405 1 : m_nWrittenInPUT = 0;
406 :
407 1 : m_pBuffer = nullptr;
408 1 : m_nChunkedBufferOff = 0;
409 1 : m_nChunkedBufferSize = 0;
410 :
411 1 : VSICURLMultiPerform(m_hCurlMulti);
412 :
413 : long response_code;
414 1 : curl_easy_getinfo(m_hCurl, CURLINFO_RESPONSE_CODE, &response_code);
415 1 : if (response_code == 200 || response_code == 201)
416 : {
417 1 : InvalidateParentDirectory();
418 : }
419 : else
420 : {
421 0 : CPLError(CE_Failure, CPLE_AppDefined, "Error %d: %s",
422 : static_cast<int>(response_code), m_osCurlErrBuf.c_str());
423 0 : return -1;
424 : }
425 1 : return 0;
426 : }
427 :
428 : /************************************************************************/
429 : /* DoEmptyPUT() */
430 : /************************************************************************/
431 :
432 2 : bool VSIChunkedWriteHandle::DoEmptyPUT()
433 : {
434 2 : bool bSuccess = true;
435 : bool bRetry;
436 4 : CPLHTTPRetryContext oRetryContext(m_oRetryParameters);
437 :
438 4 : NetworkStatisticsFileSystem oContextFS(m_poFS->GetFSPrefix().c_str());
439 4 : NetworkStatisticsFile oContextFile(m_osFilename.c_str());
440 2 : NetworkStatisticsAction oContextAction("Write");
441 :
442 2 : do
443 : {
444 2 : bRetry = false;
445 :
446 2 : PutData putData;
447 2 : putData.pabyData = nullptr;
448 2 : putData.nOff = 0;
449 2 : putData.nTotalSize = 0;
450 :
451 2 : CURL *hCurlHandle = curl_easy_init();
452 2 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_UPLOAD, 1L);
453 2 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_READFUNCTION,
454 : PutData::ReadCallBackBuffer);
455 2 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_READDATA, &putData);
456 2 : unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_INFILESIZE, 0);
457 :
458 : struct curl_slist *headers = static_cast<struct curl_slist *>(
459 2 : CPLHTTPSetOptions(hCurlHandle, m_poS3HandleHelper->GetURL().c_str(),
460 2 : m_aosHTTPOptions.List()));
461 4 : headers = VSICurlSetCreationHeadersFromOptions(
462 2 : headers, m_aosOptions.List(), m_osFilename.c_str());
463 2 : headers = VSICurlMergeHeaders(
464 2 : headers, m_poS3HandleHelper->GetCurlHeaders("PUT", headers, "", 0));
465 2 : headers = curl_slist_append(headers, "Expect: 100-continue");
466 :
467 4 : CurlRequestHelper requestHelper;
468 4 : const long response_code = requestHelper.perform(
469 2 : hCurlHandle, headers, m_poFS, m_poS3HandleHelper);
470 :
471 2 : NetworkStatisticsLogger::LogPUT(0);
472 :
473 2 : if (response_code != 200 && response_code != 201)
474 : {
475 : // Look if we should attempt a retry
476 0 : if (oRetryContext.CanRetry(
477 : static_cast<int>(response_code),
478 0 : requestHelper.sWriteFuncHeaderData.pBuffer,
479 : requestHelper.szCurlErrBuf))
480 : {
481 0 : CPLError(CE_Warning, CPLE_AppDefined,
482 : "HTTP error code: %d - %s. "
483 : "Retrying again in %.1f secs",
484 : static_cast<int>(response_code),
485 0 : m_poS3HandleHelper->GetURL().c_str(),
486 : oRetryContext.GetCurrentDelay());
487 0 : CPLSleep(oRetryContext.GetCurrentDelay());
488 0 : bRetry = true;
489 : }
490 0 : else if (requestHelper.sWriteFuncData.pBuffer != nullptr &&
491 0 : m_poS3HandleHelper->CanRestartOnError(
492 0 : requestHelper.sWriteFuncData.pBuffer,
493 0 : requestHelper.sWriteFuncHeaderData.pBuffer, false))
494 : {
495 0 : bRetry = true;
496 : }
497 : else
498 : {
499 0 : CPLDebug("S3", "%s",
500 0 : requestHelper.sWriteFuncData.pBuffer
501 : ? requestHelper.sWriteFuncData.pBuffer
502 : : "(null)");
503 0 : CPLError(CE_Failure, CPLE_AppDefined,
504 : "DoSinglePartPUT of %s failed", m_osFilename.c_str());
505 0 : bSuccess = false;
506 : }
507 : }
508 : else
509 : {
510 2 : InvalidateParentDirectory();
511 : }
512 :
513 2 : if (requestHelper.sWriteFuncHeaderData.pBuffer != nullptr)
514 : {
515 : const char *pzETag =
516 2 : strstr(requestHelper.sWriteFuncHeaderData.pBuffer, "ETag: \"");
517 2 : if (pzETag)
518 : {
519 0 : pzETag += strlen("ETag: \"");
520 0 : const char *pszEndOfETag = strchr(pzETag, '"');
521 0 : if (pszEndOfETag)
522 : {
523 0 : FileProp oFileProp;
524 0 : oFileProp.eExists = EXIST_YES;
525 0 : oFileProp.fileSize = m_nBufferOff;
526 0 : oFileProp.bHasComputedFileSize = true;
527 0 : oFileProp.ETag.assign(pzETag, pszEndOfETag - pzETag);
528 0 : m_poFS->SetCachedFileProp(
529 0 : m_poFS->GetURLFromFilename(m_osFilename.c_str())
530 : .c_str(),
531 : oFileProp);
532 : }
533 : }
534 : }
535 :
536 2 : curl_easy_cleanup(hCurlHandle);
537 : } while (bRetry);
538 4 : return bSuccess;
539 : }
540 :
541 : } // namespace cpl
542 :
543 : //! @endcond
544 :
545 : #endif // HAVE_CURL
|