Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-smbprotocol for openSUSE:Factory checked in at 2021-05-17 18:45:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-smbprotocol (Old) and /work/SRC/openSUSE:Factory/.python-smbprotocol.new.2988 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-smbprotocol" Mon May 17 18:45:00 2021 rev:11 rq:893035 version:1.5.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-smbprotocol/python-smbprotocol.changes 2021-03-25 14:53:03.828530529 +0100 +++ /work/SRC/openSUSE:Factory/.python-smbprotocol.new.2988/python-smbprotocol.changes 2021-05-17 18:45:14.408642587 +0200 @@ -1,0 +2,13 @@ +Thu May 13 16:29:17 UTC 2021 - Martin Hauke <mar...@gmx.de> + +- Update to version 1.5.1 + * Unified DFS path handling when using any API that uses a + transaction to open the file. + - This includes smbclient.rename and smbclient.replace + * Fixed up smbclient.rename to work with directories. + * smbclient.scandir will continue to use the connection cache + when getting stat information of a dir entry. + * smbclient.shutil.rmtree will continue to use the connection + cache when removing child entries. + +------------------------------------------------------------------- Old: ---- python-smbprotocol-1.5.0.tar.gz New: ---- python-smbprotocol-1.5.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-smbprotocol.spec ++++++ --- /var/tmp/diff_new_pack.Xe7pRC/_old 2021-05-17 18:45:15.136639498 +0200 +++ /var/tmp/diff_new_pack.Xe7pRC/_new 2021-05-17 18:45:15.140639481 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-smbprotocol -Version: 1.5.0 +Version: 1.5.1 Release: 0 Summary: SMBv2/v3 client for Python 2 and 3 License: MIT ++++++ python-smbprotocol-1.5.0.tar.gz -> python-smbprotocol-1.5.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/CHANGELOG.md new/smbprotocol-1.5.1/CHANGELOG.md --- old/smbprotocol-1.5.0/CHANGELOG.md 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/CHANGELOG.md 2021-05-08 01:01:14.000000000 +0200 @@ -1,5 +1,14 @@ # Changelog +## 1.5.1 - 2021-05-08 + +* Unified DFS path handling when using any API that uses a transaction to open the file + * This includes `smbclient.rename` and `smbclient.replace` +* Fixed up `smbclient.rename` to work with directories +* `smbclient.scandir` will continue to use the connection cache when getting stat information of a dir entry +* `smbclient.shutil.rmtree` will continue to use the connection cache when removing child entries + + ## 1.5.0 - 2021-03-25 * Added `smbprotocol.exceptions.SMBConnectionClosed` that is raised when trying to send or receive data on a connection that has been closed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/setup.py new/smbprotocol-1.5.1/setup.py --- old/smbprotocol-1.5.0/setup.py 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/setup.py 2021-05-08 01:01:14.000000000 +0200 @@ -18,7 +18,7 @@ setup( name='smbprotocol', - version='1.5.0', + version='1.5.1', packages=['smbclient', 'smbprotocol'], install_requires=[ 'cryptography>=2.0', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/smbclient/_io.py new/smbprotocol-1.5.1/smbclient/_io.py --- old/smbprotocol-1.5.0/smbclient/_io.py 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/smbclient/_io.py 2021-05-08 01:01:14.000000000 +0200 @@ -45,6 +45,7 @@ Open, QueryDirectoryFlags, ShareAccess, + SMB2CreateRequest, SMB2QueryInfoRequest, SMB2QueryInfoResponse, SMB2SetInfoRequest, @@ -136,6 +137,40 @@ return chunk_size, int(credit_request) +def _resolve_dfs(raw_io): + """ + Resolves a DFS path for a failed Open request. + + :param raw_io: The SMBRawIO to resolve the DFS path for. + :return: A new Open for each DFS target that was resolved. + """ + if not raw_io.fd.tree_connect.is_dfs_share: + return + + # Path is on a DFS root that is linked to another server. + client_config = ClientConfig() + raw_path = raw_io.name + + referral = dfs_request(raw_io.fd.tree_connect, raw_path[1:]) + client_config.cache_referral(referral) + info = client_config.lookup_referral([p for p in raw_path.split("\\") if p]) + connection_kwargs = getattr(raw_io, '_%s__kwargs' % type(raw_io).__name__, {}) + + for target in info: + new_path = raw_path.replace(info.dfs_path, target.target_path, 1) + + try: + tree, fd_path = get_smb_tree(new_path, **connection_kwargs) + + except SMBResponseException as link_exc: + log.warning("Failed to connect to DFS link target %s: %s" % (str(target), link_exc)) + continue + + # Record the target that worked for future reference. + info.target_hint = target + yield Open(tree, fd_path) + + def ioctl_request(transaction, ctl_code, output_size=0, flags=IOCTLFlags.SMB2_0_IOCTL_IS_IOCTL, input_buffer=b""): """ Sends an IOCTL request to the server. @@ -240,17 +275,21 @@ """ Sends a compound request to the server. Optionally opens and closes a handle in the same request if the handle is not already opened. - - :return: A list of each response for the requests set on the transaction. """ remove_index = [] - if self.raw.closed: + if ( + self.raw.closed and + ( + not self._actions or + not isinstance(self._actions[0][0], SMB2CreateRequest) + ) + ): self.raw.open(transaction=self) self._actions.insert(0, self._actions.pop(-1)) # Need to move the create to the start - remove_index.append(0) + remove_index.insert(0, 0) self.raw.close(transaction=self) - remove_index.append(len(self._actions) - 1) + remove_index.insert(0, len(self._actions) - 1) send_msgs = [] unpack_functions = [] @@ -266,19 +305,61 @@ # just enumerate the list in 1 line in case it throws an exception. failures = [] responses = [] + try_again = False for idx, func in enumerate(unpack_functions): + res = None + try: - responses.append(func(requests[idx])) + try: + res = func(requests[idx]) + + except (PathNotCovered, ObjectNameNotFound, ObjectPathNotFound): + # The MS-DFSC docs state 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 the first action is not a CreateRequest then we can't resolve the DFS path in the transaction. + if not (idx == 0 and isinstance(send_msgs[0], SMB2CreateRequest)): + raise + + for smb_open in _resolve_dfs(self.raw): + if smb_open.tree_connect.share_name == self.raw.fd.tree_connect.share_name: + continue + + self.raw.fd = smb_open + + # In case this is a transaction with an explicit open we want to reopen it with the new params + # before trying it again. + self.raw.open(transaction=self) + self._actions[0] = self._actions.pop(-1) + + try_again = True + break + + else: + # Either there wasn't any DFS referrals or none of them worked, just reraise the error. + raise + except SMBResponseException as exc: failures.append(SMBOSError(exc.status, self.raw.name)) - # If there was a failure, just raise the first exception found. - if failures: + finally: + responses.append(res) + + # Remove the existing open/close actions this transaction added internally. + for idx in remove_index: + del self._actions[idx] + del responses[idx] + + if try_again: + # If we updated the SMB Tree due to a DFS referral hit then try again. + self.commit() + + elif failures: + # If there was a failure, just raise the first exception found. raise failures[0] - remove_index.sort(reverse=True) # Need to remove from highest index first. - [responses.pop(i) for i in remove_index] # Remove the open and close response if commit() did that work. - self.results = tuple(responses) + else: + self.results = tuple(responses) class SMBRawIO(io.RawIOBase): @@ -358,63 +439,32 @@ if not self.closed: return + open_at_end = False + if not transaction: + # The SMBFileTransaction has special logic that deals with DFS paths, use that to do the actual open. + open_at_end = True + transaction = SMBFileTransaction(self) + 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 + # 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. + transaction += self.fd.create( + ImpersonationLevel.Impersonation, + self._desired_access, + self._file_attributes, + share_access, + create_disposition, + self._create_options, + send=False, + ) + + if open_at_end: + transaction.commit() + if '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. """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/smbclient/_os.py new/smbprotocol-1.5.1/smbclient/_os.py --- old/smbprotocol-1.5.0/smbclient/_os.py 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/smbclient/_os.py 2021-05-08 01:01:14.000000000 +0200 @@ -121,6 +121,16 @@ ]) +def is_remote_path(path): # type: (str) -> bool + """ + Returns True iff the given path is a remote SMB path (rather than a local path). + + :param path: The filepath. + :return: True iff the given path is a remote SMB path. + """ + return path.startswith('\\\\') + + def copyfile(src, dst, **kwargs): """ Copy a file to a different location on the same server share. This will fail if the src and dst paths are to a @@ -136,10 +146,10 @@ norm_src = ntpath.normpath(src) norm_dst = ntpath.normpath(dst) - if not norm_src.startswith('\\\\'): + if not is_remote_path(norm_src): raise ValueError("src must be an absolute path to where the file should be copied from.") - if not norm_dst.startswith('\\\\'): + if not is_remote_path(norm_dst): raise ValueError("dst must be an absolute path to where the file should be copied to.") src_root = ntpath.splitdrive(norm_src)[0] @@ -200,7 +210,7 @@ norm_src = ntpath.normpath(src) norm_dst = ntpath.normpath(dst) - if not norm_src.startswith('\\\\'): + if not is_remote_path(norm_src): raise ValueError("src must be the absolute path to where the file is hard linked to.") src_root = ntpath.splitdrive(norm_src)[0] @@ -516,13 +526,15 @@ :param kwargs: Common SMB Session arguments for smbclient. :return: An iterator of DirEntry objects in the directory. """ + connection_cache = kwargs.get('connection_cache', None) with SMBDirectoryIO(path, share_access='rwd', **kwargs) as fd: for dir_info in fd.query_directory(search_pattern, FileInformationClass.FILE_ID_FULL_DIRECTORY_INFORMATION): filename = dir_info['file_name'].get_value().decode('utf-16-le') if filename in [u'.', u'..']: continue - dir_entry = SMBDirEntry(SMBRawIO(u"%s\\%s" % (path, filename), **kwargs), dir_info) + dir_entry = SMBDirEntry(SMBRawIO(u"%s\\%s" % (path, filename), **kwargs), dir_info, + connection_cache=connection_cache) yield dir_entry @@ -648,13 +660,13 @@ :param kwargs: Common SMB Session arguments for smbclient. """ norm_dst = ntpath.normpath(dst) - if not norm_dst.startswith('\\\\'): + if not is_remote_path(norm_dst): raise ValueError("The link dst must be an absolute UNC path for where the link is to be created") norm_src = ntpath.normpath(src) print_name = norm_src - if not norm_src.startswith('\\\\'): + if not is_remote_path(norm_src): flags = SymbolicLinkFlags.SYMLINK_FLAG_RELATIVE substitute_name = norm_src dst_dir = ntpath.dirname(norm_dst) @@ -1027,24 +1039,43 @@ def _rename_information(src, dst, replace_if_exists=False, **kwargs): verb = 'replace' if replace_if_exists else 'rename' - norm_src = ntpath.normpath(src) - norm_dst = ntpath.normpath(dst) - - if not norm_dst.startswith('\\\\'): + if not is_remote_path(ntpath.normpath(dst)): raise ValueError("dst must be an absolute path to where the file or directory should be %sd." % verb) - src_root = ntpath.splitdrive(norm_src)[0] - dst_root, dst_name = ntpath.splitdrive(norm_dst) - if src_root.lower() != dst_root.lower(): - raise ValueError("Cannot %s a file to a different root than the src." % verb) + raw_args = dict(kwargs) + raw_args.update({ + 'mode': 'r', + 'share_access': 'rwd', + 'create_options': CreateOptions.FILE_OPEN_REPARSE_POINT, + }) + + # We open/close the dest (ignoring if the file does not exist) so we can resolve the DFS path and determine the + # filename src needs to be renamed to. + dst_raw = SMBRawIO(dst, desired_access=FilePipePrinterAccessMask.FILE_EXECUTE, **raw_args) + try: + SMBFileTransaction(dst_raw).commit() + except SMBOSError as err: + if err.errno != errno.ENOENT: + raise - raw = SMBRawIO(src, mode='r', share_access='rwd', desired_access=FilePipePrinterAccessMask.DELETE, - create_options=CreateOptions.FILE_OPEN_REPARSE_POINT, **kwargs) - with SMBFileTransaction(raw) as transaction: - file_rename = FileRenameInformation() - file_rename['replace_if_exists'] = replace_if_exists - file_rename['file_name'] = to_text(dst_name[1:]) # dst_name has \ prefix from splitdrive, we remove that. - set_info(transaction, file_rename) + # We need to open source first so we can get the resolved tree to compare the volumes + with SMBRawIO(src, desired_access=FilePipePrinterAccessMask.DELETE, **raw_args) as src_raw: + # We compare the server part using the GUID in case a different alias was specified for the server. The GUID + # should uniquely identify the server for our cases here. + src_guid = src_raw.fd.connection.server_guid + src_share = ntpath.normpath(src_raw.fd.tree_connect.share_name).split("\\")[-1] + + dst_guid = dst_raw.fd.connection.server_guid + dst_share = ntpath.normpath(dst_raw.fd.tree_connect.share_name).split("\\")[-1] + + if src_guid != dst_guid or src_share.lower() != dst_share.lower(): + raise ValueError("Cannot %s a file to a different root than the src." % verb) + + with SMBFileTransaction(src_raw) as transaction: + file_rename = FileRenameInformation() + file_rename['replace_if_exists'] = replace_if_exists + file_rename['file_name'] = to_text(dst_raw.fd.file_name) + set_info(transaction, file_rename) def _set_basic_information(path, creation_time=0, last_access_time=0, last_write_time=0, change_time=0, @@ -1064,11 +1095,12 @@ class SMBDirEntry(object): - def __init__(self, raw, dir_info): + def __init__(self, raw, dir_info, connection_cache=None): self._smb_raw = raw self._dir_info = dir_info self._stat = None self._lstat = None + self._connection_cache = connection_cache def __str__(self): return '<{0}: {1!r}>'.format(self.__class__.__name__, to_native(self.name)) @@ -1179,16 +1211,16 @@ if follow_symlinks: if not self._stat: if self.is_symlink(): - self._stat = stat(self.path) + self._stat = stat(self.path, connection_cache=self._connection_cache) else: # Because it's not a symlink lstat will be the same as stat so set both. if self._lstat is None: - self._lstat = lstat(self._smb_raw.name) + self._lstat = lstat(self._smb_raw.name, connection_cache=self._connection_cache) self._stat = self._lstat return self._stat else: if not self._lstat: - self._lstat = lstat(self.path) + self._lstat = lstat(self.path, connection_cache=self._connection_cache) return self._lstat @classmethod @@ -1200,7 +1232,7 @@ dir_info['file_attributes'] = file_stat.st_file_attributes dir_info['file_id'] = file_stat.st_ino - dir_entry = cls(SMBRawIO(path, **kwargs), dir_info) + dir_entry = cls(SMBRawIO(path, **kwargs), dir_info, connection_cache=kwargs.get('connection_cache', None)) dir_entry._stat = file_stat return dir_entry diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/smbclient/shutil.py new/smbprotocol-1.5.1/smbclient/shutil.py --- old/smbprotocol-1.5.0/smbclient/shutil.py 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/smbclient/shutil.py 2021-05-08 01:01:14.000000000 +0200 @@ -31,6 +31,7 @@ stat as smbclient_stat, symlink, SMBDirEntry, + is_remote_path, ) from smbclient.path import ( @@ -130,7 +131,7 @@ return raise_not_implemented norm_src = ntpath.normpath(src) - if norm_src.startswith('\\\\'): + if is_remote_path(norm_src): src_root = ntpath.splitdrive(norm_src)[0] islink_func = islink readlink_func = readlink @@ -147,7 +148,7 @@ src_kwargs = {} norm_dst = ntpath.normpath(dst) - if norm_dst.startswith('\\\\'): + if is_remote_path(norm_dst): dst_root = ntpath.splitdrive(norm_dst)[0] dst_open = open_file dst_kwargs = kwargs @@ -216,7 +217,7 @@ src_mode = stat.S_IMODE(_get_file_stat(src, follow_symlinks, **kwargs).st_mode) norm_dst = ntpath.normpath(dst) - if norm_dst.startswith('\\\\'): + if is_remote_path(norm_dst): read_only = not (src_mode & stat.S_IWRITE == stat.S_IWRITE and src_mode & stat.S_IREAD == stat.S_IREAD) _set_file_basic_info(dst, follow_symlinks, read_only=read_only, **kwargs) else: @@ -244,7 +245,7 @@ mtime_ns = getattr(src_stat, 'st_mtime_ns', src_stat.st_mtime * 1000000000) norm_dst = ntpath.normpath(dst) - if norm_dst.startswith('\\\\'): + if is_remote_path(norm_dst): read_only = not (src_mode & stat.S_IWRITE == stat.S_IWRITE and src_mode & stat.S_IREAD == stat.S_IREAD) _set_file_basic_info(dst, follow_symlinks, read_only=read_only, atime_ns=atime_ns, mtime_ns=mtime_ns, **kwargs) else: @@ -407,19 +408,19 @@ if dir_entry.is_symlink() and \ dir_entry.stat(follow_symlinks=False).st_file_attributes & FileAttributes.FILE_ATTRIBUTE_DIRECTORY: try: - rmdir(dir_entry.path) + rmdir(dir_entry.path, **kwargs) except OSError: onerror(rmdir, dir_entry.path, sys.exc_info()) elif dir_entry.is_dir(): - rmtree(dir_entry.path, ignore_errors, onerror) + rmtree(dir_entry.path, ignore_errors, onerror, **kwargs) else: try: - remove(dir_entry.path) + remove(dir_entry.path, **kwargs) except OSError: onerror(remove, dir_entry.path, sys.exc_info()) try: - rmdir(path) + rmdir(path, **kwargs) except OSError: onerror(rmdir, path, sys.exc_info()) @@ -427,7 +428,7 @@ def _copy(src, dst, follow_symlinks, copy_meta_func, **kwargs): # Need to check if dst is a UNC path before checking if it's a dir in smbclient.path before checking to see if it's # a local directory. If either one is a dir, join the filename of src onto dst. - if ntpath.normpath(dst).startswith('\\\\') and isdir(dst, **kwargs): + if is_remote_path(ntpath.normpath(dst)) and isdir(dst, **kwargs): dst = ntpath.join(dst, ntpath.basename(src)) elif os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/smbprotocol/exceptions.py new/smbprotocol-1.5.1/smbprotocol/exceptions.py --- old/smbprotocol-1.5.0/smbprotocol/exceptions.py 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/smbprotocol/exceptions.py 2021-05-08 01:01:14.000000000 +0200 @@ -77,6 +77,7 @@ error_details = { NtStatus.STATUS_OBJECT_NAME_NOT_FOUND: errno.ENOENT, NtStatus.STATUS_OBJECT_PATH_NOT_FOUND: errno.ENOENT, + NtStatus.STATUS_NOT_FOUND: errno.ENOENT, NtStatus.STATUS_OBJECT_NAME_COLLISION: errno.EEXIST, NtStatus.STATUS_PRIVILEGE_NOT_HELD: (errno.EACCES, "Required privilege not held"), NtStatus.STATUS_SHARING_VIOLATION: (errno.EPERM, "The process cannot access the file because it is being " diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/tests/conftest.py new/smbprotocol-1.5.1/tests/conftest.py --- old/smbprotocol-1.5.0/tests/conftest.py 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/tests/conftest.py 2021-05-08 01:01:14.000000000 +0200 @@ -7,6 +7,7 @@ import time from smbclient import ( + ClientConfig, delete_session, mkdir, ) @@ -177,7 +178,7 @@ @pytest.fixture(params=[ ('share', 4), ('share-encrypted', 5), -]) +], ids=['share', 'share-encrypted']) def smb_share(request, smb_real): # Use some non ASCII chars to test out edge cases by default. share_path = u"%s\\%s" % (smb_real[request.param[1]], u"P??t??s???-[%s] ????" % time.time()) @@ -198,7 +199,7 @@ ('', None), # Root, no referral targets ('share', 4), # Simple referral to a single target ('share-encrypted', 5), # Referral to 2 targets, first is known to be broken -]) +], ids=['dfs-root', 'dfs-single-target', 'dfs-broken-target']) def smb_dfs_share(request, smb_real): test_folder = u"P??t??s???-[%s] ????" % time.time() @@ -215,3 +216,7 @@ yield dfs_path finally: rmtree(target_share_path, username=smb_real[0], password=smb_real[1], port=smb_real[3]) + + config = ClientConfig() + config._domain_cache = [] + config._referral_cache = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.5.0/tests/test_smbclient_os.py new/smbprotocol-1.5.1/tests/test_smbclient_os.py --- old/smbprotocol-1.5.0/tests/test_smbclient_os.py 2021-03-25 06:15:16.000000000 +0100 +++ new/smbprotocol-1.5.1/tests/test_smbclient_os.py 2021-05-08 01:01:14.000000000 +0200 @@ -13,6 +13,7 @@ import six import smbclient # Tests that we expose this in smbclient/__init__.py import stat +import time from smbclient._io import ( query_info, @@ -25,6 +26,10 @@ SMBFileIO, ) +from smbclient.shutil import ( + rmtree, +) + from smbprotocol.exceptions import ( SMBAuthenticationError, SMBOSError, @@ -1048,8 +1053,26 @@ def test_rename_fail_dst_different_root(smb_share): expected = "Cannot rename a file to a different root than the src." + server = [p for p in ntpath.normpath(smb_share).split("\\") if p][0] + with pytest.raises(ValueError, match=re.escape(expected)): - smbclient.rename(smb_share, "\\\\server2\\share\\dst") + smbclient.rename(smb_share, "\\\\%s\\dfs\\dst" % server) + + +def test_rename_dir(smb_share): + src_dir = ntpath.join(smb_share, 'src') + dst_dir = ntpath.join(smb_share, 'dst') + + smbclient.mkdir(src_dir) + with smbclient.open_file(ntpath.join(src_dir, 'file.txt'), mode='wb') as fd: + fd.write(b'test data') + + smbclient.rename(src_dir, dst_dir) + + assert smbclient.listdir(smb_share) == ['dst'] + assert smbclient.listdir(dst_dir) == ['file.txt'] + with smbclient.open_file(ntpath.join(dst_dir, 'file.txt'), mode='rb') as fd: + assert fd.read() == b'test data' def test_renames(smb_share): @@ -1100,6 +1123,19 @@ assert actual == u"Content" +def test_replace_sharing_violation(smb_share): + src_file = ntpath.join(smb_share, 'src.txt') + dst_file = ntpath.join(smb_share, 'dst.txt') + + with smbclient.open_file(src_file, mode='wb') as fd: + fd.write(b'test data') + + expected = re.escape('The process cannot access the file because it is being used by another process') + with smbclient.open_file(dst_file, mode='wb'): + with pytest.raises(SMBOSError, match=expected): + smbclient.replace(src_file, dst_file) + + def test_rmdir(smb_share): dir_name = "%s\\directory" % smb_share @@ -1289,6 +1325,61 @@ assert entry.is_file(follow_symlinks=False) is False # broken link target +def test_scandir_with_cache(smb_real): + share_path = u"%s\\%s" % (smb_real[4], u"P??t??s???-[%s] ????" % time.time()) + cache = {} + smbclient.mkdir(share_path, username=smb_real[0], password=smb_real[1], port=smb_real[3], connection_cache=cache) + + try: + + dir_path = ntpath.join(share_path, 'directory') + smbclient.makedirs(dir_path, exist_ok=True, connection_cache=cache) + + for name in ['file.txt', u'unicode ???[????].txt']: + with smbclient.open_file(ntpath.join(dir_path, name), mode='w', connection_cache=cache) as fd: + fd.write(u"content") + + for name in ['subdir1', 'subdir2', u'unicode dir ???[????]', 'subdir1\\sub']: + smbclient.mkdir(ntpath.join(dir_path, name), connection_cache=cache) + + count = 0 + names = [] + for dir_entry in smbclient.scandir(dir_path, connection_cache=cache): + assert isinstance(dir_entry, SMBDirEntry) + names.append(dir_entry.name) + + # Test out dir_entry for specific file and dir examples + if dir_entry.name == 'subdir1': + assert str(dir_entry) == "<SMBDirEntry: 'subdir1'>" + assert dir_entry.is_dir() is True + assert dir_entry.is_file() is False + assert dir_entry.stat(follow_symlinks=False).st_ino == dir_entry.inode() + assert dir_entry.stat().st_ino == dir_entry.inode() + elif dir_entry.name == 'file.txt': + assert str(dir_entry) == "<SMBDirEntry: 'file.txt'>" + assert dir_entry.is_dir() is False + assert dir_entry.is_file() is True + assert dir_entry.stat().st_ino == dir_entry.inode() + assert dir_entry.stat(follow_symlinks=False).st_ino == dir_entry.inode() + + assert dir_entry.is_symlink() is False + assert dir_entry.inode() is not None + assert dir_entry.inode() == dir_entry.stat().st_ino + + count += 1 + + assert count == 5 + assert u'unicode ???[????].txt' in names + assert u'unicode dir ???[????]' in names + assert u'subdir2' in names + assert u'subdir1' in names + assert u'file.txt' in names + + finally: + rmtree(share_path, connection_cache=cache) + smbclient.reset_connection_cache(connection_cache=cache) + + def test_stat_directory(smb_share): actual = smbclient.stat(smb_share) assert isinstance(actual, smbclient.SMBStatResult) @@ -1973,6 +2064,48 @@ assert not info.is_file() +def test_dfs_path_rename(smb_dfs_share): + test_dir = ntpath.join(smb_dfs_share, 'test folder') + smbclient.mkdir(test_dir) + + test_file = ntpath.join(smb_dfs_share, 'test file.txt') + with smbclient.open_file(test_file, mode='wb') as fd: + fd.write(b'test data') + + dst_dfs_path = ntpath.join(test_dir, 'target renamed.txt') + smbclient.rename(test_file, dst_dfs_path) + + assert smbclient.listdir(smb_dfs_share) == ['test folder'] + assert smbclient.listdir(test_dir) == ['target renamed.txt'] + + with smbclient.open_file(dst_dfs_path, mode='rb') as fd: + assert fd.read() == b'test data' + + +def test_dfs_path_replace(smb_dfs_share): + test_dir = ntpath.join(smb_dfs_share, 'test folder') + smbclient.mkdir(test_dir) + + test_file = ntpath.join(smb_dfs_share, 'test file.txt') + with smbclient.open_file(test_file, mode='wb') as fd: + fd.write(b'other data') + + dst_dfs_path = ntpath.join(test_dir, 'target renamed.txt') + with smbclient.open_file(dst_dfs_path, mode='wb') as fd: + fd.write(b'test data') + + smbclient.replace(dst_dfs_path, test_file) + + actual_contents = smbclient.listdir(smb_dfs_share) + assert 'test folder' in actual_contents + assert 'test file.txt' in actual_contents + + assert smbclient.listdir(test_dir) == [] + + with smbclient.open_file(test_file, mode='rb') as fd: + assert fd.read() == b'test data' + + def test_broken_dfs_path(smb_real): dfs_path = u'\\\\' + ntpath.join(smb_real[2], 'dfs', 'broken')