Search code examples
pythonwindowsfile-attributes

How to read or write the a s h r i file attributes on Windows using Python and ctypes?


For the records:

  • a means 'archivable'
  • s means 'system'
  • h means 'hidden'
  • r means 'readonly'
  • i means 'indexable'

My current solution to read/write these attributes from Python scripts is to call attrib using the subprocess module.

Python code:

import os, subprocess

def attrib(path, a=None, s=None, h=None, r=None, i=None):
    attrs=[]
    if r==True:    attrs.append('+R')
    elif r==False: attrs.append('-R')
    if a==True:    attrs.append('+A')
    elif a==False: attrs.append('-A')
    if s==True:    attrs.append('+S')
    elif s==False: attrs.append('-S')
    if h==True:    attrs.append('+H')
    elif h==False: attrs.append('-H')
    if i==True:    attrs.append('+I')
    elif i==False: attrs.append('-I')

    if attrs: # write attributes
        cmd = attrs
        cmd.insert(0,'attrib')
        cmd.append(path)
        cmd.append('/L')
        return subprocess.call(cmd, shell=False)

    else: # just read attributes
        output = subprocess.check_output(
            ['attrib', path, '/L'],
            shell=False, universal_newlines=True
        )[:9]
        attrs = {'A':False, 'S':False, 'H':False, 'R':False, 'I':False}
        for char in output:
            if char in attrs:
                attrs[char] = True
        return attrs

path = 'C:\\test\\'
for thing in os.listdir(path):
    print(thing, str(attrib(os.path.join(path,thing))))

Output:

archivable.txt {'A': True, 'I': False, 'S': False, 'H': False, 'R': False}
hidden.txt {'A': True, 'I': False, 'S': False, 'H': True, 'R': False}
normal.txt {'A': True, 'I': False, 'S': False, 'H': False, 'R': False}
readonly.txt {'A': True, 'I': False, 'S': False, 'H': False, 'R': True}
system.txt {'A': True, 'I': False, 'S': True, 'H': False, 'R': False}

But this performs slow when the directory contains many entries (one subprocess call per entry).

I don't want to use the win32api module because I don't want third party module dependencies. Also, I'm curious how to do it with ctypes.

I have stumbled over Hide Folders/ File with Python [closed], Set "hide" attribute on folders in windows OS? and Python: Windows System File, but this is not clear to me. Especially, I don't understand what these 0x4 es 0x02 es are. Can you explain this? Can you give a concrete code example?


Solution

  • With the help of eriksuns comments to my question i solved it. Here is the code from my question but now using ctypes, stat and os.scandir. It requires Python 3.5+. Writes are ~50 times faster and reads are ~900 times faster.

    Python code:

    from os import scandir, stat
    from stat import (
        FILE_ATTRIBUTE_ARCHIVE as A,
        FILE_ATTRIBUTE_SYSTEM as S,
        FILE_ATTRIBUTE_HIDDEN as H,
        FILE_ATTRIBUTE_READONLY as R,
        FILE_ATTRIBUTE_NOT_CONTENT_INDEXED as I
    )
    from ctypes import WinDLL, WinError, get_last_error
    
    def read_or_write_attribs(
        # https://docs.python.org/3/library/ctypes.html#ctypes.WinDLL
        kernel32,
        
        # https://docs.python.org/3/library/os.html#os.DirEntry
        entry,
        
        # archive, system, hidden, readonly, indexed
        a=None, s=None, h=None, r=None, i=None,
        
        # Set to True when you call this function more than once on the same entry.
        update=False
    ):
    
        # Get the file attributes as an integer.
        if not update:
            # Fast because we access the stats from the entry
            attrs = entry.stat(follow_symlinks=False).st_file_attributes
        else:
            # A bit slower because we re-read the stats from the file path.
            # Notice that this will raise a "WinError: Access denied" on some entries,
            # for example C:\System Volume Information\
            attrs = stat(entry.path, follow_symlinks=False).st_file_attributes
    
        # Construct the new attributes
        newattrs = attrs
        def setattrib(attr, value):
            nonlocal newattrs
            # Use '{0:032b}'.format(number) to understand what this does.
            if value is True: newattrs = newattrs | attr
            elif value is False: newattrs = newattrs & ~attr
        setattrib(A, a)
        setattrib(S, s)
        setattrib(H, h)
        setattrib(R, r)
        
        # Because this attribute is True when the file is _not_ indexed
        setattrib(I, i if i is None else not i)
    
        # Optional add more attributes here.
        # See https://docs.python.org/3/library/stat.html#stat.FILE_ATTRIBUTE_ARCHIVE
    
        # Write the new attributes if they changed
        if newattrs != attrs:
            if not kernel32.SetFileAttributesW(entry.path, newattrs):
                raise WinError(get_last_error())
    
        # Return an info tuple consisting of bools
        return (
            bool(newattrs & A),
            bool(newattrs & S),
            bool(newattrs & H),
            bool(newattrs & R),
    
            # Because this attribute is true when the file is _not_ indexed
            not bool(newattrs & I)
        )
    
    # Test it
    if __name__ == '__main__':
    
        # Contains 'myfile.txt' with default attributes
        path = 'C:\\test\\'
        
        kernel32 = WinDLL('kernel32', use_last_error=True)
    
        # Tool for prettyprinting to the console
        template = '  {} (a={}, s={}, h={}, r={}, i={})'
        def pp (attribs):
            print(template.format(
                entry.path,
                *attribs
            ))
    
        print('\nJust read the attributes (that is quick):')
        for entry in scandir(path):
            pp(read_or_write_attribs(kernel32, entry))
    
        print("\nSet 'readonly' to true (that is quick):")
        for entry in scandir(path):
            pp(read_or_write_attribs(kernel32, entry, r=True))
    
        print(
            "\nSet 'system' to true, then set 'system' to false, "
            "then set 'readonly' to false (that is slow):"
        )
        for entry in scandir(path):
            pp(read_or_write_attribs(
                kernel32, entry,
                s=True
            ))
            pp(read_or_write_attribs(
                kernel32, entry,
                s=False,
                update=True
            ))
            pp(read_or_write_attribs(
                kernel32, entry,
                r=False,
                update=True
            ))
    

    Output:

    C:\>ashri_example.py
    
    Just read the attributes (that is quick):
      C:\test\myfile.txt (a=True, s=False, h=False, r=False, i=True)
    
    Set 'readonly' to true (that is quick):
      C:\test\myfile.txt (a=True, s=False, h=False, r=True, i=True)
    
    Set 'system' to true, then set 'system' to false, then set 'readonly' to false (slow):
      C:\test\myfile.txt (a=True, s=True, h=False, r=True, i=True)
      C:\test\myfile.txt (a=True, s=False, h=False, r=True, i=True)
      C:\test\myfile.txt (a=True, s=False, h=False, r=False, i=True)
    
    C:\>