4

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?

Nils Lindemann
  • 1,146
  • 1
  • 16
  • 26
  • 1
    File attributes are stored as a bitmap in a 32-bit number, with each bit corresponding to an attribute. Bit 0 is `2**0 == 1`. Bit 1 is `2**1 == 2`, and so on. Here's the complete list of [file attribute constants](https://msdn.microsoft.com/en-us/library/gg258117). To check for an attribute use a bitwise AND (i.e. operator `&`). For example: `readonly = attrs & FILE_ATTRIBUTE_READONLY`. To add an attribute, use a bitwise OR (i.e. operator `|`). For example: `attrs |= FILE_ATTRIBUTE_READONLY`. – Eryk Sun Nov 01 '16 at 21:29
  • 1
    For reading, Python 3.5+ `stat` has [`st_file_attributes`](https://docs.python.org/3/library/os.html#os.stat_result.st_file_attributes), and the attribute constants are defined in the [stat module](https://docs.python.org/3/library/stat.html#stat.FILE_ATTRIBUTE_ARCHIVE). The fastest way to stat all of the files in a directory in 3.5 is to use [`os.scandir`](https://docs.python.org/3/library/os.html#os.scandir). – Eryk Sun Nov 01 '16 at 21:31
  • 2
    For writing file attributes you'll have to use ctypes to call [`SetFileAttributesW`](https://msdn.microsoft.com/en-us/library/aa365535). For example: `kernel32 = ctypes.WinDLL('kernel32', use_last_error=True);` `if not kernel32.SetFileAttributesW(u"path\\to\\file", attrs): raise ctypes.WinError(ctypes.get_last_error())`. Make sure the path is a Unicode string if you're using Python 2. – Eryk Sun Nov 01 '16 at 21:39
  • @eryksun Thank you so much, that was helpful! – Nils Lindemann Nov 02 '16 at 04:47

2 Answers2

3

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:\>
Nils Lindemann
  • 1,146
  • 1
  • 16
  • 26
  • 1
    Some suggestions. Load `kernel32` only once as a module global. In `set`, replace `attrib ^ 4294967295` with `~attrib`. In `get`, replace `not not (attrs & what)` with `bool(attrs & what)`. – Eryk Sun Nov 02 '16 at 05:22
  • 1
    The code in the question has `i=None`. That corresponds to `FILE_ATTRIBUTE_NOT_CONTENT_INDEXED`, but the attribute is defined negatively. You can set the inverted value, e.g. `not_i = i if i is None else not i`. For `get` the tuple needs a flag value to indicate that the value of `'I'` is the logical negation of `attrs & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED`. – Eryk Sun Nov 02 '16 at 05:48
  • 1
    Also, in the question you use `/L` in the `attrib` command line. So the stat call should be `dirEntry.stat(follow_symlinks=False)`. – Eryk Sun Nov 02 '16 at 05:52
  • You're welcome. Is this performing significantly better for a directory with a lot of files? – Eryk Sun Nov 02 '16 at 07:12
  • i just tested it on a directory with 1000 files, measuring it with timeit (one run). Reading and writing file attributes: 5.6 seconds versus 0.12 seconds - 47 times faster. Reading file attributes: 5.6 seconds versus 0.0065 seconds - 862 times faster. Thats a wow :) – Nils Lindemann Nov 02 '16 at 07:43
  • @eryksun i have updated the code again, as i ran into errors when i reused a DirEntry object. Therefore the attrib function has now an additional parameter `useCache`. Also it now returns a tuple instead of a dict which makes the code cleaner. – Nils Lindemann Nov 06 '16 at 20:23
0

I just cooked a little more compact version of this if anyone is interested.

import stat
import ctypes

def set_file_attrib(file_path: str, attr: int, state: bool):
    current = os.stat(file_path).st_file_attributes
    if state:
        changed = current | attr
    else:
        changed = current & ~attr

    if current != changed:
        if not ctypes.windll.kernel32.SetFileAttributesW(file_path, changed):
            raise ctypes.WinError(ctypes.get_last_error())

So one can do:

set_file_attrib(file_path, stat.FILE_ATTRIBUTE_READONLY, False)

set_file_attrib(another_path, stat.FILE_ATTRIBUTE_READONLY | stat.FILE_ATTRIBUTE_ARCHIVE, True)

Sure, your can only turn on OR off certain ones but I'm not after more.

Thanks @Nils Lindemann & @Eryk Sun Weird that there is still no built-in.

ewerybody
  • 1,443
  • 16
  • 29
  • Thanks. I simplified my example a bit. BTW, my name is _Nils_ Lindemann ;-) – Nils Lindemann Mar 11 '21 at 12:11
  • Also notice, if you do this on a big number of files, over which you iterate with `os.scandir()` my example is faster because it reads the stats directly from the direntry objects yielded from the `scandir` function. – Nils Lindemann Mar 11 '21 at 12:17
  • Upps pardon me! ! Yeah that's good to know but I just needed it on a single file and for that case yours is quite a lot! ;) – ewerybody Mar 12 '21 at 11:34
  • 1
    As of 7/6//2021, ctypes.windll.kernel32.SetFileAttributesW is not a thing. You should be using win32api.SetFileAttributes instead – Jim Robinson Jul 06 '21 at 18:55