1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf8 -*- 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 22u"""Provide time related exceptions and functions""" 23from __future__ import division 24 25from past.utils import old_div 26from builtins import map 27 28import time 29import types 30import re 31import calendar 32import sys 33from duplicity import config 34from duplicity import util 35 36# For type testing against both int and long types that works in python 2/3 37if sys.version_info < (3,): 38 integer_types = (int, types.LongType) 39else: 40 integer_types = (int,) 41 42 43class TimeException(Exception): 44 pass 45 46 47_interval_conv_dict = {u"s": 1, u"m": 60, u"h": 3600, u"D": 86400, 48 u"W": 7 * 86400, u"M": 30 * 86400, u"Y": 365 * 86400} 49_integer_regexp = re.compile(u"^[0-9]+$") 50_interval_regexp = re.compile(u"^([0-9]+)([smhDWMY])") 51_genstr_date_regexp1 = re.compile(u"^(?P<year>[0-9]{4})[-/]" 52 u"(?P<month>[0-9]{1,2})[-/]" 53 u"(?P<day>[0-9]{1,2})$") 54_genstr_date_regexp2 = re.compile(u"^(?P<month>[0-9]{1,2})[-/]" 55 u"(?P<day>[0-9]{1,2})[-/]" 56 u"(?P<year>[0-9]{4})$") 57_genstr_date_regexp3 = re.compile(u"^(?P<year>[0-9]{4})" 58 u"(?P<month>[0-9]{2})" 59 u"(?P<day>[0-9]{2})Z$") 60curtime = curtimestr = None 61prevtime = prevtimestr = None 62 63bad_interval_string = _(u"""Bad interval string "%s" 64 65Intervals are specified like 2Y (2 years) or 2h30m (2.5 hours). The 66allowed special characters are s, m, h, D, W, M, and Y. See the man 67page for more information.""") 68 69bad_time_string = _(u"""Bad time string "%s" 70 71The acceptible time strings are intervals (like "3D64s"), w3-datetime 72strings, like "2002-04-26T04:22:01-07:00" (strings like 73"2002-04-26T04:22:01" are also acceptable - duplicity will use the 74current time zone), or ordinary dates like 2/4/1997 or 2001-04-23 75(various combinations are acceptable, but the month always precedes 76the day).""") 77 78 79def setcurtime(time_in_secs=None): 80 u"""Sets the current time in curtime and curtimestr""" 81 global curtime, curtimestr 82 t = time_in_secs or int(time.time()) 83 assert type(t) in integer_types 84 curtime, curtimestr = t, timetostring(t) 85 86 87def setprevtime(time_in_secs): 88 u"""Sets the previous time in prevtime and prevtimestr""" 89 global prevtime, prevtimestr 90 assert type(time_in_secs) in integer_types, prevtime 91 prevtime, prevtimestr = time_in_secs, timetostring(time_in_secs) 92 93 94def timetostring(timeinseconds): 95 u"""Return w3 or duplicity datetime compliant listing of timeinseconds""" 96 97 if config.old_filenames: 98 # We need to know if DST applies to append the correct offset. So 99 # 1. Save the tuple returned by localtime. 100 # 2. Pass the DST flag into gettzd 101 lcltime = time.localtime(timeinseconds) 102 return time.strftime(u"%Y-%m-%dT%H" + config.time_separator + 103 u"%M" + config.time_separator + u"%S", 104 lcltime) + gettzd(lcltime[-1]) 105 else: 106 # DST never applies to UTC 107 lcltime = time.gmtime(timeinseconds) 108 return time.strftime(u"%Y%m%dT%H%M%SZ", lcltime) 109 110 111def stringtotime(timestring): 112 u"""Return time in seconds from w3 or duplicity timestring 113 114 If there is an error parsing the string, or it doesn't look 115 like a valid datetime string, return None. 116 """ 117 try: 118 date, daytime = timestring[:19].split(u"T") 119 if len(timestring) == 16: 120 # new format for filename time 121 year, month, day = list(map(int, 122 [date[0:4], date[4:6], date[6:8]])) 123 hour, minute, second = list(map(int, 124 [daytime[0:2], daytime[2:4], daytime[4:6]])) 125 else: 126 # old format for filename time 127 year, month, day = list(map(int, date.split(u"-"))) 128 hour, minute, second = list(map(int, 129 daytime.split(config.time_separator))) 130 assert 1900 < year < 2100, year 131 assert 1 <= month <= 12 132 assert 1 <= day <= 31 133 assert 0 <= hour <= 23 134 assert 0 <= minute <= 59 135 assert 0 <= second <= 61 # leap seconds 136 # We want to return the time in units of seconds since the 137 # epoch. Unfortunately the only functin that does this 138 # works in terms of the current timezone and we have a 139 # timezone offset in the string. 140 timetuple = (year, month, day, hour, minute, second, -1, -1, 0) 141 142 if len(timestring) == 16: 143 # as said in documentation, time.gmtime() and timegm() are each others' inverse. 144 # As far as UTC format is used in new file format, 145 # do not rely on system's python DST and tzdata settings 146 # and use functions that working with UTC 147 utc_in_secs = calendar.timegm(timetuple) 148 else: 149 # mktime assumed that the tuple was a local time. Compensate 150 # by subtracting the value for the current timezone. 151 # We don't need to worry about DST here because we turned it 152 # off in the tuple 153 local_in_secs = time.mktime(timetuple) 154 utc_in_secs = local_in_secs - time.timezone 155 # Now apply the offset that we were given in the time string 156 # This gives the correct number of seconds from the epoch 157 # even when we're not in the same timezone that wrote the 158 # string 159 if len(timestring) == 16: 160 return int(utc_in_secs) 161 else: 162 return int(utc_in_secs + tzdtoseconds(timestring[19:])) 163 except (TypeError, ValueError, AssertionError): 164 return None 165 166 167def timetopretty(timeinseconds): 168 u"""Return pretty version of time""" 169 return time.asctime(time.localtime(timeinseconds)) 170 171 172def stringtopretty(timestring): 173 u"""Return pretty version of time given w3 time string""" 174 return timetopretty(stringtotime(timestring)) 175 176 177def inttopretty(seconds): 178 u"""Convert num of seconds to readable string like "2 hours".""" 179 partlist = [] 180 hours, seconds = divmod(seconds, 3600) 181 if hours > 1: 182 partlist.append(u"%d hours" % hours) 183 elif hours == 1: 184 partlist.append(u"1 hour") 185 186 minutes, seconds = divmod(seconds, 60) 187 if minutes > 1: 188 partlist.append(u"%d minutes" % minutes) 189 elif minutes == 1: 190 partlist.append(u"1 minute") 191 192 if seconds == 1: 193 partlist.append(u"1 second") 194 elif not partlist or seconds > 1: 195 if isinstance(seconds, integer_types): 196 partlist.append(u"%s seconds" % seconds) 197 else: 198 partlist.append(u"%.2f seconds" % seconds) 199 return u" ".join(partlist) 200 201 202def intstringtoseconds(interval_string): 203 u"""Convert a string expressing an interval (e.g. "4D2s") to seconds""" 204 def error(): 205 raise TimeException(bad_interval_string % util.escape(interval_string)) 206 207 if len(interval_string) < 2: 208 error() 209 210 total = 0 211 while interval_string: 212 match = _interval_regexp.match(interval_string) 213 if not match: 214 error() 215 num, ext = int(match.group(1)), match.group(2) 216 if ext not in _interval_conv_dict or num < 0: 217 error() 218 total += num * _interval_conv_dict[ext] 219 interval_string = interval_string[match.end(0):] 220 return total 221 222 223def gettzd(dstflag): 224 u"""Return w3's timezone identification string. 225 226 Expresed as [+/-]hh:mm. For instance, PST is -08:00. Zone is 227 coincides with what localtime(), etc., use. 228 229 """ 230 # time.daylight doesn't help us. It's a flag that indicates that we 231 # have a dst option for the current timezone. Compensate by allowing 232 # the caller to pass a flag to indicate that DST applies. This flag 233 # is in the same format as the last member of the tuple returned by 234 # time.localtime() 235 236 if dstflag > 0: 237 offset = old_div(-1 * time.altzone, 60) 238 else: 239 offset = old_div(-1 * time.timezone, 60) 240 if offset > 0: 241 prefix = u"+" 242 elif offset < 0: 243 prefix = u"-" 244 else: 245 return u"Z" # time is already in UTC 246 247 hours, minutes = list(map(abs, divmod(offset, 60))) 248 assert 0 <= hours <= 23 249 assert 0 <= minutes <= 59 250 return u"%s%02d%s%02d" % (prefix, hours, config.time_separator, minutes) 251 252 253def tzdtoseconds(tzd): 254 u"""Given w3 compliant TZD, return how far ahead UTC is""" 255 if tzd == u"Z": 256 return 0 257 assert len(tzd) == 6 # only accept forms like +08:00 for now 258 assert (tzd[0] == u"-" or tzd[0] == u"+") and \ 259 tzd[3] == config.time_separator 260 return -60 * (60 * int(tzd[:3]) + int(tzd[4:])) 261 262 263def cmp(time1, time2): 264 u"""Compare time1 and time2 and return -1, 0, or 1""" 265 if isinstance(time1, (str, u"".__class__)): 266 time1 = stringtotime(time1) 267 assert time1 is not None 268 if isinstance(time2, (str, u"".__class__)): 269 time2 = stringtotime(time2) 270 assert time2 is not None 271 272 if time1 < time2: 273 return -1 274 elif time1 == time2: 275 return 0 276 else: 277 return 1 278 279 280def genstrtotime(timestr, override_curtime=None): 281 u"""Convert a generic time string to a time in seconds""" 282 if override_curtime is None: 283 override_curtime = curtime 284 if timestr == u"now": 285 return override_curtime 286 287 def error(): 288 raise TimeException(bad_time_string % util.escape(timestr)) 289 290 # Test for straight integer 291 if _integer_regexp.search(timestr): 292 return int(timestr) 293 294 # Test for w3-datetime format, possibly missing tzd 295 # This is an ugly hack. We need to know if DST applies when doing 296 # gettzd. However, we don't have the flag to pass. Assume that DST 297 # doesn't apply and pass 0. Getting a reasonable default from 298 # localtime() is a bad idea, since we transition to/from DST between 299 # calls to this method on the same run 300 301 t = stringtotime(timestr) or stringtotime(timestr + gettzd(0)) 302 if t: 303 return t 304 305 try: # test for an interval, like "2 days ago" 306 return override_curtime - intstringtoseconds(timestr) 307 except TimeException: 308 pass 309 310 # Now check for dates like 2001/3/23 311 match = (_genstr_date_regexp1.search(timestr) or 312 _genstr_date_regexp2.search(timestr) or 313 _genstr_date_regexp3.search(timestr)) 314 if not match: 315 error() 316 timestr = u"%s-%02d-%02dT00:00:00%s" % (match.group(u'year'), 317 int(match.group(u'month')), 318 int(match.group(u'day')), 319 gettzd(0)) 320 t = stringtotime(timestr) 321 if t: 322 return t 323 else: 324 error() 325