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"""
23Provides temporary file handling cenetered around a single top-level
24securely created temporary directory.
25
26The public interface of this module is thread-safe.
27"""
28
29from __future__ import print_function
30from future import standard_library
31standard_library.install_aliases()
32from builtins import object
33
34import os
35import platform
36import subprocess
37import tempfile
38import threading
39
40from duplicity import config
41from duplicity import log
42from duplicity import util
43
44# Set up state related to managing the default temporary directory
45# instance
46_defaultLock = threading.Lock()
47_defaultInstance = None
48# backup the initial tmp dir path because we will force tempfile
49# later to use our generated _defaultInstance.dir() as temproot
50_initialSystemTempRoot = tempfile.gettempdir()
51
52
53def default():
54    u"""
55    Obtain the global default instance of TemporaryDirectory, creating
56    it first if necessary. Failures are propagated to caller. Most
57    callers are expected to use this function rather than
58    instantiating TemporaryDirectory directly, unless they explicitly
59    desdire to have their "own" directory for some reason.
60
61    This function is thread-safe.
62    """
63    global _defaultLock
64    global _defaultInstance
65
66    _defaultLock.acquire()
67    try:
68        if _defaultInstance is None or _defaultInstance.dir() is None:
69            _defaultInstance = TemporaryDirectory(temproot=config.temproot)
70            # set the temp dir to be the default in tempfile module from now on
71            tempfile.tempdir = _defaultInstance.dir()
72        return _defaultInstance
73    finally:
74        _defaultLock.release()
75
76
77class TemporaryDirectory(object):
78    u"""
79    A temporary directory.
80
81    An instance of this class is backed by a directory in the file
82    system created securely by the use of tempfile.mkdtemp(). Said
83    instance can be used to obtain unique filenames inside of this
84    directory for cases where mktemp()-like semantics is desired, or
85    (recommended) an fd,filename pair for mkstemp()-like semantics.
86
87    See further below for the security implications of using it.
88
89    Each instance will keep a list of all files ever created by it, to
90    faciliate deletion of such files and rmdir() of the directory
91    itself. It does this in order to be able to clean out the
92    directory without resorting to a recursive delete (ala rm -rf),
93    which would be risky. Calling code can optionally (recommended)
94    notify an instance of the fact that a tempfile was deleted, and
95    thus need not be kept track of anymore.
96
97    This class serves two primary purposes:
98
99    Firstly, it provides a convenient single top-level directory in
100    which all the clutter ends up, rather than cluttering up the root
101    of the system temp directory itself with many files.
102
103    Secondly, it provides a way to get mktemp() style semantics for
104    temporary file creation, with most of the risks
105    gone. Specifically, since the directory itself is created
106    securely, files in this directory can be (mostly) safely created
107    non-atomically without the usual mktemp() security
108    implications. However, in the presence of tmpwatch, tmpreaper, or
109    similar mechanisms that will cause files in the system tempdir to
110    expire, a security risk is still present because the removal of
111    the TemporaryDirectory managed directory removes all protection it
112    offers.
113
114    For this reason, use of mkstemp() is greatly preferred above use
115    of mktemp().
116
117    In addition, since cleanup is in the form of deletion based on a
118    list of filenames, completely independently of whether someone
119    else already deleted the file, there exists a race here as
120    well. The impact should however be limited to the removal of an
121    'attackers' file.
122    """
123    def __init__(self, temproot=None):
124        u"""
125        Create a new TemporaryDirectory backed by a unique and
126        securely created file system directory.
127
128        tempbase - The temp root directory, or None to use system
129        default (recommended).
130        """
131        def defaults_to_tmp(path):
132            u'''Determine if path point to a MAcOS system tmp'''
133            sys_temps = [
134                os.path.realpath(u"/tmp"),
135                os.path.realpath(u"/var/tmp"),
136            ]
137
138            user_temp = os.path.realpath(path)
139            for sys_temp in sys_temps:
140                if user_temp.startswith(sys_temp):
141                    return True
142            return False
143
144        if temproot is None:
145            if config.temproot:
146                temproot = config.temproot
147            else:
148                global _initialSystemTempRoot
149                temproot = _initialSystemTempRoot
150        if isinstance(temproot, b"".__class__):
151            temproot = util.fsdecode(temproot)
152
153        if (platform.system().startswith(u'Darwin') and defaults_to_tmp(temproot)):
154            # Use temp space from getconf, never /tmp
155            temproot = subprocess.check_output([u'getconf', u'DARWIN_USER_TEMP_DIR'])
156            temproot = util.fsdecode(temproot).rstrip()
157
158        self.__dir = tempfile.mkdtemp(u"-tempdir", u"duplicity-", temproot)
159
160        log.Info(_(u"Using temporary directory %s") % self.__dir)
161
162        # number of mktemp()/mkstemp() calls served so far
163        self.__tempcount = 0
164        # dict of paths pending deletion; use dict even though we are
165        # not concearned with association, because it is unclear whether
166        # sets are O(1), while dictionaries are.
167        self.__pending = {}
168
169        self.__lock = threading.Lock()  # protect private resources *AND* mktemp/mkstemp calls
170
171    def dir(self):
172        u"""
173        Returns the absolute pathname of the temp folder.
174        """
175        return self.__dir
176
177    def __del__(self):
178        u"""
179        Perform cleanup.
180        """
181        global _defaultInstance
182        if _defaultInstance is not None:
183            self.cleanup()
184
185    def mktemp(self):
186        u"""
187        Return a unique filename suitable for use for a temporary
188        file. The file is not created.
189
190        Subsequent calls to this method are guaranteed to never return
191        the same filename again. As a result, it is safe to use under
192        concurrent conditions.
193
194        NOTE: mkstemp() is greatly preferred.
195        """
196        filename = None
197
198        self.__lock.acquire()
199        try:
200            self.__tempcount = self.__tempcount + 1
201            suffix = u"-%d" % (self.__tempcount,)
202            filename = util.fsencode(tempfile.mktemp(suffix, u"mktemp-", self.__dir))
203
204            log.Debug(_(u"Registering (mktemp) temporary file %s") % util.fsdecode(filename))
205            self.__pending[filename] = None
206        finally:
207            self.__lock.release()
208
209        return filename
210
211    def mkstemp(self):
212        u"""
213        Returns a filedescriptor and a filename, as per os.mkstemp(),
214        but located in the temporary directory and subject to tracking
215        and automatic cleanup.
216        """
217        fd = None
218        filename = None
219
220        self.__lock.acquire()
221        try:
222            self.__tempcount = self.__tempcount + 1
223            suffix = u"-%d" % (self.__tempcount,)
224            fd, filename = tempfile.mkstemp(suffix, u"mkstemp-", self.__dir,)
225
226            log.Debug(_(u"Registering (mkstemp) temporary file %s") % filename)
227            self.__pending[filename] = None
228        finally:
229            self.__lock.release()
230
231        return fd, filename
232
233    def mkstemp_file(self):
234        u"""
235        Convenience wrapper around mkstemp(), with the file descriptor
236        converted into a file object.
237        """
238        fd, filename = self.mkstemp()
239
240        return os.fdopen(fd, u"r+"), filename
241
242    def forget(self, fname):
243        u"""
244        Forget about the given filename previously obtained through
245        mktemp() or mkstemp(). This should be called *after* the file
246        has been deleted, to stop a future cleanup() from trying to
247        delete it.
248
249        Forgetting is only needed for scaling purposes; that is, to
250        avoid n timefile creations from implying that n filenames are
251        kept in memory. Typically this whould never matter in
252        duplicity, but for niceness sake callers are recommended to
253        use this method whenever possible.
254        """
255        self.__lock.acquire()
256        try:
257            if fname in self.__pending:
258                log.Debug(_(u"Forgetting temporary file %s") % util.fsdecode(fname))
259                del(self.__pending[fname])
260            else:
261                log.Warn(_(u"Attempt to forget unknown tempfile %s - this is probably a bug.") % util.fsdecode(fname))
262                pass
263        finally:
264            self.__lock.release()
265
266    def cleanup(self):
267        u"""
268        Cleanup any files created in the temporary directory (that
269        have not been forgotten), and clean up the temporary directory
270        itself.
271
272        On failure they are logged, but this method will not raise an
273        exception.
274        """
275        self.__lock.acquire()
276        try:
277            if self.__dir is not None:
278                for file in list(self.__pending.keys()):
279                    try:
280                        log.Debug(_(u"Removing still remembered temporary file %s") % util.fsdecode(file))
281                        util.ignore_missing(os.unlink, file)
282                    except Exception:
283                        log.Info(_(u"Cleanup of temporary file %s failed") % util.fsdecode(file))
284                        pass
285                try:
286                    os.rmdir(self.__dir)
287                except Exception:
288                    log.Warn(_(u"Cleanup of temporary directory %s failed - "
289                               u"this is probably a bug.") % util.fsdecode(self.__dir))
290                    pass
291                self.__pending = None
292                self.__dir = None
293        finally:
294            self.__lock.release()
295