Source code for duplicity.librsync

# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf-8 -*-
#
# Copyright 2002 Ben Escoto <ben@emerose.org>
# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
#
# This file is part of duplicity.
#
# duplicity is free software; you can redistribute it and/or modify
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# duplicity is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

u"""Provides a high-level interface to some librsync functions

This is a python wrapper around the lower-level _librsync module,
which is written in C.  The goal was to use C as little as possible...

"""

from builtins import object
from builtins import str

import array
import os
import sys

from . import _librsync

if os.environ.get(u'READTHEDOCS') == u'True':
    import unittest.mock as mock  # pylint: disable=import-error
    import duplicity
    duplicity._librsync = mock.MagicMock()

blocksize = _librsync.RS_JOB_BLOCKSIZE


[docs]class librsyncError(Exception): u"""Signifies error in internal librsync processing (bad signature, etc.) underlying _librsync.librsyncError's are regenerated using this class because the C-created exceptions are by default unPickleable. There is probably a way to fix this in _librsync, but this scheme was easier. """ pass
[docs]class LikeFile(object): u"""File-like object used by SigFile, DeltaFile, and PatchFile""" mode = u"rb" # This will be replaced in subclasses by an object with # appropriate cycle() method maker = None
[docs] def __init__(self, infile, need_seek=None): u"""LikeFile initializer - zero buffers, set eofs off""" self.check_file(infile, need_seek) self.infile = infile self.closed = self.infile_closed = None self.inbuf = b"" self.outbuf = array.array(u'b') self.eof = self.infile_eof = None
[docs] def check_file(self, file, need_seek=None): u"""Raise type error if file doesn't have necessary attributes""" if not hasattr(file, u"read"): raise TypeError(u"Basis file must have a read() method") if not hasattr(file, u"close"): raise TypeError(u"Basis file must have a close() method") if need_seek and not hasattr(file, u"seek"): raise TypeError(u"Basis file must have a seek() method")
[docs] def read(self, length=-1): u"""Build up self.outbuf, return first length bytes""" if length == -1: while not self.eof: self._add_to_outbuf_once() real_len = len(self.outbuf) else: while not self.eof and len(self.outbuf) < length: self._add_to_outbuf_once() real_len = min(length, len(self.outbuf)) if sys.version_info.major >= 3: return_val = self.outbuf[:real_len].tobytes() else: return_val = self.outbuf[:real_len].tostring() del self.outbuf[:real_len] return return_val
[docs] def _add_to_outbuf_once(self): u"""Add one cycle's worth of output to self.outbuf""" if not self.infile_eof: self._add_to_inbuf() try: self.eof, len_inbuf_read, cycle_out = self.maker.cycle(self.inbuf) except _librsync.librsyncError as e: raise librsyncError(str(e)) self.inbuf = self.inbuf[len_inbuf_read:] if sys.version_info.major >= 3: self.outbuf.frombytes(cycle_out) else: self.outbuf.fromstring(cycle_out)
[docs] def _add_to_inbuf(self): u"""Make sure len(self.inbuf) >= blocksize""" assert not self.infile_eof while len(self.inbuf) < blocksize: new_in = self.infile.read(blocksize) if not new_in: self.infile_eof = 1 assert not self.infile.close() self.infile_closed = 1 break self.inbuf += new_in
[docs] def close(self): u"""Close infile""" if not self.infile_closed: assert not self.infile.close() self.closed = 1
[docs]class SigFile(LikeFile): u"""File-like object which incrementally generates a librsync signature"""
[docs] def __init__(self, infile, blocksize=_librsync.RS_DEFAULT_BLOCK_LEN): u"""SigFile initializer - takes basis file basis file only needs to have read() and close() methods. It will be closed when we come to the end of the signature. """ LikeFile.__init__(self, infile) try: self.maker = _librsync.new_sigmaker(blocksize) except _librsync.librsyncError as e: raise librsyncError(str(e))
[docs]class DeltaFile(LikeFile): u"""File-like object which incrementally generates a librsync delta"""
[docs] def __init__(self, signature, new_file): u"""DeltaFile initializer - call with signature and new file Signature can either be a string or a file with read() and close() methods. New_file also only needs to have read() and close() methods. It will be closed when self is closed. """ LikeFile.__init__(self, new_file) if isinstance(signature, bytes): sig_string = signature else: self.check_file(signature) sig_string = signature.read() assert not signature.close() try: self.maker = _librsync.new_deltamaker(sig_string) except _librsync.librsyncError as e: raise librsyncError(str(e))
[docs]class PatchedFile(LikeFile): u"""File-like object which applies a librsync delta incrementally"""
[docs] def __init__(self, basis_file, delta_file): u"""PatchedFile initializer - call with basis delta Here basis_file must be a true Python file, because we may need to seek() around in it a lot, and this is done in C. delta_file only needs read() and close() methods. """ LikeFile.__init__(self, delta_file) try: basis_file.fileno() except: u""" tempfile.TemporaryFile() only guarantees a true file object on posix platforms. on cygwin/windows a file-like object whose file attribute is the underlying true file object is returned. """ if hasattr(basis_file, u'file') and hasattr(basis_file.file, u'fileno'): basis_file = basis_file.file else: raise TypeError(_(u"basis_file must be a (true) file or an object whose " u"file attribute is the underlying true file object")) try: self.maker = _librsync.new_patchmaker(basis_file) except _librsync.librsyncError as e: raise librsyncError(str(e))
[docs]class SigGenerator(object): u"""Calculate signature. Input and output is same as SigFile, but the interface is like md5 module, not filelike object """
[docs] def __init__(self, blocksize=_librsync.RS_DEFAULT_BLOCK_LEN): u"""Return new signature instance""" try: self.sig_maker = _librsync.new_sigmaker(blocksize) except _librsync.librsyncError as e: raise librsyncError(str(e)) self.gotsig = None self.buffer = b"" self.sigstring_list = []
[docs] def update(self, buf): u"""Add buf to data that signature will be calculated over""" if self.gotsig: raise librsyncError(u"SigGenerator already provided signature") self.buffer += buf while len(self.buffer) >= blocksize: if self.process_buffer(): raise librsyncError(u"Premature EOF received from sig_maker")
[docs] def process_buffer(self): u"""Run self.buffer through sig_maker, add to self.sig_string""" try: eof, len_buf_read, cycle_out = self.sig_maker.cycle(self.buffer) except _librsync.librsyncError as e: raise librsyncError(str(e)) self.buffer = self.buffer[len_buf_read:] self.sigstring_list.append(cycle_out) return eof
[docs] def getsig(self): u"""Return signature over given data""" while not self.process_buffer(): pass # keep running until eof return b''.join(self.sigstring_list)