1
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3
# Copyright 2002 Ben Escoto <ben@emerose.org>
4
# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
6
# This file is part of duplicity.
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.
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.
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
22
"""Generate and process backup statistics"""
26
from duplicity import dup_time
29
class StatsException(Exception):
33
"""Contains various statistics, provide string conversion functions"""
34
# used when quoting files in get_stats_line
35
space_regex = re.compile(" ")
37
stat_file_attrs = ('SourceFiles',
47
stat_misc_attrs = ('Errors',
48
'TotalDestinationSizeChange')
49
stat_time_attrs = ('StartTime',
52
stat_attrs = (('Filename',) + stat_time_attrs +
53
stat_misc_attrs + stat_file_attrs)
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),
60
('NewFileSize', True),
61
('DeletedFiles', False),
62
('ChangedFiles', False),
63
('ChangedFileSize', True),
64
('ChangedDeltaSize', True),
65
('DeltaEntries', False),
66
('RawDeltaSize', True))
68
# This is used in get_byte_summary_string below
69
byte_abbrev_list = ((1024*1024*1024*1024, "TB"),
70
(1024*1024*1024, "GB"),
75
"""Set attributes to None"""
76
for attr in self.stat_attrs:
77
self.__dict__[attr] = None
79
def get_stat(self, attribute):
81
return self.__dict__[attribute]
83
def set_stat(self, attr, value):
84
"""Set attribute to given value"""
85
self.__dict__[attr] = value
87
def increment_stat(self, attr):
88
"""Add 1 to value of attribute"""
89
self.__dict__[attr] += 1
91
def get_total_dest_size_change(self):
92
"""Return total destination size change
94
This represents the total increase in the size of the
95
duplicity destination directory, or None if not available.
98
return 0 # this needs to be re-done for duplicity
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)
107
filename = apply(os.path.join, index)
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)
114
def set_stats_from_line(self, line):
115
"""Set statistics from given line"""
117
raise StatsException("Bad line '%s'" % line)
120
lineparts = line.split(" ")
121
if len(lineparts) < len(self.stat_file_attrs):
123
for attr, val_string in zip(self.stat_file_attrs,
124
lineparts[-len(self.stat_file_attrs):]):
126
val = long(val_string)
129
val = float(val_string)
132
self.set_stat(attr, val)
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())
141
def get_timestats_string(self):
142
"""Return portion of statistics string dealing with time"""
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)
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)
167
return "%s %s (%s)\n" % (attr, val,
168
self.get_byte_summary_string(val))
170
return "%s %s\n" % (attr, val)
172
return "".join(map(fileline, self.stat_file_pairs))
174
def get_miscstats_string(self):
175
"""Return portion of extended stat string about misc attributes"""
177
tdsc = self.TotalDestinationSizeChange
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
185
def get_byte_summary_string(self, byte_count):
186
"""Turn byte count into human readable string like "7.23GB" """
189
byte_count = -byte_count
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:
199
elif abbrev_count >= 10:
203
return "%s%%.%df %s" % (sign, precision, abbrev_string) \
205
byte_count = round(byte_count)
207
return sign + "1 byte"
209
return "%s%d bytes" % (sign, byte_count)
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)
217
def set_stats_from_string(self, s):
218
"""Initialize attributes from string, return self for convenience"""
220
raise StatsException("Bad line '%s'" % line)
222
for line in s.split("\n"):
225
line_parts = line.split()
226
if len(line_parts) < 2:
228
attr, value_string = line_parts[:2]
229
if not attr in self.stat_attrs:
233
val1 = long(value_string)
236
val2 = float(value_string)
238
self.set_stat(attr, val1) # use integer val
240
self.set_stat(attr, val2) # use float
245
def write_stats_to_path(self, path):
246
"""Write statistics string to given path"""
248
fin.write(self.get_stats_string())
249
assert not fin.close()
251
def read_stats_from_path(self, path):
252
"""Set statistics from path, return self for convenience"""
254
self.set_stats_from_string(fp.read())
255
assert not fp.close()
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):
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) +
278
# Don't compute average starting/stopping time
279
self.StartTime = None
282
for attr in self.stat_attrs:
283
if self.get_stat(attr) is not None:
285
self.get_stat(attr)/float(len(statobj_list)))
288
def get_statsobj_copy(self):
289
"""Return new StatsObj object with same stats as self"""
291
for attr in self.stat_attrs:
292
s.set_stat(attr, self.get_stat(attr))
296
class StatsDeltaProcess(StatsObj):
297
"""Keep track of statistics during DirDelta process"""
299
"""StatsDeltaProcess initializer - zero file attributes"""
300
StatsObj.__init__(self)
301
for attr in StatsObj.stat_file_attrs:
302
self.__dict__[attr] = 0
304
self.StartTime = time.time()
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
312
self.NewFileSize += filesize
313
self.DeltaEntries += 1
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
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
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
336
"""End collection of data, set EndTime"""
337
self.EndTime = time.time()