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"""Manage temporary files"""
23
24from __future__ import print_function
25from future import standard_library
26standard_library.install_aliases()
27from builtins import object
28
29import os
30import sys
31import shutil
32
33from duplicity import log
34from duplicity import path
35from duplicity import file_naming
36from duplicity import tempdir
37from duplicity import config
38from duplicity import gpg
39
40
41def new_temppath():
42    u"""
43    Return a new TempPath
44    """
45    filename = tempdir.default().mktemp()
46    return TempPath(filename)
47
48
49class TempPath(path.Path):
50    u"""
51    Path object used as a temporary file
52    """
53    def delete(self):
54        u"""
55        Forget and delete
56        """
57        path.Path.delete(self)
58        tempdir.default().forget(self.name)
59
60    def open_with_delete(self, mode):
61        u"""
62        Returns a fileobj.  When that is closed, delete file
63        """
64        fh = FileobjHooked(path.Path.open(self, mode))
65        fh.addhook(self.delete)
66        return fh
67
68
69def get_fileobj_duppath(dirpath, partname, permname, remname, overwrite=False):
70    u"""
71    Return a file object open for writing, will write to filename
72
73    Data will be processed and written to a temporary file.  When the
74    return fileobject is closed, rename to final position.  filename
75    must be a recognizable duplicity data file.
76    """
77    if not config.restart:
78        td = tempdir.TemporaryDirectory(dirpath.name)
79        tdpname = td.mktemp()
80        tdp = TempDupPath(tdpname, parseresults=file_naming.parse(partname))
81        fh = FileobjHooked(tdp.filtered_open(u"wb"), tdp=tdp, dirpath=dirpath,
82                           partname=partname, permname=permname, remname=remname)
83    else:
84        dp = path.DupPath(dirpath.name, index=(partname,))
85        mode = u"ab"
86        if overwrite:
87            mode = u"wb"
88        fh = FileobjHooked(dp.filtered_open(mode), tdp=None, dirpath=dirpath,
89                           partname=partname, permname=permname, remname=remname)
90
91    def rename_and_forget():
92        tdp.rename(dirpath.append(partname))
93        td.forget(tdpname)
94
95    if not config.restart:
96        fh.addhook(rename_and_forget)
97
98    return fh
99
100
101def new_tempduppath(parseresults):
102    u"""
103    Return a new TempDupPath, using settings from parseresults
104    """
105    filename = tempdir.default().mktemp()
106    return TempDupPath(filename, parseresults=parseresults)
107
108
109class TempDupPath(path.DupPath):
110    u"""
111    Like TempPath, but build around DupPath
112    """
113    def delete(self):
114        u"""
115        Forget and delete
116        """
117        path.DupPath.delete(self)
118        tempdir.default().forget(self.name)
119
120    def filtered_open_with_delete(self, mode):
121        u"""
122        Returns a filtered fileobj.  When that is closed, delete file
123        """
124        fh = FileobjHooked(path.DupPath.filtered_open(self, mode))
125        fh.addhook(self.delete)
126        return fh
127
128    def open_with_delete(self, mode=u"rb"):
129        u"""
130        Returns a fileobj.  When that is closed, delete file
131        """
132        assert mode == u"rb"  # Why write a file and then close it immediately?
133        fh = FileobjHooked(path.DupPath.open(self, mode))
134        fh.addhook(self.delete)
135        return fh
136
137
138class FileobjHooked(object):
139    u"""
140    Simulate a file, but add hook on close
141    """
142    def __init__(self, fileobj, tdp=None, dirpath=None,
143                 partname=None, permname=None, remname=None):
144        u"""
145        Initializer.  fileobj is the file object to simulate
146        """
147        self.fileobj = fileobj  # the actual file object
148        self.closed = False  # True if closed
149        self.hooklist = []  # filled later with thunks to run on close
150        self.tdp = tdp  # TempDupPath object
151        self.dirpath = dirpath  # path to directory
152        self.partname = partname  # partial filename
153        self.permname = permname  # permanent filename
154        self.remname = remname  # remote filename
155
156    def write(self, buf):
157        u"""
158        Write fileobj, return result of write()
159        """
160        return self.fileobj.write(buf)
161
162    def flush(self):
163        u"""
164        Flush fileobj and force sync.
165        """
166        self.fileobj.flush()
167        os.fsync(self.fileobj.fileno())
168
169    def to_partial(self):
170        u"""
171        We have achieved the first checkpoint, make file visible and permanent.
172        """
173        assert not config.restart
174        self.tdp.rename(self.dirpath.append(self.partname))
175        self.fileobj.flush()
176        del self.hooklist[0]
177
178    def to_remote(self):
179        u"""
180        We have written the last checkpoint, now encrypt or compress
181        and send a copy of it to the remote for final storage.
182        """
183        pr = file_naming.parse(self.remname)
184        src = self.dirpath.append(self.partname)
185        tgt = self.dirpath.append(self.remname)
186        src_iter = SrcIter(src)
187        if pr.compressed:
188            gpg.GzipWriteFile(src_iter, tgt.name, size=sys.maxsize)
189        elif pr.encrypted:
190            gpg.GPGWriteFile(src_iter, tgt.name, config.gpg_profile, size=sys.maxsize)
191        else:
192            shutil.copyfile(src.name, tgt.name)
193        config.backend.move(tgt)
194
195    def to_final(self):
196        u"""
197        We are finished, rename to final, gzip if needed.
198        """
199        src = self.dirpath.append(self.partname)
200        tgt = self.dirpath.append(self.permname)
201        src_iter = SrcIter(src)
202        pr = file_naming.parse(self.permname)
203        if pr.compressed:
204            gpg.GzipWriteFile(src_iter, tgt.name, size=sys.maxsize)
205            os.unlink(src.name)
206        else:
207            os.rename(src.name, tgt.name)
208
209    def read(self, length=-1):
210        u"""
211        Read fileobj, return result of read()
212        """
213        return self.fileobj.read(length)
214
215    def tell(self):
216        u"""
217        Returns current location of fileobj
218        """
219        return self.fileobj.tell()
220
221    def seek(self, offset):
222        u"""
223        Seeks to a location of fileobj
224        """
225        return self.fileobj.seek(offset)
226
227    def close(self):
228        u"""
229        Close fileobj, running hooks right afterwards
230        """
231        assert not self.fileobj.close()
232        for hook in self.hooklist:
233            hook()
234
235    def addhook(self, hook):
236        u"""
237        Add hook (function taking no arguments) to run upon closing
238        """
239        self.hooklist.append(hook)
240
241    def get_name(self):
242        u"""
243        Return the name of the file
244        """
245        return self.fileobj.name
246
247    name = property(get_name)
248
249
250class Block(object):
251    u"""
252    Data block to return from SrcIter
253    """
254    def __init__(self, data):
255        self.data = data
256
257
258class SrcIter(object):
259    u"""
260    Iterate over source and return Block of data.
261    """
262    def __init__(self, src):
263        self.src = src
264        self.fp = src.open(u"rb")
265
266    def __next__(self):
267        try:
268            res = Block(self.fp.read(self.get_read_size()))
269        except Exception:
270            log.FatalError(_(u"Failed to read %s: %s") %
271                           (self.src.uc_name, sys.exc_info()),
272                           log.ErrorCode.generic)
273        if not res.data:
274            self.fp.close()
275            raise StopIteration
276        return res
277
278    def get_read_size(self):
279        return 128 * 1024
280
281    def get_footer(self):
282        return b""
283