~ubuntu-branches/ubuntu/trusty/duplicity/trusty

« back to all changes in this revision

Viewing changes to src/statistics.py

  • Committer: Package Import Robot
  • Author(s): Michael Terry
  • Date: 2011-12-06 14:15:01 UTC
  • mfrom: (1.9.4)
  • Revision ID: package-import@ubuntu.com-20111206141501-nvfaaauqivpwyb7f
Tags: 0.6.17-0ubuntu1
* New upstream release
* debian/patches/06_use_passphrase.dpatch,
  debian/patches/07_large_rackspace_list.dpatch,
  debian/patches/08_check_volumes.dpatch:
  - Dropped, applied upstream
* debian/rules:
  - Run new upstream test suite during build
* debian/control:
  - Add rdiff as a build-dep to run above test suite
* debian/patches/06testfixes.dpatch:
  - Fix a few tests to not fail erroneously
* debian/patches/07fixincresume.dpatch:
  - Fix a bug with resuming an incremental backup that would result in
    a bogus error.  Also patches in a test for it.
* debian/tests/full-cycle-local:
  - New DEP-8 test script that backs up locally, restores, and checks files
* debian/tests/full-cycle-u1:
  - New DEP-8 test script that does the same as above, but to Ubuntu One
* debian/tests/control:
  - Start of DEP-8 test suite.  Only enable above full-cycle-local test
    for automatic execution.  The other is for manual testing right now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
2
 
#
3
 
# Copyright 2002 Ben Escoto <ben@emerose.org>
4
 
# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
5
 
#
6
 
# This file is part of duplicity.
7
 
#
8
 
# Duplicity is free software; you can redistribute it and/or modify it
9
 
# under the terms of the GNU General Public License as published by the
10
 
# Free Software Foundation; either version 2 of the License, or (at your
11
 
# option) any later version.
12
 
#
13
 
# Duplicity is distributed in the hope that it will be useful, but
14
 
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
 
# General Public License for more details.
17
 
#
18
 
# You should have received a copy of the GNU General Public License
19
 
# along with duplicity; if not, write to the Free Software Foundation,
20
 
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
 
 
22
 
"""Generate and process backup statistics"""
23
 
 
24
 
import re, time, os
25
 
 
26
 
from duplicity import dup_time
27
 
 
28
 
 
29
 
class StatsException(Exception):
30
 
    pass
31
 
 
32
 
class StatsObj:
33
 
    """Contains various statistics, provide string conversion functions"""
34
 
    # used when quoting files in get_stats_line
35
 
    space_regex = re.compile(" ")
36
 
 
37
 
    stat_file_attrs = ('SourceFiles',
38
 
                       'SourceFileSize',
39
 
                       'NewFiles',
40
 
                       'NewFileSize',
41
 
                       'DeletedFiles',
42
 
                       'ChangedFiles',
43
 
                       'ChangedFileSize',
44
 
                       'ChangedDeltaSize',
45
 
                       'DeltaEntries',
46
 
                       'RawDeltaSize')
47
 
    stat_misc_attrs = ('Errors',
48
 
                       'TotalDestinationSizeChange')
49
 
    stat_time_attrs = ('StartTime',
50
 
                       'EndTime',
51
 
                       'ElapsedTime')
52
 
    stat_attrs = (('Filename',) + stat_time_attrs +
53
 
                  stat_misc_attrs + stat_file_attrs)
54
 
 
55
 
    # Below, the second value in each pair is true iff the value
56
 
    # indicates a number of bytes
57
 
    stat_file_pairs = (('SourceFiles', False),
58
 
                       ('SourceFileSize', True),
59
 
                       ('NewFiles', False),
60
 
                       ('NewFileSize', True),
61
 
                       ('DeletedFiles', False),
62
 
                       ('ChangedFiles', False),
63
 
                       ('ChangedFileSize', True),
64
 
                       ('ChangedDeltaSize', True),
65
 
                       ('DeltaEntries', False),
66
 
                       ('RawDeltaSize', True))
67
 
 
68
 
    # This is used in get_byte_summary_string below
69
 
    byte_abbrev_list = ((1024*1024*1024*1024, "TB"),
70
 
                        (1024*1024*1024, "GB"),
71
 
                        (1024*1024, "MB"),
72
 
                        (1024, "KB"))
73
 
 
74
 
    def __init__(self):
75
 
        """Set attributes to None"""
76
 
        for attr in self.stat_attrs:
77
 
            self.__dict__[attr] = None
78
 
 
79
 
    def get_stat(self, attribute):
80
 
        """Get a statistic"""
81
 
        return self.__dict__[attribute]
82
 
 
83
 
    def set_stat(self, attr, value):
84
 
        """Set attribute to given value"""
85
 
        self.__dict__[attr] = value
86
 
 
87
 
    def increment_stat(self, attr):
88
 
        """Add 1 to value of attribute"""
89
 
        self.__dict__[attr] += 1
90
 
 
91
 
    def get_total_dest_size_change(self):
92
 
        """Return total destination size change
93
 
 
94
 
        This represents the total increase in the size of the
95
 
        duplicity destination directory, or None if not available.
96
 
 
97
 
        """
98
 
        return 0 # this needs to be re-done for duplicity
99
 
 
100
 
    def get_stats_line(self, index, use_repr = 1):
101
 
        """Return one line abbreviated version of full stats string"""
102
 
        file_attrs = map(lambda attr: str(self.get_stat(attr)),
103
 
                         self.stat_file_attrs)
104
 
        if not index:
105
 
            filename = "."
106
 
        else:
107
 
            filename = apply(os.path.join, index)
108
 
            if use_repr:
109
 
                # use repr to quote newlines in relative filename, then
110
 
                # take of leading and trailing quote and quote spaces.
111
 
                filename = self.space_regex.sub("\\x20", repr(filename)[1:-1])
112
 
        return " ".join([filename,] + file_attrs)
113
 
 
114
 
    def set_stats_from_line(self, line):
115
 
        """Set statistics from given line"""
116
 
        def error():
117
 
            raise StatsException("Bad line '%s'" % line)
118
 
        if line[-1] == "\n":
119
 
            line = line[:-1]
120
 
        lineparts = line.split(" ")
121
 
        if len(lineparts) < len(self.stat_file_attrs):
122
 
            error()
123
 
        for attr, val_string in zip(self.stat_file_attrs,
124
 
                                    lineparts[-len(self.stat_file_attrs):]):
125
 
            try:
126
 
                val = long(val_string)
127
 
            except ValueError:
128
 
                try:
129
 
                    val = float(val_string)
130
 
                except ValueError:
131
 
                    error()
132
 
            self.set_stat(attr, val)
133
 
        return self
134
 
 
135
 
    def get_stats_string(self):
136
 
        """Return extended string printing out statistics"""
137
 
        return "%s%s%s" % (self.get_timestats_string(),
138
 
                           self.get_filestats_string(),
139
 
                           self.get_miscstats_string())
140
 
 
141
 
    def get_timestats_string(self):
142
 
        """Return portion of statistics string dealing with time"""
143
 
        timelist = []
144
 
        if self.StartTime is not None:
145
 
            timelist.append("StartTime %.2f (%s)\n" %
146
 
                     (self.StartTime, dup_time.timetopretty(self.StartTime)))
147
 
        if self.EndTime is not None:
148
 
            timelist.append("EndTime %.2f (%s)\n" %
149
 
                         (self.EndTime, dup_time.timetopretty(self.EndTime)))
150
 
        if self.ElapsedTime or (self.StartTime is not None and
151
 
                                self.EndTime is not None):
152
 
            if self.ElapsedTime is None:
153
 
                self.ElapsedTime = self.EndTime - self.StartTime
154
 
            timelist.append("ElapsedTime %.2f (%s)\n" %
155
 
                 (self.ElapsedTime, dup_time.inttopretty(self.ElapsedTime)))
156
 
        return "".join(timelist)
157
 
 
158
 
    def get_filestats_string(self):
159
 
        """Return portion of statistics string about files and bytes"""
160
 
        def fileline(stat_file_pair):
161
 
            """Return zero or one line of the string"""
162
 
            attr, in_bytes = stat_file_pair
163
 
            val = self.get_stat(attr)
164
 
            if val is None:
165
 
                return ""
166
 
            if in_bytes:
167
 
                return "%s %s (%s)\n" % (attr, val,
168
 
                                         self.get_byte_summary_string(val))
169
 
            else:
170
 
                return "%s %s\n" % (attr, val)
171
 
 
172
 
        return "".join(map(fileline, self.stat_file_pairs))
173
 
 
174
 
    def get_miscstats_string(self):
175
 
        """Return portion of extended stat string about misc attributes"""
176
 
        misc_string = ""
177
 
        tdsc = self.TotalDestinationSizeChange
178
 
        if tdsc is not None:
179
 
            misc_string += ("TotalDestinationSizeChange %s (%s)\n" %
180
 
                            (tdsc, self.get_byte_summary_string(tdsc)))
181
 
        if self.Errors is not None:
182
 
            misc_string += "Errors %d\n" % self.Errors
183
 
        return misc_string
184
 
 
185
 
    def get_byte_summary_string(self, byte_count):
186
 
        """Turn byte count into human readable string like "7.23GB" """
187
 
        if byte_count < 0:
188
 
            sign = "-"
189
 
            byte_count = -byte_count
190
 
        else:
191
 
            sign = ""
192
 
 
193
 
        for abbrev_bytes, abbrev_string in self.byte_abbrev_list:
194
 
            if byte_count >= abbrev_bytes:
195
 
                # Now get 3 significant figures
196
 
                abbrev_count = float(byte_count)/abbrev_bytes
197
 
                if abbrev_count >= 100:
198
 
                    precision = 0
199
 
                elif abbrev_count >= 10:
200
 
                    precision = 1
201
 
                else:
202
 
                    precision = 2
203
 
                return "%s%%.%df %s" % (sign, precision, abbrev_string) \
204
 
                       % (abbrev_count,)
205
 
        byte_count = round(byte_count)
206
 
        if byte_count == 1:
207
 
            return sign + "1 byte"
208
 
        else:
209
 
            return "%s%d bytes" % (sign, byte_count)
210
 
 
211
 
    def get_stats_logstring(self, title):
212
 
        """Like get_stats_string, but add header and footer"""
213
 
        header = "--------------[ %s ]--------------" % title
214
 
        footer = "-" * len(header)
215
 
        return "%s\n%s%s\n" % (header, self.get_stats_string(), footer)
216
 
 
217
 
    def set_stats_from_string(self, s):
218
 
        """Initialize attributes from string, return self for convenience"""
219
 
        def error(line):
220
 
            raise StatsException("Bad line '%s'" % line)
221
 
 
222
 
        for line in s.split("\n"):
223
 
            if not line:
224
 
                continue
225
 
            line_parts = line.split()
226
 
            if len(line_parts) < 2:
227
 
                error(line)
228
 
            attr, value_string = line_parts[:2]
229
 
            if not attr in self.stat_attrs:
230
 
                error(line)
231
 
            try:
232
 
                try:
233
 
                    val1 = long(value_string)
234
 
                except ValueError:
235
 
                    val1 = None
236
 
                val2 = float(value_string)
237
 
                if val1 == val2:
238
 
                    self.set_stat(attr, val1) # use integer val
239
 
                else:
240
 
                    self.set_stat(attr, val2) # use float
241
 
            except ValueError:
242
 
                error(line)
243
 
        return self
244
 
 
245
 
    def write_stats_to_path(self, path):
246
 
        """Write statistics string to given path"""
247
 
        fin = path.open("w")
248
 
        fin.write(self.get_stats_string())
249
 
        assert not fin.close()
250
 
 
251
 
    def read_stats_from_path(self, path):
252
 
        """Set statistics from path, return self for convenience"""
253
 
        fp = path.open("r")
254
 
        self.set_stats_from_string(fp.read())
255
 
        assert not fp.close()
256
 
        return self
257
 
 
258
 
    def stats_equal(self, s):
259
 
        """Return true if s has same statistics as self"""
260
 
        assert isinstance(s, StatsObj)
261
 
        for attr in self.stat_file_attrs:
262
 
            if self.get_stat(attr) != s.get_stat(attr):
263
 
                return None
264
 
        return 1
265
 
 
266
 
    def set_to_average(self, statobj_list):
267
 
        """Set self's attributes to average of those in statobj_list"""
268
 
        for attr in self.stat_attrs:
269
 
            self.set_stat(attr, 0)
270
 
        for statobj in statobj_list:
271
 
            for attr in self.stat_attrs:
272
 
                if statobj.get_stat(attr) is None:
273
 
                    self.set_stat(attr, None)
274
 
                elif self.get_stat(attr) is not None:
275
 
                    self.set_stat(attr, statobj.get_stat(attr) +
276
 
                                  self.get_stat(attr))
277
 
 
278
 
        # Don't compute average starting/stopping time
279
 
        self.StartTime = None
280
 
        self.EndTime = None
281
 
 
282
 
        for attr in self.stat_attrs:
283
 
            if self.get_stat(attr) is not None:
284
 
                self.set_stat(attr,
285
 
                              self.get_stat(attr)/float(len(statobj_list)))
286
 
        return self
287
 
 
288
 
    def get_statsobj_copy(self):
289
 
        """Return new StatsObj object with same stats as self"""
290
 
        s = StatsObj()
291
 
        for attr in self.stat_attrs:
292
 
            s.set_stat(attr, self.get_stat(attr))
293
 
        return s
294
 
 
295
 
 
296
 
class StatsDeltaProcess(StatsObj):
297
 
    """Keep track of statistics during DirDelta process"""
298
 
    def __init__(self):
299
 
        """StatsDeltaProcess initializer - zero file attributes"""
300
 
        StatsObj.__init__(self)
301
 
        for attr in StatsObj.stat_file_attrs:
302
 
            self.__dict__[attr] = 0
303
 
        self.Errors = 0
304
 
        self.StartTime = time.time()
305
 
 
306
 
    def add_new_file(self, path):
307
 
        """Add stats of new file path to statistics"""
308
 
        filesize = path.getsize()
309
 
        self.SourceFiles += 1
310
 
        # SourceFileSize is added-to incrementally as read
311
 
        self.NewFiles += 1
312
 
        self.NewFileSize += filesize
313
 
        self.DeltaEntries += 1
314
 
 
315
 
    def add_changed_file(self, path):
316
 
        """Add stats of file that has changed since last backup"""
317
 
        filesize = path.getsize()
318
 
        self.SourceFiles += 1
319
 
        # SourceFileSize is added-to incrementally as read
320
 
        self.ChangedFiles += 1
321
 
        self.ChangedFileSize += filesize
322
 
        self.DeltaEntries += 1
323
 
 
324
 
    def add_deleted_file(self):
325
 
        """Add stats of file no longer in source directory"""
326
 
        self.DeletedFiles += 1 # can't add size since not available
327
 
        self.DeltaEntries += 1
328
 
 
329
 
    def add_unchanged_file(self, path):
330
 
        """Add stats of file that hasn't changed since last backup"""
331
 
        filesize = path.getsize()
332
 
        self.SourceFiles += 1
333
 
        self.SourceFileSize += filesize
334
 
 
335
 
    def close(self):
336
 
        """End collection of data, set EndTime"""
337
 
        self.EndTime = time.time()