~launchpad-pqm/launchpad/devel

12094.2.1 by Henning Eggers
Moved tarfile helper into services.
1
# Copyright 2010 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""Helpers to work with tar files more easily."""
5
6
__metaclass__ = type
7
8
__all__ = [
9
    'LaunchpadWriteTarFile',
10
    ]
11
12
import os
13
from StringIO import StringIO
14
import tarfile
15
import tempfile
16
import time
17
18
# A note about tarballs, StringIO and unicode. SQLObject returns unicode
19
# values for columns which are declared as StringCol. We have to be careful
20
# not to pass unicode instances to the tarfile module, because when the
21
# tarfile's filehandle is a StringIO object, the StringIO object gets upset
22
# later when we ask it for its value and it tries to join together its
23
# buffers. This is why the tarball code is sprinkled with ".encode('ascii')".
24
# If we get separate StringCol and UnicodeCol column types, we won't need this
25
# any longer.
26
# -- Dafydd Harries, 2005-04-07.
27
28
class LaunchpadWriteTarFile:
29
    """Convenience wrapper around the tarfile module.
30
31
    This class makes it convenient to generate tar files in various ways.
32
    """
33
34
    def __init__(self, stream):
35
        self.tarfile = tarfile.open('', 'w:gz', stream)
36
        self.closed = False
37
38
    @classmethod
39
    def files_to_stream(cls, files):
40
        """Turn a dictionary of files into a data stream."""
41
        buffer = tempfile.TemporaryFile()
42
        archive = cls(buffer)
43
        archive.add_files(files)
44
        archive.close()
45
        buffer.seek(0)
46
        return buffer
47
48
    @classmethod
49
    def files_to_string(cls, files):
50
        """Turn a dictionary of files into a data string."""
51
        return cls.files_to_stream(files).read()
52
53
    @classmethod
54
    def files_to_tarfile(cls, files):
55
        """Turn a dictionary of files into a tarfile object."""
56
        return tarfile.open('', 'r', cls.files_to_stream(files))
57
58
    def close(self):
59
        """Close the archive.
60
61
        After the archive is closed, the data written to the filehandle will
62
        be complete. The archive may not be appended to after it has been
63
        closed.
64
        """
65
66
        self.tarfile.close()
67
        self.closed = True
68
69
    def add_file(self, path, contents):
70
        """Add a file to the archive."""
71
        assert not self.closed, "Can't add a file to a closed archive"
72
73
        now = int(time.time())
74
        path_bits = path.split(os.path.sep)
75
76
        # Ensure that all the directories in the path are present in the
77
        # archive.
78
        for i in range(1, len(path_bits)):
79
            joined_path = os.path.join(*path_bits[:i])
80
81
            try:
82
                self.tarfile.getmember(joined_path)
83
            except KeyError:
84
                tarinfo = tarfile.TarInfo(joined_path)
85
                tarinfo.type = tarfile.DIRTYPE
86
                tarinfo.mtime = now
87
                tarinfo.mode = 0755
88
                tarinfo.uname = 'launchpad'
89
                tarinfo.gname = 'launchpad'
90
                self.tarfile.addfile(tarinfo)
91
92
        tarinfo = tarfile.TarInfo(path)
93
        tarinfo.time = now
94
        tarinfo.mtime = now
95
        tarinfo.mode = 0644
96
        tarinfo.size = len(contents)
97
        tarinfo.uname = 'launchpad'
98
        tarinfo.gname = 'launchpad'
99
        self.tarfile.addfile(tarinfo, StringIO(contents))
100
101
    def add_files(self, files):
102
        """Add a number of files to the archive.
103
104
        :param files: A dictionary mapping file names to file contents.
105
        """
106
107
        for filename in sorted(files.keys()):
108
            self.add_file(filename, files[filename])