Possible to grab the modified date of a file on numerous Windows machines?

Hello,

I’ve just started learning Python.

We have an important file that sites on our Windows machines that sit in remote factories. I need to pull this modified date from each machine and wonder if Python can do that? These machines are not domain’d, but I have a local username and password to get remote access.

I’d like the output to be in csv for starters. So poll time, IP/Hostname, Modified Date.

I did find this script and wonder what you thought?

import os
import socket
import ctypes
from ctypes import wintypes

def get_unc_credentials(remote_path):
    # Get the username and password for the UNC path
    username = input("Enter the username: ")
    password = input("Enter the password: ")
    return username, password

def remote_check_modified_date(remote_path):
    try:
        # Check if the path is a UNC path (starts with \\)
        if remote_path.startswith('\\\\'):
            # Split the UNC path into server and share
            server, share, path = remote_path[2:].split('\\', 2)
            
            # Check if the server is reachable
            if socket.gethostbyname(server):
                # Construct the full UNC path
                full_unc_path = f'\\\\{server}\\{share}\\{path}'

                # Get credentials for the UNC path
                username, password = get_unc_credentials(full_unc_path)

                # Use the Windows API to authenticate with the remote server
                net_use_info = wintypes.NETRESOURCE()
                net_use_info.lpRemoteName = full_unc_path
                net_use_info.dwType = 0
                net_use_info.lpLocalName = None
                net_use_info.lpProvider = None

                result = ctypes.windll.mpr.WNetAddConnection2W(
                    ctypes.byref(net_use_info),
                    password.encode('utf-16le'),
                    username.encode('utf-16le'),
                    0
                )

                if result == 0:  # Success
                    # Get the file information
                    file_stat = os.stat(full_unc_path)
                    
                    # Get the modified time from the file information
                    modified_time = file_stat.st_mtime
                    
                    print(f"Modified date of {remote_path}: {modified_time}")

                    # Disconnect from the remote server
                    ctypes.windll.mpr.WNetCancelConnection2W(full_unc_path, 0, 1)

                else:
                    print(f"Error: Unable to connect to {full_unc_path}, error code: {result}")

            else:
                print(f"Error: Server {server} not reachable")

        else:
            print("Error: Not a valid UNC path")

    except Exception as e:
        print(f"Error: {e}")

# Example usage
remote_check_modified_date('\\\\remote_host\\share\\path\\to\\file.txt')

I’m yet to test this until I’m onsite and will need a way to look at a list of multiple IP/Hostnames and send to csv. Getting just 1 remote IP working would be great though.

Thanks

My main advice is to avoid the arrow anti-pattern. The Zen of Python tells us that “flat is better than nested”.

We can do this by using “guard clauses” instead, and by moving generic exception handling (except Exception as e:) out of the function (if we use it at all). The latter also allows the calling code to control what happens if there is an exception (what if a user doesn’t want to print a message?).

Better yet, instead of printing our own error messages and exiting the function, we can raise our own exceptions deliberately. That way the calling code can use its own exception handling to decide what to do about the problem, and it can know that a problem occurred (it has no way to “read” the printed messages).

In fact, we cannot detect a problem with socket.gethostbyname in the way that you imagined. If the server is not reachable, it will raise an exception - it will not return an empty string, False, None or any other such value that we could test with if. So, better if we just allow that exception to fall through, and let the caller handle it (since we already decided on that).

That gives us:

def remote_check_modified_date(remote_path):
    # Check if the path is a UNC path (starts with \\)
    if not remote_path.startswith('\\\\'):
        raise ValueError("Not a valid UNC path")

    # Split the UNC path into server and share
    server, share, path = remote_path[2:].split('\\', 2)
            
    # Check if the server is reachable (raise an exception otherwise)
    socket.gethostbyname(server)

    # Construct the full UNC path
    full_unc_path = f'\\\\{server}\\{share}\\{path}'

    # Get credentials for the UNC path
    username, password = get_unc_credentials(full_unc_path)

    # Use the Windows API to authenticate with the remote server
    net_use_info = wintypes.NETRESOURCE()
    net_use_info.lpRemoteName = full_unc_path
    net_use_info.dwType = 0
    net_use_info.lpLocalName = None
    net_use_info.lpProvider = None

    result = ctypes.windll.mpr.WNetAddConnection2W(
        ctypes.byref(net_use_info),
        password.encode('utf-16le'),
        username.encode('utf-16le'),
        0
    )

    if result == 0:  # Success
        # Get the file information
        file_stat = os.stat(full_unc_path)
                    
        # Get the modified time from the file information
        modified_time = file_stat.st_mtime
                    
        print(f"Modified date of {remote_path}: {modified_time}")

        # Disconnect from the remote server
        ctypes.windll.mpr.WNetCancelConnection2W(full_unc_path, 0, 1)

    else:
        raise RuntimeError(f"Unable to connect to {full_unc_path}, error code: {result}")

try:
    remote_check_modified_date('\\\\remote_host\\share\\path\\to\\file.txt')
except Exception as e:
    print(f"Error: {e}")

I left the last if condition as it was, because at thie point we are so far into the logic that it doesn’t help to use the guard-clause technique again. Generally the idea is to put a few simple checks at the beginning, and if we get past them, we know that our basic expectations are met.

The next advice is to reduce comments. If you have a single line that calls a function called get_unc_credentials, for example, then you don’t need to explain that it is getting the UNC credentials - the name of the function was chosen for a reason. We might use a comment if we have to work with some old, low-level API and the names don’t seem very obvious.)

On the other hand, if you have several lines of code that represent some sub-task, that is exactly what functions are for - so let’s pull that out into its own function. If we do that, we may find that using another guard clause makes sense. Or we could make that function use an exception to report an error. (The main reason the Windows API doesn’t already do that is because it was originally designed for C, which doesn’t support exceptions.)

Finally, I noticed that you currently split the remote_path into parts, but then put it back together exactly again in order to get the full_unc_path. You should be able to prove to yourself that the full_unc_path that your function calculates is the same as the original remote_path, so let’s just use that.

Applying these principles gives:

def authenticate_with_windows_API(remote_path):
    username, password = get_unc_credentials(remote_path)

    net_use_info = wintypes.NETRESOURCE()
    net_use_info.lpRemoteName = remote_path
    net_use_info.dwType = 0
    net_use_info.lpLocalName = None
    net_use_info.lpProvider = None

    error_code = ctypes.windll.mpr.WNetAddConnection2W(
        ctypes.byref(net_use_info),
        password.encode('utf-16le'),
        username.encode('utf-16le'),
        0
    )

    if error_code:
        raise RuntimeError(f"Unable to connect to {remote_path}, error code: {result}")

def remote_check_modified_date(remote_path):
    if not remote_path.startswith('\\\\'):
        raise ValueError("Not a valid UNC path")

    # Check if the server is reachable (raise an exception otherwise)
    socket.gethostbyname(remote_path[2:].split('\\')[0])

    authenticate_with_windows_API(remote_path)
                    
    modified_time = os.stat(remote_path).st_mtime
    print(f"Modified date of {remote_path}: {modified_time}")

    # Since we connected to the server in authenticate_with_windows_API,
    # we need to disconnect now.
    ctypes.windll.mpr.WNetCancelConnection2W(remote_path, 0, 1)

try:
    remote_check_modified_date('\\\\remote_host\\share\\path\\to\\file.txt')
except Exception as e:
    print(f"Error: {e}")