Detecting files in the process of being copied or written to in Python

Detecting files in the process of being copied or written to in Python

Learn how to detect whether a given file is being copied or currently being modified/written to using pure Python.


If you have an application that relies on detecting, moving or copying files on a host, it can be quite dangerous to simply access these files without first confirming they are not being manipulated by another process or are currently in the process of being copied or written to. In such a situation, you'll first want to check that this is not the case, wait if necessary and the only proceed with accessing the necessary file.


What is the best way to measure changes to a file in Python?


Method 1:


The most accurate way to measure changes to a file is to directly compare the new version of the file to the old version. This can be done by storing a checksum of the initial file, waiting over a predetermined polling period, then comparing the old checksum to the new one. This will accurately measure any changes made to the file. 


The issue with this method is that for larger files it can take quite some time and processing power to calculate a checksum of the file. If you are working with files that are larger than just a few megabytes, you could run in to issues such as high CPU usage, long wait times or running out of memory entirely. So for larger files you may want to consider another method.


Method 2:


The next best way to measure whether a file is in the process of being modified is to analyze its size over time, this can be be done by either reading the file's properties from the operating system, or to open the file and measure its size from there.


The issue with using the file properties from the operating system is that it may not be accurate depending on the operating system you are using. For example, when you copy a large file in Windows, the created file will show its final size throughout the copying process, not its current size. So to create a platform independent solution you won't be able to go this route.


A better solution is to open the file in read-only mode and calculate the file's size. The difficult part about this is to avoid reading the entire file into memory in order to measure its size, as this could make the process extremely slow for larger files, this can however be avoided using some file mechanic trickery.


Detecting changed made to large files using size comparison


To measure the size of a large file in Python and not suffer any serious performance issues is not as difficult as it may seem, all you really need to do is make use of the file object's read/write pointer. Simply open the file in Python and move the read/write pointer to the end of the file using the seek() method, then retrieve its position using the tell() method, this position is equal to the size of the file in bytes.


To set the pointer to the end of a file you can use seek(0, 2), this means set the pointer to position 0 relative to the end of the file (0 = absolute positioning, 1 = relative to the start of the file, 2 = relative to the end of the file).


def get_file_size(filepath):           
    # open the file in read only           
    with open(filepath, "r") as file:           
        # move pointer to the end of the file           
        file.seek(0, 2)           
        # retrieve the current position of the pointer           
        # this will be the file's size in bytes           
        size = file.tell()           
        return size           
    # if the function reaches this statement it means an error occurred within the above context handler           
    return False


Certain operating systems may not allow you to open the file (even just in reading mode) while it is being written to, so if an error occurs relating to that, this function will return False, if this is the case you can assume the file is/was being modified.


Now that you can accurately determine the size of a file at a given point, you'll need to create a piece of code that measures a file's size at two different intervals. If the size differs during the two intervals, you know there has been a modification made to the file.


To wait a specified amount of time you can make use of the sleep() method which can be imported from the time module.


For this example, we'll measure the initial size of the file, wait 2 seconds, then measure it again and compare the two sizes to see if it changed.


from time import sleep  
# include get_file_size() function here  
initial_size = get_file_size("testfile.txt")      
sleep(2)      
final_size = get_file_size("testfile.txt")      
# if the initial size is equal to the final size, the file has most likely      
# not changed, unless they are both False.      
if initial_size == final_size and initial_size != False:      
    print("File has not changed!")      
else:      
    print("File has been modified!")


In the above example, if you run this application and leave the target file untouched, the script will let you know that it has not been modified. If you try modify the file during the poll time, it will let you know that it has been modified.


Note:

if both file sizes come back as False, then we can assume the files were in the middle of being written to while attempting to access them, assuming that you have set up your permissions on the file correctly,


Detecting changes to smaller files using an MD5 checksum


Measuring a file's size gives us a pretty good idea of whether a file has been modified, but if a file is changed in such as way that it remains the same size after being modified, such as a single character being replaced, then comparing sizes will be ineffective


Creating an MD5 hash of the entire file before and after a predetermined polling period can tell whether a file has been modified with a high amount of accuracy. Although this method may not be suitable for large files (unless performance and time is not an issue) it is much safer to use this method whenever possible.


To generate an MD5 checksum of a file, we can make use of the

hashlib module

which is a default package in Python. The md5() function generates a hash using a given string, then we can retrieve that hash using the hexdigest() or digest() function, the only difference between those two functions is that hexdigest() returns the hash in the form of hexadecimals instead of bytes.


In this example we create a function that generates an MD5 hash from the contents of a given file.


import hashlib   
def get_file_md5(filepath):    
    # open the file in read only    
    with open(filepath, "rb") as file:    
        file_contents = file.read()    
        # generate and return the md5 hash    
        return hashlib.md5(file_contents).hexdigest()    
    # if the function reaches this statement it means an error occurred within the above context handler    
    return False


If you do want to detect modifications to larger files in this manner, consider making use of the update() function to feed your file in chunks to the MD5 function, this is more memory efficient.


Just as we did in the first method, to determine whether a file has been modified, all we need to do is call our comparison function at two intervals, then compare the output.


from time import sleep  
import hashlib   
# include get_file_md5() function here  
initial_md5 = get_file_md5("testfile.txt")   
sleep(2)   
final_md5 = get_file_md5("testfile.txt")   
# if the initial hash is equal to the final hash, the file not changed      
# unless they are both False.     
if initial_md5 == final_md5 and initial_md5 != False:   
    print("File has not changed!")   
else:   
    print("File has been modified!")


Detecting any changes made to an entire directory


To compare all files within a directory, all you need to do is create a function to iterate over the contents of a folder, creating a hash or measuring its size and storing that result in a dictionary, if the item you are iterating over is a sub-directory, we can recursively call the same function.


import os  
import hashlib   
  
# include get_file_md5() function here  
def scan_dir(path):  
    # list all items in the directory  
    filelist = os.listdir(path)  
    hashes = []  
    for file in filelist:  
        filepath = path + file  
        # generate a hash if it's a file and add it to the output list  
        if os.path.isfile(filepath):  
            file_hash = get_file_md5(filepath)  
            # return False if any files are found to be in use  
            if file_hash == False:  
                return False  
            hashes.append({filepath: file_hash})  
        # generate hashed for all files in a sub-directory, add the output to the list  
        elif os.path.isdir(filepath):  
            dir_contents = scan_dir(filepath + "/")  
            # return False if any files are found to be in use  
            if dir_contents == False:  
                return False  
            hashes.append({filepath: dir_contents})  
    return hashes  


To see if anything has changed in the directory, all you need to do is compare the output of this function over a given interval just as we did in the first two examples


import os  
import hashlib  
# list the current directory 
initial_list = scan_dir("./")  
sleep(2)  
final_list = scan_dir("./")  
# if the initial list is equal to the final list, no files in the directory      
# have changed, unless they are both False.    
if initial_list == final_list and initial_list != False:  
    print("Directory has not changed!")  
else:  
    print("Directory has been modified!")


If you have any questions feel free to leave a comment below!



Christopher Thornton@Instructobit 3 years ago
or