mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-11-04 02:55:08 -05:00
759 lines
23 KiB
Python
759 lines
23 KiB
Python
import hashlib
|
|
import os
|
|
import socket
|
|
import struct
|
|
|
|
MPLEX_BASE = 7
|
|
MSG_DATA = 0
|
|
MSG_ERROR_XFER = 1
|
|
MSG_INFO = 2
|
|
MSG_ERROR = 3
|
|
MSG_WARNING = 4
|
|
MSG_DELETED = 101
|
|
MSG_NO_SEND = 102
|
|
|
|
XMIT_TOP_DIR = 1 << 0
|
|
XMIT_SAME_MODE = 1 << 1
|
|
XMIT_EXTENDED_FLAGS = 1 << 2
|
|
XMIT_SAME_UID = 1 << 3
|
|
XMIT_SAME_GID = 1 << 4
|
|
XMIT_SAME_NAME = 1 << 5
|
|
XMIT_LONG_NAME = 1 << 6
|
|
XMIT_SAME_TIME = 1 << 7
|
|
XMIT_SAME_RDEV_MAJOR = 1 << 8
|
|
XMIT_NO_CONTENT_DIR = 1 << 8
|
|
XMIT_HLINKED = 1 << 9
|
|
XMIT_USER_NAME_FOLLOWS = 1 << 10
|
|
XMIT_GROUP_NAME_FOLLOWS = 1 << 11
|
|
XMIT_HLINK_FIRST = 1 << 12
|
|
XMIT_MOD_NSEC = 1 << 13
|
|
|
|
S_IFREG = 0o100000
|
|
S_IFDIR = 0o040000
|
|
S_IFLNK = 0o120000
|
|
S_IFMT = 0o170000
|
|
|
|
def S_ISREG(m):
|
|
return (m & S_IFMT) == S_IFREG
|
|
|
|
def S_ISDIR(m):
|
|
return (m & S_IFMT) == S_IFDIR
|
|
|
|
def S_ISLNK(m):
|
|
return (m & S_IFMT) == S_IFLNK
|
|
|
|
DEFAULT_PROTOCOL = 30
|
|
|
|
NDX_DONE = -1
|
|
NDX_FLIST_EOF = -2
|
|
NDX_FLIST_OFFSET = -101
|
|
|
|
ITEM_BASIS_TYPE_FOLLOWS = 1 << 11
|
|
ITEM_XNAME_FOLLOWS = 1 << 12
|
|
ITEM_IS_NEW = 1 << 13
|
|
ITEM_TRANSFER = 1 << 15
|
|
FNAMECMP_FNAME = 0x80
|
|
FNAMECMP_FUZZY = 0x83
|
|
|
|
CHUNK_SIZE = 32 * 1024
|
|
|
|
def w_byte(x):
|
|
return bytes([x & 0xFF])
|
|
|
|
def w_shortint(x):
|
|
return struct.pack('<H', x & 0xFFFF)
|
|
|
|
def w_int(x):
|
|
return struct.pack('<i', _s32(x))
|
|
|
|
def _s32(x):
|
|
x &= 0xFFFFFFFF
|
|
return x - (1 << 32) if x & 0x80000000 else x
|
|
|
|
def w_varint(x):
|
|
b = bytearray(5)
|
|
v = x & 0xFFFFFFFF
|
|
b[1] = v & 0xFF
|
|
b[2] = (v >> 8) & 0xFF
|
|
b[3] = (v >> 16) & 0xFF
|
|
b[4] = (v >> 24) & 0xFF
|
|
cnt = 4
|
|
while cnt > 1 and b[cnt] == 0:
|
|
cnt -= 1
|
|
bit = 1 << (7 - cnt + 1)
|
|
if b[cnt] >= bit:
|
|
cnt += 1
|
|
b[0] = (~(bit - 1)) & 0xFF
|
|
elif cnt > 1:
|
|
b[0] = (b[cnt] | ((~(bit * 2 - 1)) & 0xFF)) & 0xFF
|
|
else:
|
|
b[0] = b[1]
|
|
return bytes(b[:cnt])
|
|
|
|
def w_varlong(x, min_bytes):
|
|
b = bytearray(9)
|
|
v = x & ((1 << 64) - 1)
|
|
for i in range(8):
|
|
b[1 + i] = (v >> (8 * i)) & 0xFF
|
|
cnt = 8
|
|
while cnt > min_bytes and b[cnt] == 0:
|
|
cnt -= 1
|
|
bit = 1 << (7 - cnt + min_bytes)
|
|
if b[cnt] >= bit:
|
|
cnt += 1
|
|
b[0] = (~(bit - 1)) & 0xFF
|
|
elif cnt > min_bytes:
|
|
b[0] = (b[cnt] | ((~(bit * 2 - 1)) & 0xFF)) & 0xFF
|
|
else:
|
|
b[0] = b[cnt]
|
|
return bytes(b[:cnt])
|
|
|
|
def w_varint30(x, protocol=DEFAULT_PROTOCOL):
|
|
return w_varint(x) if protocol >= 30 else w_int(x)
|
|
|
|
def w_varlong30(x, min_bytes, protocol=DEFAULT_PROTOCOL):
|
|
return w_varlong(x, min_bytes) if protocol >= 30 else w_int(x)
|
|
|
|
_INT_BYTE_EXTRA = ([0] * 32) + ([1] * 16) + ([2] * 8) + ([3] * 4) + ([4] * 2) + [5, 6]
|
|
|
|
def _read_varint(read):
|
|
first = read(1)[0]
|
|
extra = _INT_BYTE_EXTRA[first >> 2]
|
|
value = bytearray(5)
|
|
if extra:
|
|
bit = 1 << (8 - extra)
|
|
value[:extra] = read(extra)
|
|
value[extra] = first & (bit - 1)
|
|
else:
|
|
value[0] = first
|
|
return value[0] | (value[1] << 8) | (value[2] << 16) | (value[3] << 24)
|
|
|
|
def to_wire_mode(mode):
|
|
return mode
|
|
|
|
def w_sum_head(count, blength, s2length, remainder):
|
|
return w_int(count) + w_int(blength) + w_int(s2length) + w_int(remainder)
|
|
|
|
def get_checksum1(buf):
|
|
if isinstance(buf, str):
|
|
buf = buf.encode()
|
|
sb = [c - 256 if c >= 128 else c for c in buf]
|
|
n = len(sb)
|
|
s1 = s2 = 0
|
|
i = 0
|
|
while i < n - 4:
|
|
s2 = (s2 + 4 * (s1 + sb[i]) + 3 * sb[i + 1] + 2 * sb[i + 2] + sb[i + 3]) & 0xFFFFFFFF
|
|
s1 = (s1 + sb[i] + sb[i + 1] + sb[i + 2] + sb[i + 3]) & 0xFFFFFFFF
|
|
i += 4
|
|
while i < n:
|
|
s1 = (s1 + sb[i]) & 0xFFFFFFFF
|
|
s2 = (s2 + s1) & 0xFFFFFFFF
|
|
i += 1
|
|
return ((s1 & 0xFFFF) + ((s2 & 0xFFFF) << 16)) & 0xFFFFFFFF
|
|
|
|
def w_vstring(s):
|
|
if isinstance(s, str):
|
|
s = s.encode()
|
|
n = len(s)
|
|
if n > 0x7F:
|
|
return bytes([n // 0x100 + 0x80, n & 0xFF]) + s
|
|
return bytes([n]) + s
|
|
|
|
_PERM_BITS = [
|
|
(S_IFDIR, 'd'), (S_IFLNK, 'l'), (0o020000, 'c'), (0o060000, 'b'),
|
|
(0o010000, 'p'), (0o140000, 's'),
|
|
]
|
|
|
|
def sort_key(entry):
|
|
return entry.name + (b'/' if entry.is_dir else b'')
|
|
|
|
def sort_entries(entries):
|
|
return sorted(entries, key=sort_key)
|
|
|
|
def mode_to_perms(mode):
|
|
typ = '-'
|
|
for bits, ch in _PERM_BITS:
|
|
if (mode & S_IFMT) == bits:
|
|
typ = ch
|
|
break
|
|
out = [typ]
|
|
for who in (6, 3, 0):
|
|
out.append('r' if mode & (4 << who) else '-')
|
|
out.append('w' if mode & (2 << who) else '-')
|
|
out.append('x' if mode & (1 << who) else '-')
|
|
return ''.join(out)
|
|
|
|
def xattr_list_wire(items):
|
|
out = bytearray(w_varint(0) + w_varint(len(items)))
|
|
for name, datum_len, datum in items:
|
|
if isinstance(name, str):
|
|
name = name.encode()
|
|
out += w_varint(len(name)) + w_varint(datum_len) + name + datum
|
|
return bytes(out)
|
|
|
|
class FileEntry:
|
|
def __init__(self, name, *, mode=S_IFREG | 0o644, length=0, modtime=1700000000,
|
|
csum=None, extra_flags=0, protocol=DEFAULT_PROTOCOL, raw=None,
|
|
hlink_ndx=None, uid=None, user_name=None):
|
|
self.name = name.encode() if isinstance(name, str) else name
|
|
self.mode = mode
|
|
self.length = length
|
|
self.modtime = modtime
|
|
self.csum = csum
|
|
self.extra_flags = extra_flags
|
|
self.protocol = protocol
|
|
self.raw = raw
|
|
|
|
self.hlink_ndx = hlink_ndx
|
|
|
|
self.uid = uid
|
|
self.user_name = (user_name.encode() if isinstance(user_name, str)
|
|
else user_name)
|
|
|
|
def encode(self):
|
|
if self.raw is not None:
|
|
return self.raw
|
|
|
|
is_reg = (self.mode & 0o170000) == S_IFREG
|
|
is_dir = (self.mode & 0o170000) == S_IFDIR
|
|
|
|
xflags = XMIT_SAME_UID | XMIT_SAME_GID
|
|
xflags |= self.extra_flags
|
|
if self.hlink_ndx is not None:
|
|
xflags |= XMIT_HLINKED
|
|
if self.uid is not None:
|
|
xflags &= ~XMIT_SAME_UID
|
|
if self.user_name is not None:
|
|
xflags |= XMIT_USER_NAME_FOLLOWS
|
|
|
|
out = bytearray()
|
|
|
|
if not xflags and not is_dir:
|
|
xflags |= XMIT_TOP_DIR
|
|
if (xflags & 0xFF00) or not xflags:
|
|
xflags |= XMIT_EXTENDED_FLAGS
|
|
out += w_shortint(xflags)
|
|
else:
|
|
out += w_byte(xflags)
|
|
|
|
l2 = len(self.name)
|
|
if xflags & XMIT_LONG_NAME:
|
|
out += w_varint30(l2, self.protocol)
|
|
else:
|
|
out += w_byte(l2)
|
|
out += self.name
|
|
|
|
if self.hlink_ndx is not None:
|
|
out += w_varint(self.hlink_ndx)
|
|
|
|
out += w_varlong30(self.length, 3, self.protocol)
|
|
if not (xflags & XMIT_SAME_TIME):
|
|
out += w_varlong(self.modtime, 4) if self.protocol >= 30 else w_int(self.modtime)
|
|
if not (xflags & XMIT_SAME_MODE):
|
|
out += w_int(to_wire_mode(self.mode))
|
|
|
|
if self.uid is not None:
|
|
out += w_varint(self.uid)
|
|
if self.user_name is not None:
|
|
out += w_byte(len(self.user_name)) + self.user_name
|
|
|
|
if self.csum is not None:
|
|
out += self.csum
|
|
|
|
return bytes(out)
|
|
|
|
def end_of_flist(io_error=0, protocol=DEFAULT_PROTOCOL):
|
|
return w_byte(0)
|
|
|
|
class ProtocolError(Exception):
|
|
pass
|
|
|
|
class ParsedEntry:
|
|
__slots__ = ('name', 'mode', 'length', 'mtime', 'link_target')
|
|
|
|
def __init__(self, name, mode, length, mtime, link_target=None):
|
|
self.name = name
|
|
self.mode = mode
|
|
self.length = length
|
|
self.mtime = mtime
|
|
self.link_target = link_target
|
|
|
|
@property
|
|
def is_reg(self):
|
|
return S_ISREG(self.mode)
|
|
|
|
@property
|
|
def is_dir(self):
|
|
return S_ISDIR(self.mode)
|
|
|
|
@property
|
|
def is_link(self):
|
|
return S_ISLNK(self.mode)
|
|
|
|
def __repr__(self):
|
|
return f"ParsedEntry({self.name!r}, mode={self.mode:o}, length={self.length})"
|
|
|
|
class _SocketPeer:
|
|
def __init__(self, sock):
|
|
self.sock = sock
|
|
self._rbuf = b''
|
|
self._ndx_prev_positive = -1
|
|
self._ndx_prev_negative = 1
|
|
|
|
def _recv_exact(self, n):
|
|
data = b''
|
|
while len(data) < n:
|
|
if self._rbuf:
|
|
take = self._rbuf[:n - len(data)]
|
|
self._rbuf = self._rbuf[len(take):]
|
|
data += take
|
|
continue
|
|
chunk = self.sock.recv(n - len(data))
|
|
if not chunk:
|
|
raise ProtocolError(f"EOF after {len(data)}/{n} bytes")
|
|
data += chunk
|
|
return data
|
|
|
|
def _readline(self):
|
|
line = b''
|
|
while not line.endswith(b'\n'):
|
|
if self._rbuf:
|
|
c, self._rbuf = self._rbuf[:1], self._rbuf[1:]
|
|
else:
|
|
c = self.sock.recv(1)
|
|
if not c:
|
|
raise ProtocolError("EOF reading a line")
|
|
line += c
|
|
return line.decode('latin-1').rstrip('\n')
|
|
|
|
def _send_raw(self, data):
|
|
self.sock.sendall(data)
|
|
|
|
def _frame(self, code, payload):
|
|
return struct.pack('<I', ((MPLEX_BASE + code) << 24) | len(payload)) + payload
|
|
|
|
def send_data(self, payload):
|
|
self._send_raw(self._frame(MSG_DATA, payload))
|
|
|
|
def w_ndx(self, ndx):
|
|
if ndx == NDX_DONE:
|
|
return b'\x00'
|
|
data = bytearray()
|
|
if ndx >= 0:
|
|
diff = ndx - self._ndx_prev_positive
|
|
self._ndx_prev_positive = ndx
|
|
absolute = ndx
|
|
else:
|
|
data.append(0xFF)
|
|
absolute = -ndx
|
|
diff = absolute - self._ndx_prev_negative
|
|
self._ndx_prev_negative = absolute
|
|
if 0 < diff < 0xFE:
|
|
data.append(diff)
|
|
elif diff < 0 or diff > 0x7FFF:
|
|
data += bytes([0xFE, ((absolute >> 24) | 0x80) & 0xFF, absolute & 0xFF,
|
|
(absolute >> 8) & 0xFF, (absolute >> 16) & 0xFF])
|
|
else:
|
|
data += bytes([0xFE, (diff >> 8) & 0xFF, diff & 0xFF])
|
|
return bytes(data)
|
|
|
|
def close(self):
|
|
try:
|
|
self.sock.close()
|
|
except OSError:
|
|
pass
|
|
|
|
class DaemonClient(_SocketPeer):
|
|
def __init__(self, host, port, timeout=10):
|
|
sock = socket.create_connection((host, port), timeout=timeout)
|
|
self._initialise(sock, timeout)
|
|
|
|
@classmethod
|
|
def from_socket(cls, sock, timeout=10):
|
|
client = cls.__new__(cls)
|
|
client._initialise(sock, timeout)
|
|
return client
|
|
|
|
def _initialise(self, sock, timeout):
|
|
sock.settimeout(timeout)
|
|
super().__init__(sock)
|
|
self.protocol = DEFAULT_PROTOCOL
|
|
self.compat_flags = None
|
|
self.seed = None
|
|
self.xfer_sum_len = 16
|
|
self._mux_in = b''
|
|
self._r_ndx_prev_positive = -1
|
|
self._r_ndx_prev_negative = 1
|
|
self.messages = []
|
|
|
|
def _r_int(self):
|
|
return _s32(int.from_bytes(self._recv_exact(4), 'little'))
|
|
|
|
def _r_varint(self):
|
|
return _read_varint(self._recv_exact)
|
|
|
|
def _read_data(self, n):
|
|
while len(self._mux_in) < n:
|
|
val = int.from_bytes(self._recv_exact(4), 'little')
|
|
tag = (val >> 24) - MPLEX_BASE
|
|
ln = val & 0xFFFFFF
|
|
payload = self._recv_exact(ln) if ln else b''
|
|
if tag == MSG_DATA:
|
|
self._mux_in += payload
|
|
else:
|
|
self.messages.append((tag, payload))
|
|
out, self._mux_in = self._mux_in[:n], self._mux_in[n:]
|
|
return out
|
|
|
|
def r_int(self):
|
|
return _s32(int.from_bytes(self._read_data(4), 'little'))
|
|
|
|
def r_byte(self):
|
|
return self._read_data(1)[0]
|
|
|
|
def r_shortint(self):
|
|
return int.from_bytes(self._read_data(2), 'little')
|
|
|
|
def r_buf(self, n):
|
|
return self._read_data(n)
|
|
|
|
def r_vstring(self):
|
|
n = self._read_data(1)[0]
|
|
if n & 0x80:
|
|
n = (n & 0x7F) * 0x100 + self._read_data(1)[0]
|
|
return self._read_data(n) if n else b''
|
|
|
|
def r_ndx(self):
|
|
b0 = self._read_data(1)[0]
|
|
if b0 == 0xFF:
|
|
b0 = self._read_data(1)[0]
|
|
neg = True
|
|
elif b0 == 0:
|
|
return NDX_DONE
|
|
else:
|
|
neg = False
|
|
prev = self._r_ndx_prev_negative if neg else self._r_ndx_prev_positive
|
|
if b0 == 0xFE:
|
|
b = self._read_data(2)
|
|
if b[0] & 0x80:
|
|
rest = self._read_data(2)
|
|
num = ((b[0] & 0x7F) << 24) | b[1] | (rest[0] << 8) | (rest[1] << 16)
|
|
else:
|
|
num = (b[0] << 8) + b[1] + prev
|
|
else:
|
|
num = b0 + prev
|
|
if neg:
|
|
self._r_ndx_prev_negative = num
|
|
return -num
|
|
self._r_ndx_prev_positive = num
|
|
return num
|
|
|
|
def r_sum_head(self):
|
|
return (self.r_int(), self.r_int(), self.r_int(), self.r_int())
|
|
|
|
def r_varint(self):
|
|
return _read_varint(self._read_data)
|
|
|
|
def r_varlong(self, min_bytes):
|
|
b2 = self._read_data(min_bytes)
|
|
u = bytearray(9)
|
|
u[0:min_bytes - 1] = b2[1:min_bytes]
|
|
extra = _INT_BYTE_EXTRA[b2[0] >> 2]
|
|
if extra:
|
|
bit = 1 << (8 - extra)
|
|
u[min_bytes - 1:min_bytes - 1 + extra] = self._read_data(extra)
|
|
u[min_bytes + extra - 1] = b2[0] & (bit - 1)
|
|
else:
|
|
u[min_bytes - 1] = b2[0]
|
|
x = 0
|
|
for i in range(8):
|
|
x |= u[i] << (8 * i)
|
|
return x
|
|
|
|
def r_varint30(self):
|
|
return self.r_varint() if self.protocol >= 30 else self.r_int()
|
|
|
|
def r_varlong30(self, min_bytes):
|
|
return self.r_varlong(min_bytes) if self.protocol >= 30 else self.r_int()
|
|
|
|
def handshake(self, module, server_args, greeting_version=30):
|
|
greeting = self._readline()
|
|
if not greeting.startswith('@RSYNCD:'):
|
|
raise ProtocolError(f"bad greeting: {greeting!r}")
|
|
|
|
self._send_raw(f"@RSYNCD: {greeting_version}.0\n".encode())
|
|
self._send_raw(module.encode() + b"\n")
|
|
resp = self._readline()
|
|
if 'OK' not in resp:
|
|
raise ProtocolError(f"daemon did not send OK (got {resp!r}); "
|
|
"module may require auth or be unknown")
|
|
|
|
payload = b''.join(a.encode() + b"\0" for a in server_args) + b"\0"
|
|
self._send_raw(payload)
|
|
|
|
self.compat_flags = self._r_varint()
|
|
self.seed = self._r_int()
|
|
|
|
def send_message(self, code, payload):
|
|
self._send_raw(self._frame(code, payload))
|
|
|
|
def send_flat_flist(self, entries, io_error=0):
|
|
buf = bytearray()
|
|
for e in entries:
|
|
buf += e.encode()
|
|
buf += end_of_flist(io_error, self.protocol)
|
|
self.send_data(bytes(buf))
|
|
|
|
def send_transfer_ndx(self, ndx, iflags=0):
|
|
self.send_data(self.w_ndx(ndx) + w_shortint(iflags))
|
|
|
|
def send_ndx_done(self):
|
|
self.send_data(self.w_ndx(NDX_DONE))
|
|
|
|
def send_subflist_marker(self, dir_ndx):
|
|
self.send_data(self.w_ndx(NDX_FLIST_OFFSET - dir_ndx))
|
|
|
|
def run_forged_transfer(self, forged_type, xname, literal_tail=b'',
|
|
file_csum_len=16, max_phase=2):
|
|
try:
|
|
self._forged_transfer_loop(forged_type, xname, literal_tail,
|
|
file_csum_len, max_phase)
|
|
except (ProtocolError, socket.timeout, OSError):
|
|
pass
|
|
|
|
def _forged_transfer_loop(self, forged_type, xname, literal_tail,
|
|
file_csum_len, max_phase):
|
|
phase = 0
|
|
while True:
|
|
ndx = self.r_ndx()
|
|
if ndx == NDX_DONE:
|
|
self.send_data(self.w_ndx(NDX_DONE))
|
|
phase += 1
|
|
if phase > max_phase:
|
|
break
|
|
continue
|
|
iflags = self.r_shortint()
|
|
if iflags & ITEM_BASIS_TYPE_FOLLOWS:
|
|
self.r_byte()
|
|
if iflags & ITEM_XNAME_FOLLOWS:
|
|
self.r_vstring()
|
|
count, blength, s2length, remainder = self.r_sum_head()
|
|
for _ in range(count):
|
|
self.r_int()
|
|
self.r_buf(s2length)
|
|
out = bytearray()
|
|
out += self.w_ndx(ndx)
|
|
out += w_shortint(iflags | ITEM_BASIS_TYPE_FOLLOWS | ITEM_XNAME_FOLLOWS)
|
|
out += w_byte(forged_type)
|
|
out += w_vstring(xname)
|
|
out += w_sum_head(count, blength, s2length, remainder)
|
|
if count > 0:
|
|
out += w_int(-1)
|
|
if literal_tail:
|
|
out += w_int(len(literal_tail)) + literal_tail
|
|
out += w_int(0)
|
|
out += b'\x00' * file_csum_len
|
|
self.send_data(bytes(out))
|
|
|
|
def recv_flist(self, preserve_links=True):
|
|
self.send_data(w_int(0))
|
|
entries = []
|
|
prev_name = b''
|
|
prev_mode = 0
|
|
prev_mtime = 0
|
|
while True:
|
|
flags = self.r_byte()
|
|
if flags & XMIT_EXTENDED_FLAGS:
|
|
flags |= self.r_byte() << 8
|
|
if flags == 0:
|
|
break
|
|
l1 = self.r_byte() if flags & XMIT_SAME_NAME else 0
|
|
l2 = self.r_varint30() if flags & XMIT_LONG_NAME else self.r_byte()
|
|
name = prev_name[:l1] + self.r_buf(l2)
|
|
prev_name = name
|
|
length = self.r_varlong30(3)
|
|
if not (flags & XMIT_SAME_TIME):
|
|
prev_mtime = self.r_varlong30(4)
|
|
mtime = prev_mtime
|
|
if flags & XMIT_MOD_NSEC:
|
|
self.r_varint()
|
|
if not (flags & XMIT_SAME_MODE):
|
|
prev_mode = self.r_int()
|
|
mode = prev_mode
|
|
link_target = None
|
|
if preserve_links and S_ISLNK(mode):
|
|
link_target = self.r_buf(self.r_varint30())
|
|
entries.append(ParsedEntry(name, mode, length, mtime, link_target))
|
|
return entries
|
|
|
|
def make_request(self, ndx):
|
|
return (self.w_ndx(ndx) + w_shortint(ITEM_TRANSFER)
|
|
+ w_sum_head(0, 0, 0, 0))
|
|
|
|
def recv_file_transfer(self, ndx):
|
|
iflags = self.r_shortint()
|
|
if iflags & ITEM_BASIS_TYPE_FOLLOWS:
|
|
self.r_byte()
|
|
if iflags & ITEM_XNAME_FOLLOWS:
|
|
self.r_vstring()
|
|
self.r_sum_head()
|
|
data = bytearray()
|
|
while True:
|
|
tok = self.r_int()
|
|
if tok == 0:
|
|
break
|
|
if tok > 0:
|
|
data += self.r_buf(tok)
|
|
else:
|
|
raise ProtocolError(f"unexpected block match token {tok} "
|
|
"(no basis was offered)")
|
|
self.r_buf(self.xfer_sum_len)
|
|
return bytes(data)
|
|
|
|
def pull(self, dest_dir, verbose=False, preserve_times=True,
|
|
preserve_perms=True):
|
|
entries = sort_entries(self.recv_flist())
|
|
reg = []
|
|
for ndx, e in enumerate(entries):
|
|
rel = e.name.decode('utf-8', 'surrogateescape')
|
|
if rel in ('.', ''):
|
|
continue
|
|
path = os.path.join(dest_dir, rel)
|
|
if e.is_dir:
|
|
os.makedirs(path, exist_ok=True)
|
|
elif e.is_link and e.link_target is not None:
|
|
tgt = e.link_target.decode('utf-8', 'surrogateescape')
|
|
if os.path.lexists(path):
|
|
os.unlink(path)
|
|
os.symlink(tgt, path)
|
|
elif e.is_reg:
|
|
reg.append((ndx, e, path))
|
|
if verbose:
|
|
print(rel)
|
|
|
|
for ndx, e, path in reg:
|
|
self.send_data(self.make_request(ndx))
|
|
self.send_data(self.w_ndx(NDX_DONE))
|
|
|
|
got = {}
|
|
phase = 1
|
|
while True:
|
|
ndx = self.r_ndx()
|
|
if ndx == NDX_DONE:
|
|
phase += 1
|
|
if phase > 2:
|
|
break
|
|
self.send_data(self.w_ndx(NDX_DONE))
|
|
continue
|
|
got[ndx] = self.recv_file_transfer(ndx)
|
|
for ndx, e, path in reg:
|
|
if ndx not in got:
|
|
continue
|
|
with open(path, 'wb') as fh:
|
|
fh.write(got[ndx])
|
|
if preserve_perms:
|
|
os.chmod(path, e.mode & 0o7777)
|
|
if preserve_times:
|
|
os.utime(path, (e.mtime, e.mtime))
|
|
return entries
|
|
|
|
def finish_no_transfer(self):
|
|
self.send_data(self.w_ndx(NDX_DONE))
|
|
phase = 1
|
|
try:
|
|
while True:
|
|
if self.r_ndx() == NDX_DONE:
|
|
phase += 1
|
|
if phase > 2:
|
|
break
|
|
self.send_data(self.w_ndx(NDX_DONE))
|
|
except (ProtocolError, socket.timeout, OSError):
|
|
pass
|
|
|
|
def push(self, files, modtime=1700000000):
|
|
names = [n.encode() if isinstance(n, str) else n for n, _ in files]
|
|
entries = [FileEntry(n, mode=S_IFREG | 0o644, length=len(c),
|
|
modtime=modtime, protocol=self.protocol)
|
|
for n, (_, c) in zip(names, files)]
|
|
self.send_flat_flist(entries)
|
|
|
|
order = sorted(range(len(files)), key=lambda i: names[i])
|
|
content_by_ndx = {ndx: files[i][1] for ndx, i in enumerate(order)}
|
|
phase = 0
|
|
while True:
|
|
ndx = self.r_ndx()
|
|
if ndx == NDX_DONE:
|
|
self.send_data(self.w_ndx(NDX_DONE))
|
|
phase += 1
|
|
if phase > 2:
|
|
break
|
|
continue
|
|
iflags = self.r_shortint()
|
|
if iflags & ITEM_BASIS_TYPE_FOLLOWS:
|
|
self.r_byte()
|
|
if iflags & ITEM_XNAME_FOLLOWS:
|
|
self.r_vstring()
|
|
count, blength, s2length, remainder = self.r_sum_head()
|
|
for _ in range(count):
|
|
self.r_int()
|
|
self.r_buf(s2length)
|
|
content = content_by_ndx.get(ndx, b'')
|
|
out = bytearray()
|
|
out += self.w_ndx(ndx)
|
|
out += w_shortint(ITEM_TRANSFER)
|
|
out += w_sum_head(0, 0, 0, 0)
|
|
out += self.make_file_token_stream(content)
|
|
out += hashlib.md5(content).digest()
|
|
self.send_data(bytes(out))
|
|
|
|
def make_file_token_stream(self, content):
|
|
out = bytearray()
|
|
for off in range(0, len(content), CHUNK_SIZE):
|
|
chunk = content[off:off + CHUNK_SIZE]
|
|
out += w_int(len(chunk)) + chunk
|
|
out += w_int(0)
|
|
return bytes(out)
|
|
|
|
def drain(self, timeout=3.0):
|
|
self.sock.settimeout(timeout)
|
|
out = b''
|
|
try:
|
|
while True:
|
|
chunk = self.sock.recv(65536)
|
|
if not chunk:
|
|
break
|
|
out += chunk
|
|
except socket.timeout:
|
|
pass
|
|
return out
|
|
|
|
DaemonSender = DaemonClient
|
|
|
|
class DaemonReceiver(_SocketPeer):
|
|
def __init__(self, sock, greeting_version=30, protocol=DEFAULT_PROTOCOL):
|
|
super().__init__(sock)
|
|
self.protocol = protocol
|
|
self.greeting_version = greeting_version
|
|
|
|
def handshake(self, compat_flags=0, seed=0):
|
|
self._send_raw(f"@RSYNCD: {self.greeting_version}.0\n".encode())
|
|
self._readline()
|
|
self._readline()
|
|
self._send_raw(b"@RSYNCD: OK\n")
|
|
self._send_raw(w_varint(compat_flags) + w_int(seed))
|
|
|
|
def send_sum_request(self, ndx, count, blength, s2length, remainder,
|
|
iflags=ITEM_TRANSFER):
|
|
buf = (self.w_ndx(ndx) + w_shortint(iflags)
|
|
+ w_sum_head(count, blength, s2length, remainder))
|
|
self.send_data(buf)
|
|
|
|
def drain(self, timeout=5.0):
|
|
self.sock.settimeout(timeout)
|
|
try:
|
|
while self.sock.recv(65536):
|
|
pass
|
|
except (OSError, ProtocolError):
|
|
pass
|
|
|