def open(self, desired_access=None, shared_access=None, open_options=None): """Open file. :param desired_access: desired access (see e3.os.windows.native_api.Access) :type desired_access: int :param shared_access: sharing parameters (see e3.os.windows.native_api.Shared) :type shared_access: int :param open_options: open options (see e3.os.windows.native_api.OpenOptions) :type open_options: int """ if desired_access is None: desired_access = self.desired_access if shared_access is None: shared_access = self.shared_access if open_options is None: open_options = self.open_options self.handle = HANDLE() status = NT.OpenFile( pointer(self.handle), desired_access, pointer(self.attr), pointer(self.io_status), shared_access, open_options, ) if status < 0: raise NTException( status=status, message="cannot open file %s" % self.path, origin="NTFile.open", )
def uid(self): """Retrieve the ID of the file. On NTFS system we are sure that this ID is unique on the given volume :return: the uid :rtype: int ;raise: NTException """ result = FileInfo.Internal() status = NT.QueryInformationFile( self.handle, pointer(self.io_status), pointer(result), sizeof(result), FileInfo.Internal.class_id, ) if status < 0: # defensive code # we should already have raised an error here when trying # to open the file raise NTException(status=status, message="cannot find file uid", origin="NTFile.uid") return result.index_number
def read_attributes(self): """Retrieve file basic information. It updates the basic_info attribute including timestamp information :raise: NTException """ result = FileInfo.Basic() status = NT.QueryInformationFile( self.handle, pointer(self.io_status), pointer(result), sizeof(result), FileInfo.Basic.class_id, ) if status < 0: # defensive code # we should already have raised an error here when trying # to open the file raise NTException( status=status, message="cannot read attributes", origin="NTFile.read_attributes", ) self.basic_info = result return result
def rename(self, filename: str, replace: bool = False) -> None: """Move file. :param filename: target location :param replace: if True replace the target file if it exists :raise: NTException """ file_target = "\\??\\%s" % os.path.abspath(filename) target = file_target.encode("utf_16_le") s = "?PL%ss" % len(target) b = create_string_buffer(struct.calcsize(s)) b.raw = struct.pack(s, replace, 0, len(target), target) set_infofile: Callable = NT.SetInformationFile # type: ignore status = set_infofile( self.handle, pointer(self.io_status), b, struct.calcsize(s), FileInfo.Rename.class_id, ) if status < 0: raise NTException( status=status, message=f"move of {self.path} to {filename} failed", origin="NTFile.rename", )
def rename(self, filename, replace=False): """Move file. :param filename: target location :type filename: unicode :param replace: if True replace the target file if it exists :type replace: bool :raise: NTException """ target = "\\??\\%s" % os.path.abspath(filename) target = target.encode("utf_16_le") s = "?PL%ss" % len(target) b = create_string_buffer(struct.calcsize(s)) b.raw = struct.pack(s, replace, 0, len(target), target) status = NT.SetInformationFile( self.handle, pointer(self.io_status), b, struct.calcsize(s), FileInfo.Rename.class_id, ) if status < 0: raise NTException( status=status, message="move of %s to %s failed" % (self.path, filename), origin="NTFile.rename", )
def volume_path(self): """Retrieve path to the volume containing the file. :return: path to a windows volume after junction resolution :rtype: unicode :raise: NTException """ result = create_unicode_buffer(1024) status = NT.GetVolumePathName(c_wchar_p(self.path), result, 1024) if not status: raise NTException(status=status, message="cannot find volume for %s" % self.path, origin="NTFile.volume_path") return result.value
def dispose(self): """Remove the file (low level). The remove is effective on call to close method """ fd = FileInfo.Disposition(1) status = NT.SetInformationFile(self.handle, pointer(self.io_status), pointer(fd), sizeof(FileInfo.Disposition), FileInfo.Disposition.class_id) if status < 0: raise NTException(status=status, message="cannot dispose", origin="NTFile.dispose")
def write_attributes(self): """Update file attributes. :raise: NTException """ status = NT.SetInformationFile(self.handle, pointer(self.io_status), pointer(self.basic_info), sizeof(self.basic_info), FileInfo.Basic.class_id) if status < 0: raise NTException(status=status, message='cannot write attributes to %s' % self.path, origin='NTFile.write_attributes') self.read_attributes()
def read_attributes(self): """Retrieve file basic information. It updates the basic_info attributes :raise: NTException """ status = NT.QueryAttributesFile(pointer(self.attr), pointer(self.basic_info)) logger.debug('read_attributes status: %d', status) if status < 0: raise NTException(status=status, message="cannot query attributes %s" % self.path, origin="NTFile.read_attributes") return self.basic_info
def read_attributes_internal(self): """Retrieve file basic attributes (internal function). The function is used internally to check file basic attributes (kind of entry and windows attributes such as readonly). Retrieved attributes are stored in basic_info Python attribute. It requires less rights than the read_attributes method. """ status = NT.QueryAttributesFile(pointer(self.attr), pointer(self.basic_info)) if status < 0: raise NTException( status=status, message="cannot query attributes %s" % self.path, origin="NTFile.read_attributes_internal", )
def iterate_on_dir(self, fun, default_result=None): """Iterate on directory. :param fun: function called on each entry (. are .. are skipped) :param default_result: default return value :return: last return value or fun or default_result """ result = default_result s_size = struct.calcsize("LLL") b_size = 100 * 1024 b = create_string_buffer(b_size) status = NT.QueryDirectoryFile(self.handle, None, None, None, pointer(self.io_status), b, b_size, FileInfo.Names.class_id, False, None, True) if status == Status.NO_MORE_FILES: # defensive code # In theory this case should not occurs at it means that the # directory does not even have the . and .. entries. In practice # it can occurs (probably because of an intermediate state). # In that case behave as if the directory is empty return result if status < 0: raise NTException(status=status, message="can't read dir %s" % self.path, origin="NTFile.iterate_on_dir") while status >= 0 and status != Status.NO_MORE_FILES: pos = 0 while True: off, _, size = struct.unpack_from("LLL", b.raw, pos) name = b.raw[pos + s_size:pos + s_size + size].decode('utf-16-le') if name != u"." and name != "..": result, should_exit = fun(name, self) if should_exit: return result if off == 0: break pos += off status = NT.QueryDirectoryFile(self.handle, None, None, None, pointer(self.io_status), b, b_size, FileInfo.Names.class_id, False, None, False) return result
def volume_path(self) -> str: """Retrieve path to the volume containing the file. :return: path to a windows volume after junction resolution :raise: NTException """ result = create_unicode_buffer(1024) get_vol_name: Callable = NT.GetVolumePathName # type: ignore status = get_vol_name(c_wchar_p(self.path), result, 1024) if not status: raise NTException( status=status, message=f"cannot find volume for {self.path}", origin="NTFile.volume_path", ) return result.value
def iterate_on_dir(self, fun, default_result=None): """Iterate on directory. :param fun: function called on each entry (. are .. are skipped) :param default_result: default return value :return: last return value or fun or default_result """ result = default_result s_size = struct.calcsize("LLL") b_size = 100 * 1024 b = create_string_buffer(b_size) status = NT.QueryDirectoryFile(self.handle, None, None, None, pointer(self.io_status), b, b_size, FileInfo.Names.class_id, False, None, True) if status == Status.NO_MORE_FILES: return result if status < 0: raise NTException(status=status, message="can't read dir %s" % self.path, origin="NTFile.iterate_on_dir") while status >= 0 and status != Status.NO_MORE_FILES: pos = 0 while True: off, _, size = struct.unpack_from("LLL", b.raw, pos) name = b.raw[pos + s_size:pos + s_size + size].decode('utf-16-le') if name != u"." and name != "..": result, should_exit = fun(name, self) if should_exit: return result if off == 0: break pos += off status = NT.QueryDirectoryFile(self.handle, None, None, None, pointer(self.io_status), b, b_size, FileInfo.Names.class_id, False, None, False) return result
def uid(self): """Retrieve the ID of the file. On NTFS system we are sure that this ID is unique on the given volume :return: the uid :rtype: int ;raise: NTException """ result = FileInfo.Internal() status = NT.QueryInformationFile(self.handle, pointer(self.io_status), pointer(result), sizeof(result), FileInfo.Internal.class_id) if status < 0: raise NTException(status=status, message='cannot find file uid', origin="NTFile.uid") return result.index_number
def write_attributes(self) -> None: """Update file attributes. :raise: NTException """ set_infofile: Callable = NT.SetInformationFile # type: ignore status = set_infofile( self.handle, pointer(self.io_status), pointer(self.basic_info), sizeof(self.basic_info), FileInfo.Basic.class_id, ) if status < 0: raise NTException( status=status, message=f"cannot write attributes to {self.path}", origin="NTFile.write_attributes", ) self.read_attributes()
def open( self, desired_access: Optional[int] = None, shared_access: Optional[int] = None, open_options: Optional[int] = None, ) -> None: """Open file. :param desired_access: desired access (see e3.os.windows.native_api.Access) :param shared_access: sharing parameters (see e3.os.windows.native_api.Shared) :param open_options: open options (see e3.os.windows.native_api.OpenOptions) """ if desired_access is None: desired_access = self.desired_access if shared_access is None: shared_access = self.shared_access if open_options is None: open_options = self.open_options self.handle = HANDLE() open_file: Callable = NT.OpenFile # type: ignore status = open_file( pointer(self.handle), desired_access, pointer(self.attr), pointer(self.io_status), shared_access, open_options, ) if status < 0: raise NTException( status=status, message=f"cannot open file {self.path}", origin="NTFile.open", )
def unlink(self): """Remove file safely. :raise: NTException """ open_options = self.open_options is_in_trash = False # First we need to check that file is not mark readonly try: self.read_attributes_internal() except NTException as e: if e.status == Status.OBJECT_NAME_NOT_FOUND: return elif e.status == Status.DELETE_PENDING: return else: raise if self.is_readonly: # Try to remove the readonly flag self.basic_info.file_attributes.attr &= ~FileAttribute.READONLY self.write_attributes() # set our access modes desired_access = Access.DELETE shared_access = Share.DELETE if self.is_dir: desired_access |= Access.LIST_DIRECTORY | Access.SYNCHRONIZE open_options |= OpenOptions.SYNCHRONOUS_IO_NON_ALERT try_counter = 10 # Open the file for deletion while try_counter > 0: try: self.open(desired_access, shared_access, open_options) break except NTException as e: if e.status == Status.SHARING_VIOLATION: # File is already open elsewhere for a non delete operation # Try a few times to open it with relaxed share settings shared_access = Share.ALL elif e.status == Status.DELETE_PENDING: # defensive code # file is already pending deletion (just after our call # to read_attributes) so consider the deletion # is done and return return else: # defensive code # We don't know what to do here so just fail raise # Wait a few ms before attempting again to open the file NT.Sleep(5) try_counter -= 1 if try_counter == 0: raise NTException( status=1, message="cannot open file %s for deletion" % self.path, origin="NTFile.unlink", ) # From there we assume that the file has been opened try: if shared_access == Share.ALL: # The file is also opened elsewhere for a non delete operation # In that case we will try to move it to the trash # first check that the directory is empty if self.is_dir and not self.is_dir_empty: raise NTException( status=1, message="directory not empty: %s" % self.path, origin="NTFile.unlink", ) self.move_to_trash() is_in_trash = True # If the file has been moved away then we try to delete it in the # trash but it is not necessary to try as hard as the goal is to # just keep the trash space used as low as possible. if is_in_trash: try_counter = 5 else: try_counter = 20 while try_counter > 0: try: self.dispose() break except NTException as e: if e.status == Status.DIRECTORY_NOT_EMPTY: # The directory is not empty but that might be because # of remaining files in PENDING_DELETE status. Our # is_dir_empty method return empty if the directory # contains only files pending for deletion if not self.is_dir_empty: raise NTException( status=e.status, message="dir %s is not empty" % self.path, origin="NTFile.unlink", ) elif e.status == Status.CANNOT_DELETE: # defensive code # At this stage we are sure that the file is not # read_only but it seems that we can get this error # when the file has been mapped to memory. if not is_in_trash: try: self.move_to_trash() is_in_trash = True try_counter = min(try_counter, 5) except NTException: pass else: # defensive code # Unknown error. If the file has been moved away # consider it success. Otherwise reraise exception if is_in_trash: break raise NT.Sleep(5) try_counter -= 1 finally: self.close()