1#!/usr/bin/env python3
2###############################################################################
3# $Id: gdal_cp.py faa1d4a1e4ec5876bf06c69377920f635779126a 2021-04-22 11:13:58 +0200 Even Rouault $
4#
5#  Project:  GDAL samples
6#  Purpose:  Copy a virtual file
7#  Author:   Even Rouault <even dot rouault at spatialys.com>
8#
9###############################################################################
10#  Copyright (c) 2011, Even Rouault <even dot rouault at spatialys.com>
11#
12#  Permission is hereby granted, free of charge, to any person obtaining a
13#  copy of this software and associated documentation files (the "Software"),
14#  to deal in the Software without restriction, including without limitation
15#  the rights to use, copy, modify, merge, publish, distribute, sublicense,
16#  and/or sell copies of the Software, and to permit persons to whom the
17#  Software is furnished to do so, subject to the following conditions:
18#
19#  The above copyright notice and this permission notice shall be included
20#  in all copies or substantial portions of the Software.
21#
22#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
23#  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
25#  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
28#  DEALINGS IN THE SOFTWARE.
29###############################################################################
30
31import fnmatch
32import os
33import stat
34import sys
35
36from osgeo import gdal
37
38
39def needsVSICurl(filename):
40    return filename.startswith('http://') or filename.startswith('https://') or filename.startswith('ftp://')
41
42
43def Usage():
44    print('Usage: gdal_cp [-progress] [-r] [-skipfailures] source_file target_file')
45    return -1
46
47
48class TermProgress(object):
49    def __init__(self):
50        self.nLastTick = -1
51        self.nThisTick = 0
52
53    def Progress(self, dfComplete, message):
54        # pylint: disable=unused-argument
55        self.nThisTick = int(dfComplete * 40.0)
56        if self.nThisTick > 40:
57            self.nThisTick = 40
58
59        # // Have we started a new progress run?
60        if self.nThisTick < self.nLastTick and self.nLastTick >= 39:
61            self.nLastTick = -1
62
63        if self.nThisTick <= self.nLastTick:
64            return True
65
66        while self.nThisTick > self.nLastTick:
67            self.nLastTick = self.nLastTick + 1
68            if self.nLastTick % 4 == 0:
69                val = int((self.nLastTick / 4) * 10)
70                sys.stdout.write("%d" % val)
71            else:
72                sys.stdout.write(".")
73
74        if self.nThisTick == 40:
75            print(" - done.")
76
77        sys.stdout.flush()
78
79        return True
80
81
82class ScaledProgress(object):
83    def __init__(self, dfMin, dfMax, UnderlyingProgress):
84        self.dfMin = dfMin
85        self.dfMax = dfMax
86        self.UnderlyingProgress = UnderlyingProgress
87
88    def Progress(self, dfComplete, message):
89        return self.UnderlyingProgress.Progress(dfComplete * (self.dfMax - self.dfMin) + self.dfMin,
90                                                message)
91
92
93def gdal_cp_single(srcfile, targetfile, progress):
94    if targetfile.endswith('/'):
95        stat_res = gdal.VSIStatL(targetfile)
96    else:
97        stat_res = gdal.VSIStatL(targetfile + '/')
98
99    if (stat_res is None and targetfile.endswith('/')) or \
100       (stat_res is not None and stat.S_ISDIR(stat_res.mode)):
101        (_, tail) = os.path.split(srcfile)
102        if targetfile.endswith('/'):
103            targetfile = targetfile + tail
104        else:
105            targetfile = targetfile + '/' + tail
106
107    fin = gdal.VSIFOpenL(srcfile, "rb")
108    if fin is None:
109        print('Cannot open %s' % srcfile)
110        return -1
111
112    fout = gdal.VSIFOpenL(targetfile, "wb")
113    if fout is None:
114        print('Cannot create %s' % targetfile)
115        gdal.VSIFCloseL(fin)
116        return -1
117
118    version_num = int(gdal.VersionInfo('VERSION_NUM'))
119    total_size = 0
120    if version_num < 1900 or progress is not None:
121        gdal.VSIFSeekL(fin, 0, 2)
122        total_size = gdal.VSIFTellL(fin)
123        gdal.VSIFSeekL(fin, 0, 0)
124
125    buf_max_size = 4096
126    copied = 0
127    ret = 0
128    # print('Copying %s...' % srcfile)
129    if progress is not None:
130        if not progress.Progress(0.0, 'Copying %s' % srcfile):
131            print('Copy stopped by user')
132            ret = -2
133
134    while ret == 0:
135        if total_size != 0 and copied + buf_max_size > total_size:
136            to_read = total_size - copied
137        else:
138            to_read = buf_max_size
139        buf = gdal.VSIFReadL(1, to_read, fin)
140        if buf is None:
141            if copied == 0:
142                print('Cannot read %d bytes in %s' % (to_read, srcfile))
143                ret = -1
144            break
145        buf_size = len(buf)
146        if gdal.VSIFWriteL(buf, 1, buf_size, fout) != buf_size:
147            print('Error writing %d bytes' % buf_size)
148            ret = -1
149            break
150        copied += buf_size
151        if progress is not None and total_size != 0:
152            if not progress.Progress(copied * 1.0 / total_size, 'Copying %s' % srcfile):
153                print('Copy stopped by user')
154                ret = -2
155                break
156        if to_read < buf_max_size or buf_size != buf_max_size:
157            break
158
159    gdal.VSIFCloseL(fin)
160    gdal.VSIFCloseL(fout)
161
162    return ret
163
164
165def gdal_cp_recurse(srcdir, targetdir, progress, skip_failure):
166
167    if srcdir[-1] == '/':
168        srcdir = srcdir[0:len(srcdir) - 1]
169    lst = gdal.ReadDir(srcdir)
170    if lst is None:
171        print('%s is not a directory' % srcdir)
172        return -1
173
174    if gdal.VSIStatL(targetdir) is None:
175        gdal.Mkdir(targetdir, int('0755', 8))
176
177    for srcfile in lst:
178        if srcfile == '.' or srcfile == '..':
179            continue
180        fullsrcfile = srcdir + '/' + srcfile
181        statBuf = gdal.VSIStatL(fullsrcfile, gdal.VSI_STAT_EXISTS_FLAG | gdal.VSI_STAT_NATURE_FLAG)
182        if statBuf.IsDirectory():
183            ret = gdal_cp_recurse(fullsrcfile, targetdir + '/' + srcfile, progress, skip_failure)
184        else:
185            ret = gdal_cp_single(fullsrcfile, targetdir, progress)
186        if ret == -2 or (ret == -1 and not skip_failure):
187            return ret
188    return 0
189
190
191def gdal_cp_pattern_match(srcdir, pattern, targetfile, progress, skip_failure):
192
193    if srcdir == '':
194        srcdir = '.'
195
196    lst = gdal.ReadDir(srcdir)
197    lst2 = []
198    if lst is None:
199        print('Cannot read directory %s' % srcdir)
200        return -1
201
202    for filename in lst:
203        if filename == '.' or filename == '..':
204            continue
205        if srcdir != '.':
206            lst2.append(srcdir + '/' + filename)
207        else:
208            lst2.append(filename)
209
210    if progress is not None:
211        total_size = 0
212        filesizelst = []
213        for srcfile in lst2:
214            filesize = 0
215            if fnmatch.fnmatch(srcfile, pattern):
216                fin = gdal.VSIFOpenL(srcfile, "rb")
217                if fin is not None:
218                    gdal.VSIFSeekL(fin, 0, 2)
219                    filesize = gdal.VSIFTellL(fin)
220                    gdal.VSIFCloseL(fin)
221
222            filesizelst.append(filesize)
223            total_size = total_size + filesize
224
225        if total_size == 0:
226            return -1
227
228        cursize = 0
229        i = 0
230        for srcfile in lst2:
231            if filesizelst[i] != 0:
232                dfMin = cursize * 1.0 / total_size
233                dfMax = (cursize + filesizelst[i]) * 1.0 / total_size
234                scaled_progress = ScaledProgress(dfMin, dfMax, progress)
235
236                ret = gdal_cp_single(srcfile, targetfile, scaled_progress)
237                if ret == -2 or (ret == -1 and not skip_failure):
238                    return ret
239
240                cursize += filesizelst[i]
241
242            i = i + 1
243
244    else:
245        for srcfile in lst2:
246            if fnmatch.fnmatch(srcfile, pattern):
247                ret = gdal_cp_single(srcfile, targetfile, progress)
248                if ret == -2 or (ret == -1 and not skip_failure):
249                    return ret
250    return 0
251
252
253def gdal_cp(argv, progress=None):
254    srcfile = None
255    targetfile = None
256    recurse = False
257    skip_failure = False
258
259    argv = gdal.GeneralCmdLineProcessor(argv)
260    if argv is None:
261        return -1
262
263    for i in range(1, len(argv)):
264        if argv[i] == '-progress':
265            progress = TermProgress()
266        elif argv[i] == '-r':
267            version_num = int(gdal.VersionInfo('VERSION_NUM'))
268            if version_num < 1900:
269                print('ERROR: Python bindings of GDAL 1.9.0 or later required for -r option')
270                return -1
271            recurse = True
272        elif len(argv[i]) >= 5 and argv[i][0:5] == '-skip':
273            skip_failure = True
274        elif argv[i][0] == '-':
275            print('Unrecognized option : %s' % argv[i])
276            return Usage()
277        elif srcfile is None:
278            srcfile = argv[i]
279        elif targetfile is None:
280            targetfile = argv[i]
281        else:
282            print('Unexpected option : %s' % argv[i])
283            return Usage()
284
285    if srcfile is None or targetfile is None:
286        return Usage()
287
288    if needsVSICurl(srcfile):
289        srcfile = '/vsicurl/' + srcfile
290
291    if recurse:
292        # Make sure that 'gdal_cp.py -r [srcdir/]lastsubdir targetdir' creates
293        # targetdir/lastsubdir if targetdir already exists (like cp -r does).
294        if srcfile[-1] == '/':
295            srcfile = srcfile[0:len(srcfile) - 1]
296        statBufSrc = gdal.VSIStatL(srcfile, gdal.VSI_STAT_EXISTS_FLAG | gdal.VSI_STAT_NATURE_FLAG)
297        if statBufSrc is None:
298            statBufSrc = gdal.VSIStatL(srcfile + '/', gdal.VSI_STAT_EXISTS_FLAG | gdal.VSI_STAT_NATURE_FLAG)
299        statBufDst = gdal.VSIStatL(targetfile, gdal.VSI_STAT_EXISTS_FLAG | gdal.VSI_STAT_NATURE_FLAG)
300        if statBufSrc is not None and statBufSrc.IsDirectory() and statBufDst is not None and statBufDst.IsDirectory():
301            if targetfile[-1] == '/':
302                targetfile = targetfile[0:-1]
303            if srcfile.rfind('/') != -1:
304                targetfile = targetfile + srcfile[srcfile.rfind('/'):]
305            else:
306                targetfile = targetfile + '/' + srcfile
307
308            if gdal.VSIStatL(targetfile) is None:
309                gdal.Mkdir(targetfile, int('0755', 8))
310
311        return gdal_cp_recurse(srcfile, targetfile, progress, skip_failure)
312
313    (srcdir, pattern) = os.path.split(srcfile)
314    if not srcdir.startswith('/vsi') and ('*' in pattern or '?' in pattern):
315        return gdal_cp_pattern_match(srcdir, pattern, targetfile, progress, skip_failure)
316    return gdal_cp_single(srcfile, targetfile, progress)
317
318
319def main(argv):
320    version_num = int(gdal.VersionInfo('VERSION_NUM'))
321    if version_num < 1800:
322        print('ERROR: Python bindings of GDAL 1.8.0 or later required')
323        return 1
324    return gdal_cp(argv)
325
326
327if __name__ == '__main__':
328    sys.exit(main(sys.argv))
329