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