class SMBRawIO(io.RawIOBase): FILE_TYPE = None # 'file', 'dir', or None (for unknown) _INVALID_MODE = '' def __init__(self, path, mode='r', share_access=None, desired_access=None, file_attributes=None, create_options=0, buffer_size=MAX_PAYLOAD_SIZE, **kwargs): tree, fd_path = get_smb_tree(path, **kwargs) self.share_access = share_access self.fd = Open(tree, fd_path) self._mode = mode self._name = path self._offset = 0 self._flush = False self._buffer_size = buffer_size if desired_access is None: desired_access = 0 # While we can open a directory, the values for FilePipePrinterAccessMask also apply to Dirs so just use # the same enum to simplify code. if 'r' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_READ_DATA | \ FilePipePrinterAccessMask.FILE_READ_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_READ_EA if 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_WRITE_DATA | \ FilePipePrinterAccessMask.FILE_WRITE_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_WRITE_EA self._desired_access = desired_access if file_attributes is None: file_attributes = FileAttributes.FILE_ATTRIBUTE_DIRECTORY if self.FILE_TYPE == 'dir' \ else FileAttributes.FILE_ATTRIBUTE_NORMAL self._file_attributes = file_attributes self._create_options = create_options self._create_options |= { 'dir': CreateOptions.FILE_DIRECTORY_FILE, 'file': CreateOptions.FILE_NON_DIRECTORY_FILE, }.get(self.FILE_TYPE, 0) super(SMBRawIO, self).__init__() def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() @property def closed(self): return not self.fd.connected @property def mode(self): return self._mode @property def name(self): return self._name def close(self, transaction=None): if transaction: transaction += self.fd.close(send=False) else: self.fd.close() def flush(self): if self._flush and self.FILE_TYPE != 'pipe': self.fd.flush() def open(self, transaction=None): if not self.closed: return share_access = _parse_share_access(self.share_access, self.mode) create_disposition = _parse_mode(self.mode, invalid=self._INVALID_MODE) try: # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wpo/feeb3122-cfe0-4b34-821d-e31c036d763c # Impersonation on SMB has little meaning when opening files but is important if using RPC so set to a sane # default of Impersonation. open_result = self.fd.create( ImpersonationLevel.Impersonation, self._desired_access, self._file_attributes, share_access, create_disposition, self._create_options, send=(transaction is None), ) except SMBResponseException as exc: raise SMBOSError(exc.status, self.name) if transaction is not None: transaction += open_result elif 'a' in self.mode and self.FILE_TYPE != 'pipe': self._offset = self.fd.end_of_file def readable(self): """ True if file was opened in a read mode. """ return 'r' in self.mode or '+' in self.mode def seek(self, offset, whence=SEEK_SET): """ Move to new file position and return the file position. Argument offset is a byte count. Optional argument whence defaults to SEEK_SET or 0 (offset from start of file, offset should be >= 0); other values are SEEK_CUR or 1 (move relative to current position, positive or negative), and SEEK_END or 2 (move relative to end of file, usually negative, although many platforms allow seeking beyond the end of a file). Note that not all file objects are seekable. """ seek_offset = { SEEK_SET: 0, SEEK_CUR: self._offset, SEEK_END: self.fd.end_of_file, }[whence] self._offset = seek_offset + offset return self._offset def seekable(self): """ True if file supports random-access. """ return True def tell(self): """ Current file position. Can raise OSError for non seekable files. """ return self._offset def truncate(self, size): """ Truncate the file to at most size bytes and return the truncated size. Size defaults to the current file position, as returned by tell(). The current file position is changed to the value of size. """ with SMBFileTransaction(self) as transaction: eof_info = FileEndOfFileInformation() eof_info['end_of_file'] = size set_info(transaction, eof_info) self.fd.end_of_file = size self._flush = True return size def writable(self): """ True if file was opened in a write mode. """ return 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode def readall(self): """ Read and return all the bytes from the stream until EOF, using multiple calls to the stream if necessary. :return: The byte string read from the SMB file. """ data = b"" remaining_bytes = self.fd.end_of_file - self._offset while len(data) < remaining_bytes or self.FILE_TYPE == 'pipe': try: data += self.fd.read(self._offset, self._buffer_size) except SMBResponseException as exc: if exc.status == NtStatus.STATUS_PIPE_BROKEN: break raise if self.FILE_TYPE != 'pipe': self._offset += len(data) return data def readinto(self, b): """ Read bytes into a pre-allocated, writable bytes-like object b, and return the number of bytes read. :param b: bytes-like object to read the data into. :return: The number of bytes read. """ if self._offset >= self.fd.end_of_file and self.FILE_TYPE != 'pipe': return 0 try: file_bytes = self.fd.read(self._offset, len(b)) except SMBResponseException as exc: if exc.status == NtStatus.STATUS_PIPE_BROKEN: file_bytes = b"" else: raise b[:len(file_bytes)] = file_bytes if self.FILE_TYPE != 'pipe': self._offset += len(file_bytes) return len(file_bytes) def write(self, b): """ Write buffer b to file, return number of bytes written. Only makes one system call, so not all of the data may be written. The number of bytes actually written is returned. """ if isinstance(b, memoryview): b = b.tobytes() with SMBFileTransaction(self) as transaction: transaction += self.fd.write(b, offset=self._offset, send=False) # Send the request with an SMB2QueryInfoRequest for FileStandardInformation so we can update the end of # file stored internally. if self.FILE_TYPE != 'pipe': query_info(transaction, FileStandardInformation) bytes_written = transaction.results[0] if self.FILE_TYPE != 'pipe': self._offset += bytes_written self.fd.end_of_file = transaction.results[1][ 'end_of_file'].get_value() self._flush = True return bytes_written
class SMBRawIO(io.RawIOBase): FILE_TYPE = None # 'file', 'dir', or None (for unknown) _INVALID_MODE = '' def __init__(self, path, mode='r', share_access=None, desired_access=None, file_attributes=None, create_options=0, **kwargs): tree, fd_path = get_smb_tree(path, **kwargs) self.share_access = share_access self.fd = Open(tree, fd_path) self._mode = mode self._name = path self._offset = 0 self._flush = False self.__kwargs = kwargs # Used in open for DFS referrals if desired_access is None: desired_access = 0 # While we can open a directory, the values for FilePipePrinterAccessMask also apply to Dirs so just use # the same enum to simplify code. if 'r' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_READ_DATA | \ FilePipePrinterAccessMask.FILE_READ_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_READ_EA if 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_WRITE_DATA | \ FilePipePrinterAccessMask.FILE_WRITE_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_WRITE_EA self._desired_access = desired_access if file_attributes is None: file_attributes = FileAttributes.FILE_ATTRIBUTE_DIRECTORY if self.FILE_TYPE == 'dir' \ else FileAttributes.FILE_ATTRIBUTE_NORMAL self._file_attributes = file_attributes self._create_options = create_options self._create_options |= { 'dir': CreateOptions.FILE_DIRECTORY_FILE, 'file': CreateOptions.FILE_NON_DIRECTORY_FILE, }.get(self.FILE_TYPE, 0) super(SMBRawIO, self).__init__() def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() @property def closed(self): return not self.fd.connected @property def mode(self): return self._mode @property def name(self): return self._name def close(self, transaction=None): if transaction: transaction += self.fd.close(send=False) else: self.fd.close() def flush(self): if self._flush and self.FILE_TYPE != 'pipe': self.fd.flush() def open(self, transaction=None): if not self.closed: return share_access = _parse_share_access(self.share_access, self.mode) create_disposition = _parse_mode(self.mode, invalid=self._INVALID_MODE) try: # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wpo/feeb3122-cfe0-4b34-821d-e31c036d763c # Impersonation on SMB has little meaning when opening files but is important if using RPC so set to a sane # default of Impersonation. open_result = self.fd.create( ImpersonationLevel.Impersonation, self._desired_access, self._file_attributes, share_access, create_disposition, self._create_options, send=(transaction is None), ) except (PathNotCovered, ObjectNameNotFound, ObjectPathNotFound) as exc: # The MS-DFSC docs status that STATUS_PATH_NOT_COVERED is used when encountering a link to a different # server but Samba seems to return the generic name or path not found. if not self.fd.tree_connect.is_dfs_share: raise SMBOSError(exc.status, self.name) # Path is on a DFS root that is linked to another server. client_config = ClientConfig() referral = dfs_request(self.fd.tree_connect, self.name[1:]) client_config.cache_referral(referral) info = client_config.lookup_referral([p for p in self.name.split("\\") if p]) for target in info: new_path = self.name.replace(info.dfs_path, target.target_path, 1) try: tree, fd_path = get_smb_tree(new_path, **self.__kwargs) self.fd = Open(tree, fd_path) self.open(transaction=transaction) except SMBResponseException as link_exc: log.warning("Failed to connect to DFS link target %s: %s" % (str(target), link_exc)) else: # Record the target that worked for future reference. info.target_hint = target break else: # None of the targets worked so raise the original error. raise SMBOSError(exc.status, self.name) return except SMBResponseException as exc: raise SMBOSError(exc.status, self.name) if transaction is not None: transaction += open_result elif 'a' in self.mode and self.FILE_TYPE != 'pipe': self._offset = self.fd.end_of_file def readable(self): """ True if file was opened in a read mode. """ return 'r' in self.mode or '+' in self.mode def seek(self, offset, whence=SEEK_SET): """ Move to new file position and return the file position. Argument offset is a byte count. Optional argument whence defaults to SEEK_SET or 0 (offset from start of file, offset should be >= 0); other values are SEEK_CUR or 1 (move relative to current position, positive or negative), and SEEK_END or 2 (move relative to end of file, usually negative, although many platforms allow seeking beyond the end of a file). Note that not all file objects are seekable. """ seek_offset = { SEEK_SET: 0, SEEK_CUR: self._offset, SEEK_END: self.fd.end_of_file, }[whence] self._offset = seek_offset + offset return self._offset def seekable(self): """ True if file supports random-access. """ return True def tell(self): """ Current file position. Can raise OSError for non seekable files. """ return self._offset def truncate(self, size): """ Truncate the file to at most size bytes and return the truncated size. Size defaults to the current file position, as returned by tell(). The current file position is changed to the value of size. """ with SMBFileTransaction(self) as transaction: eof_info = FileEndOfFileInformation() eof_info['end_of_file'] = size set_info(transaction, eof_info) self.fd.end_of_file = size self._flush = True return size def writable(self): """ True if file was opened in a write mode. """ return 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode def readall(self): """ Read and return all the bytes from the stream until EOF, using multiple calls to the stream if necessary. :return: The byte string read from the SMB file. """ data = bytearray() while True: read_length = min( # We always want to be reading a minimum of 64KiB. max(self.fd.end_of_file - self._offset, MAX_PAYLOAD_SIZE), self.fd.connection.max_read_size # We can never read more than this. ) buffer = bytearray(b'\x00' * read_length) bytes_read = self.readinto(buffer) if not bytes_read: break data += buffer[:bytes_read] return bytes(data) def readinto(self, b): """ Read bytes into a pre-allocated, writable bytes-like object b, and return the number of bytes read. This may read less bytes than requested as it depends on the negotiated read size and SMB credits available. :param b: bytes-like object to read the data into. :return: The number of bytes read. """ if self._offset >= self.fd.end_of_file and self.FILE_TYPE != 'pipe': return 0 chunk_size, credit_request = _chunk_size(self.fd.connection, len(b), 'read') read_msg, recv_func = self.fd.read(self._offset, chunk_size, send=False) request = self.fd.connection.send( read_msg, sid=self.fd.tree_connect.session.session_id, tid=self.fd.tree_connect.tree_connect_id, credit_request=credit_request ) try: file_bytes = recv_func(request) except PipeBroken: # A pipe will block until it returns the data available or was closed/broken. file_bytes = b"" b[:len(file_bytes)] = file_bytes if self.FILE_TYPE != 'pipe': self._offset += len(file_bytes) return len(file_bytes) def write(self, b): """ Write buffer b to file, return number of bytes written. Only makes one system call, so not all of the data may be written. The number of bytes actually written is returned. This can be less than the length of b as it depends on the underlying connection. """ chunk_size, credit_request = _chunk_size(self.fd.connection, len(b), 'write') # Python 2 compat, can be removed and just use the else statement. if isinstance(b, memoryview): data = b[:chunk_size].tobytes() else: data = bytes(b[:chunk_size]) write_msg, recv_func = self.fd.write(data, offset=self._offset, send=False) request = self.fd.connection.send( write_msg, sid=self.fd.tree_connect.session.session_id, tid=self.fd.tree_connect.tree_connect_id, credit_request=credit_request ) bytes_written = recv_func(request) if self.FILE_TYPE != 'pipe': self._offset += bytes_written self.fd.end_of_file = max(self.fd.end_of_file, self._offset) self._flush = True return bytes_written