Welcome, guest | Sign In | My Account | Store | Cart

A class that enables a client to securely update an existing file, including the ability to make an automated backup version.

Python, 114 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import os, string

def replaceFile(oldname, newname):
    """ Rename file 'oldname' to 'newname'.
    """
    if os.name == 'nt' and os.path.exists(oldname):
        # POSIX rename does an atomic replace, WIN32 rename does not. :-(
        try:
            os.remove(newname)
        except OSError, exc:
            import errno
            if exc.errno != errno.ENOENT: raise exc

    # rename it
    os.rename(oldname, newname)


class FileMorpher:
    """ A class that enables a client to securely update an existing file,
        including the ability to make an automated backup version.
    """

    def __init__(self, filename, **kw):
        """ The constructor takes the filename and some options.

            backup -- boolean indicating whether you want a backup file
                (default is yes)
        """
        self.filename = filename
        self.do_backup = kw.get('backup', 1)

        self.stream = None
        self.basename, ext = os.path.splitext(self.filename)


    def __del__(self):
        if self.stream:
            # Remove open temp file
            self.__close()
            os.remove(self.__tempfile())


    def __tempfile(self):
        return self.basename + ".tmp"


    def __close(self):
        """ Close temp stream, if open.
        """
        if self.stream:
            self.stream.close()
            self.stream = None


    def load(self):
        """ Load the content of the original file into a string and
            return it. All I/O exceptions are passed through.
        """
        file = open(self.filename, "rt")
        try:
            content = file.read()
        finally:
            file.close()

        return content


    def save(self, content):
        """ Save new content, using a temporary file.
        """
        file = self.opentemp()
        file.write(content)
        self.commit()


    def opentemp(self):
        """ Open a temporary file for writing and return an open stream.
        """
        assert not self.stream, "Write stream already open"

        self.stream = open(self.__tempfile(), "wt")

        return self.stream        


    def commit(self):
        """ Close the open temp stream and replace the original file,
            optionally making a backup copy.
        """
        assert self.stream, "Write stream not open"

        # close temp file
        self.__close()

        # do optional backup and rename temp file to the correct name
        if self.do_backup:
            replaceFile(self.filename, self.basename + ".bak")
        replaceFile(self.__tempfile(), self.filename)


if __name__ == "__main__":
    # prepare test file
    test = open('foo.txt', 'wt')
    test.write('\txxx\nline 2\n\t\t2 tabs\n')
    test.close()

    # load the file's content
    file = FileMorpher('foo.txt')
    text = file.load()

    # detab it, this could also be done shorter by file.save()
    stream = file.opentemp()
    stream.write(string.expandtabs(text))
    file.commit()

This code encapsulates the method to safely update files in-place, by first writing to a temporary file and finally renaming the new temporary file to the old filename. This also make it possible to create a backup copy of the original file.

The "FileMorpher" class encapsulates all the necessary steps, including the filename handling, which makes the calling code much cleaner as opposed to the naive approach where all the gory details are spread over that higher-level code.

1 comment

Andreas Kloss 19 years, 7 months ago  # | flag

os.mkstemp(). A well coded recipe. I find only one drawback: Using [filename].tmp as temporary filename might pose a security risk. Use os.mkstemp() instead to get rid of the risk.