Coverage Report

Created: 2026-02-14 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_vsi_copy.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  gdal "vsi copy" subcommand
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_vsi_copy.h"
14
15
#include "cpl_conv.h"
16
#include "cpl_string.h"
17
#include "cpl_vsi.h"
18
#include "cpl_vsi_error.h"
19
20
#include <algorithm>
21
22
//! @cond Doxygen_Suppress
23
24
#ifndef _
25
0
#define _(x) (x)
26
#endif
27
28
/************************************************************************/
29
/*             GDALVSICopyAlgorithm::GDALVSICopyAlgorithm()             */
30
/************************************************************************/
31
32
GDALVSICopyAlgorithm::GDALVSICopyAlgorithm()
33
0
    : GDALAlgorithm(NAME, DESCRIPTION, HELP_URL)
34
0
{
35
0
    {
36
0
        auto &arg =
37
0
            AddArg("source", 0, _("Source file or directory name"), &m_source)
38
0
                .SetPositional()
39
0
                .SetMinCharCount(1)
40
0
                .SetRequired();
41
0
        SetAutoCompleteFunctionForFilename(arg, 0);
42
0
    }
43
0
    {
44
0
        auto &arg =
45
0
            AddArg("destination", 0, _("Destination file or directory name"),
46
0
                   &m_destination)
47
0
                .SetPositional()
48
0
                .SetMinCharCount(1)
49
0
                .SetRequired()
50
0
                .AddAction(
51
0
                    [this]()
52
0
                    {
53
                        // If outputting to stdout, automatically turn off progress bar
54
0
                        if (m_destination == "/vsistdout/")
55
0
                        {
56
0
                            auto quietArg = GetArg(GDAL_ARG_NAME_QUIET);
57
0
                            if (quietArg && quietArg->GetType() == GAAT_BOOLEAN)
58
0
                                quietArg->Set(true);
59
0
                        }
60
0
                    });
61
0
        SetAutoCompleteFunctionForFilename(arg, 0);
62
0
    }
63
64
0
    AddArg("recursive", 'r', _("Copy subdirectories recursively"),
65
0
           &m_recursive);
66
67
0
    AddArg("skip-errors", 0, _("Skip errors"), &m_skip);
68
0
    AddProgressArg();
69
0
}
70
71
/************************************************************************/
72
/*                   GDALVSICopyAlgorithm::RunImpl()                    */
73
/************************************************************************/
74
75
bool GDALVSICopyAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
76
                                   void *pProgressData)
77
0
{
78
0
    const auto ReportSourceNotAccessible =
79
0
        [this](int nOldErrorNum, int nNewErrorNum, const std::string &filename)
80
0
    {
81
0
        if (nOldErrorNum != nNewErrorNum)
82
0
        {
83
0
            ReportError(CE_Failure, CPLE_FileIO,
84
0
                        "'%s' cannot be accessed. %s: %s", filename.c_str(),
85
0
                        VSIErrorNumToString(nNewErrorNum),
86
0
                        VSIGetLastErrorMsg());
87
0
        }
88
0
        else
89
0
        {
90
0
            ReportError(CE_Failure, CPLE_FileIO, "'%s' cannot be accessed.",
91
0
                        filename.c_str());
92
0
        }
93
0
    };
94
95
0
    if (m_recursive || cpl::ends_with(m_source, "/*") ||
96
0
        cpl::ends_with(m_source, "\\*"))
97
0
    {
98
        // Make sure that copy -r [srcdir/]lastsubdir targetdir' creates
99
        // targetdir/lastsubdir if targetdir already exists (like cp -r does).
100
0
        if (m_source.back() == '/')
101
0
            m_source.pop_back();
102
103
0
        if (!cpl::ends_with(m_source, "/*") && !cpl::ends_with(m_source, "\\*"))
104
0
        {
105
0
            VSIErrorReset();
106
0
            const auto nOldErrorNum = VSIGetLastErrorNo();
107
0
            VSIStatBufL statBufSrc;
108
0
            bool srcExists = VSIStatL(m_source.c_str(), &statBufSrc) == 0;
109
0
            if (!srcExists)
110
0
            {
111
0
                srcExists = VSIStatL(std::string(m_source).append("/").c_str(),
112
0
                                     &statBufSrc) == 0;
113
0
            }
114
0
            const auto nNewErrorNum = VSIGetLastErrorNo();
115
0
            VSIStatBufL statBufDst;
116
0
            const bool dstExists =
117
0
                VSIStatExL(m_destination.c_str(), &statBufDst,
118
0
                           VSI_STAT_EXISTS_FLAG | VSI_STAT_NATURE_FLAG) == 0;
119
0
            if (srcExists && VSI_ISDIR(statBufSrc.st_mode) && dstExists &&
120
0
                VSI_ISDIR(statBufDst.st_mode))
121
0
            {
122
0
                if (m_destination.back() == '/')
123
0
                    m_destination.pop_back();
124
0
                const auto srcLastSlashPos = m_source.rfind('/');
125
0
                if (srcLastSlashPos != std::string::npos)
126
0
                    m_destination += m_source.substr(srcLastSlashPos);
127
0
                else
128
0
                    m_destination = CPLFormFilenameSafe(
129
0
                        m_destination.c_str(), m_source.c_str(), nullptr);
130
0
            }
131
0
            else if (!srcExists)
132
0
            {
133
0
                ReportSourceNotAccessible(nOldErrorNum, nNewErrorNum, m_source);
134
0
                return false;
135
0
            }
136
0
        }
137
0
        else
138
0
        {
139
0
            m_source.resize(m_source.size() - 2);
140
0
            VSIErrorReset();
141
0
            const auto nOldErrorNum = VSIGetLastErrorNo();
142
0
            VSIStatBufL statBufSrc;
143
0
            bool srcExists = VSIStatL(m_source.c_str(), &statBufSrc) == 0;
144
0
            if (!srcExists)
145
0
            {
146
0
                const auto nNewErrorNum = VSIGetLastErrorNo();
147
0
                ReportSourceNotAccessible(nOldErrorNum, nNewErrorNum, m_source);
148
0
                return false;
149
0
            }
150
0
        }
151
152
0
        uint64_t curAmount = 0;
153
0
        return CopyRecursive(m_source, m_destination, 0, m_recursive ? -1 : 0,
154
0
                             curAmount, 0, pfnProgress, pProgressData);
155
0
    }
156
0
    else
157
0
    {
158
0
        VSIStatBufL statBufSrc;
159
0
        VSIErrorReset();
160
0
        const auto nOldErrorNum = VSIGetLastErrorNo();
161
0
        bool srcExists = VSIStatL(m_source.c_str(), &statBufSrc) == 0;
162
0
        if (!srcExists)
163
0
        {
164
0
            const auto nNewErrorNum = VSIGetLastErrorNo();
165
0
            ReportSourceNotAccessible(nOldErrorNum, nNewErrorNum, m_source);
166
0
            return false;
167
0
        }
168
0
        if (VSI_ISDIR(statBufSrc.st_mode))
169
0
        {
170
0
            ReportError(CE_Failure, CPLE_FileIO,
171
0
                        "%s is a directory. Use -r/--recursive option",
172
0
                        m_source.c_str());
173
0
            return false;
174
0
        }
175
176
0
        return CopySingle(m_source, m_destination, ~(static_cast<uint64_t>(0)),
177
0
                          pfnProgress, pProgressData);
178
0
    }
179
0
}
180
181
/************************************************************************/
182
/*                  GDALVSICopyAlgorithm::CopySingle()                  */
183
/************************************************************************/
184
185
bool GDALVSICopyAlgorithm::CopySingle(const std::string &src,
186
                                      const std::string &dstIn, uint64_t size,
187
                                      GDALProgressFunc pfnProgress,
188
                                      void *pProgressData) const
189
0
{
190
0
    CPLDebug("gdal_vsi_copy", "Copying file %s...", src.c_str());
191
0
    VSIStatBufL sStat;
192
0
    std::string dst = dstIn;
193
0
    const bool bExists =
194
0
        VSIStatExL(dst.back() == '/' ? dst.c_str()
195
0
                                     : std::string(dst).append("/").c_str(),
196
0
                   &sStat, VSI_STAT_EXISTS_FLAG | VSI_STAT_NATURE_FLAG) == 0;
197
0
    if ((!bExists && dst.back() == '/') ||
198
0
        (bExists && VSI_ISDIR(sStat.st_mode)))
199
0
    {
200
0
        const std::string filename = CPLGetFilename(src.c_str());
201
0
        dst = CPLFormFilenameSafe(dst.c_str(), filename.c_str(), nullptr);
202
0
    }
203
0
    return VSICopyFile(src.c_str(), dst.c_str(), nullptr, size, nullptr,
204
0
                       pfnProgress, pProgressData) == 0 ||
205
0
           m_skip;
206
0
}
207
208
/************************************************************************/
209
/*                GDALVSICopyAlgorithm::CopyRecursive()                 */
210
/************************************************************************/
211
212
bool GDALVSICopyAlgorithm::CopyRecursive(const std::string &srcIn,
213
                                         const std::string &dst, int depth,
214
                                         int maxdepth, uint64_t &curAmount,
215
                                         uint64_t totalAmount,
216
                                         GDALProgressFunc pfnProgress,
217
                                         void *pProgressData) const
218
0
{
219
0
    std::string src(srcIn);
220
0
    if (src.back() == '/')
221
0
        src.pop_back();
222
223
0
    if (pfnProgress && depth == 0)
224
0
    {
225
0
        CPLDebug("gdal_vsi_copy", "Listing source files...");
226
0
        std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> dir(
227
0
            VSIOpenDir(src.c_str(), maxdepth, nullptr), VSICloseDir);
228
0
        if (dir)
229
0
        {
230
0
            while (const auto entry = VSIGetNextDirEntry(dir.get()))
231
0
            {
232
0
                if (!(entry->pszName[0] == '.' &&
233
0
                      (entry->pszName[1] == '.' || entry->pszName[1] == 0)))
234
0
                {
235
0
                    totalAmount += entry->nSize + 1;
236
0
                    if (!pfnProgress(0.0, "", pProgressData))
237
0
                        return false;
238
0
                }
239
0
            }
240
0
        }
241
0
    }
242
0
    totalAmount = std::max<uint64_t>(1, totalAmount);
243
244
0
    CPLDebug("gdal_vsi_copy", "Copying directory %s...", src.c_str());
245
0
    std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> dir(
246
0
        VSIOpenDir(src.c_str(), 0, nullptr), VSICloseDir);
247
0
    if (dir)
248
0
    {
249
0
        VSIStatBufL sStat;
250
0
        if (VSIStatL(dst.c_str(), &sStat) != 0)
251
0
        {
252
0
            if (VSIMkdir(dst.c_str(), 0755) != 0)
253
0
            {
254
0
                ReportError(m_skip ? CE_Warning : CE_Failure, CPLE_FileIO,
255
0
                            "Cannot create directory %s", dst.c_str());
256
0
                return m_skip;
257
0
            }
258
0
        }
259
260
0
        while (const auto entry = VSIGetNextDirEntry(dir.get()))
261
0
        {
262
0
            if (!(entry->pszName[0] == '.' &&
263
0
                  (entry->pszName[1] == '.' || entry->pszName[1] == 0)))
264
0
            {
265
0
                const std::string subsrc =
266
0
                    CPLFormFilenameSafe(src.c_str(), entry->pszName, nullptr);
267
0
                if (VSI_ISDIR(entry->nMode))
268
0
                {
269
0
                    const std::string subdest = CPLFormFilenameSafe(
270
0
                        dst.c_str(), entry->pszName, nullptr);
271
0
                    if (maxdepth < 0 || depth < maxdepth)
272
0
                    {
273
0
                        if (!CopyRecursive(subsrc, subdest, depth + 1, maxdepth,
274
0
                                           curAmount, totalAmount, pfnProgress,
275
0
                                           pProgressData) &&
276
0
                            !m_skip)
277
0
                        {
278
0
                            return false;
279
0
                        }
280
0
                    }
281
0
                    else
282
0
                    {
283
0
                        if (VSIStatL(subdest.c_str(), &sStat) != 0)
284
0
                        {
285
0
                            if (VSIMkdir(subdest.c_str(), 0755) != 0)
286
0
                            {
287
0
                                ReportError(m_skip ? CE_Warning : CE_Failure,
288
0
                                            CPLE_FileIO,
289
0
                                            "Cannot create directory %s",
290
0
                                            subdest.c_str());
291
0
                                if (!m_skip)
292
0
                                    return false;
293
0
                            }
294
0
                        }
295
0
                    }
296
0
                    curAmount += 1;
297
298
0
                    if (pfnProgress &&
299
0
                        !pfnProgress(
300
0
                            std::min(1.0, static_cast<double>(curAmount) /
301
0
                                              static_cast<double>(totalAmount)),
302
0
                            "", pProgressData))
303
0
                    {
304
0
                        return false;
305
0
                    }
306
0
                }
307
0
                else
308
0
                {
309
0
                    void *pScaledProgressData = GDALCreateScaledProgress(
310
0
                        static_cast<double>(curAmount) /
311
0
                            static_cast<double>(totalAmount),
312
0
                        std::min(1.0, static_cast<double>(curAmount +
313
0
                                                          entry->nSize + 1) /
314
0
                                          static_cast<double>(totalAmount)),
315
0
                        pfnProgress, pProgressData);
316
0
                    const bool bRet = CopySingle(
317
0
                        subsrc, dst, entry->nSize,
318
0
                        pScaledProgressData ? GDALScaledProgress : nullptr,
319
0
                        pScaledProgressData);
320
0
                    GDALDestroyScaledProgress(pScaledProgressData);
321
322
0
                    curAmount += entry->nSize + 1;
323
324
0
                    if (!bRet)
325
0
                        return false;
326
0
                }
327
0
            }
328
0
        }
329
0
    }
330
0
    else
331
0
    {
332
0
        ReportError(m_skip ? CE_Warning : CE_Failure, CPLE_AppDefined,
333
0
                    "%s is not a directory or cannot be opened", src.c_str());
334
0
        if (!m_skip)
335
0
            return false;
336
0
    }
337
0
    return true;
338
0
}
339
340
//! @endcond