diff options
-rw-r--r-- | AUTHORS.rst | 1 | ||||
-rw-r--r-- | HISTORY.rst | 5 | ||||
-rw-r--r-- | dotfiles/core.py | 214 |
3 files changed, 215 insertions, 5 deletions
diff --git a/AUTHORS.rst b/AUTHORS.rst index aa0ecaa..7e14a61 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -14,3 +14,4 @@ Patches and Suggestions - Sebastian Rahlf - Reinout van Rees <reinout@vanrees.org> - Daniel Harding +- Gary Oberbrunner diff --git a/HISTORY.rst b/HISTORY.rst index b17a94b..d53e88d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,11 @@ History ------- +0.5.5 ++++++ + +* Add support for Windows symlinks with Python 2 + 0.5.4 +++++ diff --git a/dotfiles/core.py b/dotfiles/core.py index 4e08073..93e45f1 100644 --- a/dotfiles/core.py +++ b/dotfiles/core.py @@ -8,14 +8,218 @@ This module provides the basic functionality of dotfiles. """ import os +import os.path import shutil import fnmatch -__version__ = '0.5.4' +__version__ = '0.5.5' __author__ = 'Jon Bernard' __license__ = 'ISC' +if hasattr(os, 'symlink'): + symlink = os.symlink + islink = os.path.islink + realpath = os.path.realpath +else: + # Windows symlinks -- ctypes version + # symlink, islink, readlink, realpath, is_link_to + + win32_verbose = False # set to True to debug symlink stuff + import os, ctypes, struct + from ctypes import windll, wintypes + + FSCTL_GET_REPARSE_POINT = 0x900a8 + + FILE_ATTRIBUTE_READONLY = 0x0001 + FILE_ATTRIBUTE_HIDDEN = 0x0002 + FILE_ATTRIBUTE_DIRECTORY = 0x0010 + FILE_ATTRIBUTE_NORMAL = 0x0080 + FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 + + + GENERIC_READ = 0x80000000 + GENERIC_WRITE = 0x40000000 + OPEN_EXISTING = 3 + FILE_READ_ATTRIBUTES = 0x80 + FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 + INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value + + INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF + + FILE_FLAG_OPEN_REPARSE_POINT = 2097152 + FILE_FLAG_BACKUP_SEMANTICS = 33554432 + # FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTI + FILE_FLAG_REPARSE_BACKUP = 35651584 + + + kdll = windll.LoadLibrary("kernel32.dll") + CreateSymbolicLinkA = windll.kernel32.CreateSymbolicLinkA + CreateSymbolicLinkA.restype = wintypes.BOOLEAN + CreateSymbolicLinkW = windll.kernel32.CreateSymbolicLinkW + CreateSymbolicLinkW.restype = wintypes.BOOLEAN + GetFileAttributesA = windll.kernel32.GetFileAttributesA + GetFileAttributesW = windll.kernel32.GetFileAttributesW + CloseHandle = windll.kernel32.CloseHandle + _CreateFileW = windll.kernel32.CreateFileW + _CreateFileA = windll.kernel32.CreateFileA + _DevIoCtl = windll.kernel32.DeviceIoControl + _DevIoCtl.argtypes = [ + wintypes.HANDLE, #HANDLE hDevice + wintypes.DWORD, #DWORD dwIoControlCode + wintypes.LPVOID, #LPVOID lpInBuffer + wintypes.DWORD, #DWORD nInBufferSize + wintypes.LPVOID, #LPVOID lpOutBuffer + wintypes.DWORD, #DWORD nOutBufferSize + ctypes.POINTER(wintypes.DWORD), #LPDWORD lpBytesReturned + wintypes.LPVOID] #LPOVERLAPPED lpOverlapped + _DevIoCtl.restype = wintypes.BOOL + + + def CreateSymbolicLink(name, target, is_dir): + assert type(name) == type(target) + if type(name) == unicode: + stat = CreateSymbolicLinkW(name, target, is_dir) + else: + stat = CreateSymbolicLinkA(name, target, is_dir) + if win32_verbose: + print "CreateSymbolicLink(name=%s, target=%s, is_dir=%d) = %#x"%(name,target,is_dir, stat) + if not stat: + print "Can't create symlink %s -> %s"%(name, target) + raise ctypes.WinError() + + def symlink(target, name): + CreateSymbolicLink(name, target, 0) + + def GetFileAttributes(path): + if type(path) == unicode: + return GetFileAttributesW(path) + else: + return GetFileAttributesA(path) + + def islink(path): + assert path + has_link_attr = GetFileAttributes(path) & FILE_ATTRIBUTE_REPARSE_POINT + if win32_verbose: + print "islink(%s): attrs=%#x: %s"%(path, GetFileAttributes(path), has_link_attr != 0) + return has_link_attr != 0 + + def DeviceIoControl(hDevice, ioControlCode, input, output): + # DeviceIoControl Function + # http://msdn.microsoft.com/en-us/library/aa363216(v=vs.85).aspx + if input: + input_size = len(input) + else: + input_size = 0 + if isinstance(output, int): + output = ctypes.create_string_buffer(output) + output_size = len(output) + assert isinstance(output, ctypes.Array) + bytesReturned = wintypes.DWORD() + status = _DevIoCtl(hDevice, ioControlCode, input, + input_size, output, output_size, bytesReturned, None) + if win32_verbose: + print "DeviceIOControl: status = %d" % status + if status != 0: + return output[:bytesReturned.value] + else: + return None + + + def CreateFile(path, access, sharemode, creation, flags): + if type(path) == unicode: + return _CreateFileW(path, access, sharemode, None, creation, flags, None) + else: + return _CreateFileA(path, access, sharemode, None, creation, flags, None) + + SymbolicLinkReparseFormat = "LHHHHHHL" + SymbolicLinkReparseSize = struct.calcsize(SymbolicLinkReparseFormat); + + def readlink(path): + """ Windows readlink implementation. """ + # This wouldn't return true if the file didn't exist, as far as I know. + if not islink(path): + if win32_verbose: + print "readlink(%s): not a link."%path + return None + + # Open the file correctly depending on the string type. + hfile = CreateFile(path, GENERIC_READ, 0, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT) + + # MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384 = (16*1024) + buffer = DeviceIoControl(hfile, FSCTL_GET_REPARSE_POINT, None, 16384) + CloseHandle(hfile) + + # Minimum possible length (assuming length of the target is bigger than 0) + if not buffer or len(buffer) < 9: + if win32_verbose: + print "readlink(%s): no reparse buffer."%path + return None + + # Parse and return our result. + # typedef struct _REPARSE_DATA_BUFFER { + # ULONG ReparseTag; + # USHORT ReparseDataLength; + # USHORT Reserved; + # union { + # struct { + # USHORT SubstituteNameOffset; + # USHORT SubstituteNameLength; + # USHORT PrintNameOffset; + # USHORT PrintNameLength; + # ULONG Flags; + # WCHAR PathBuffer[1]; + # } SymbolicLinkReparseBuffer; + # struct { + # USHORT SubstituteNameOffset; + # USHORT SubstituteNameLength; + # USHORT PrintNameOffset; + # USHORT PrintNameLength; + # WCHAR PathBuffer[1]; + # } MountPointReparseBuffer; + # struct { + # UCHAR DataBuffer[1]; + # } GenericReparseBuffer; + # } DUMMYUNIONNAME; + # } REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER; + + # Only handle SymbolicLinkReparseBuffer + (tag, dataLength, reserver, SubstituteNameOffset, SubstituteNameLength, + PrintNameOffset, PrintNameLength, + Flags) = struct.unpack(SymbolicLinkReparseFormat, + buffer[:SymbolicLinkReparseSize]) + # print tag, dataLength, reserver, SubstituteNameOffset, SubstituteNameLength + start = SubstituteNameOffset + SymbolicLinkReparseSize + actualPath = buffer[start : start + SubstituteNameLength].decode("utf-16") + # This utf-16 string is null terminated + index = actualPath.find(u"\0") + if index > 0: + actualPath = actualPath[:index] + if actualPath.startswith(u"\\??\\"): # ASCII 92, 63, 63, 92 + ret = actualPath[4:] # strip off leading junk + else: + ret = actualPath + if win32_verbose: + print "readlink(%s->%s->%s): index(null) = %d"%\ + (path,repr(actualPath),repr(ret),index) + return ret + + def realpath(fpath): + while islink(fpath): + rpath = readlink(fpath) + if rpath is None: + return fpath + if not os.path.isabs(rpath): + rpath = os.path.abspath(os.path.join(os.path.dirname(fpath), rpath)) + fpath = rpath + return fpath + + +def is_link_to(path, target): + def normalize(path): + return os.path.normcase(os.path.normpath(path)) + return islink(path) and \ + normalize(realpath(path)) == normalize(target) class Dotfile(object): @@ -29,12 +233,12 @@ class Dotfile(object): self.status = '' if not os.path.lexists(self.name): self.status = 'missing' - elif os.path.realpath(self.name) != self.target: + elif not is_link_to(self.name, self.target): self.status = 'unsynced' def sync(self, force): if self.status == 'missing': - os.symlink(self.target, self.name) + symlink(self.target, self.name) elif self.status == 'unsynced': if not force: print("Skipping \"%s\", use --force to override" @@ -44,7 +248,7 @@ class Dotfile(object): shutil.rmtree(self.name) else: os.remove(self.name) - os.symlink(self.target, self.name) + symlink(self.target, self.name) def add(self): if self.status == 'missing': @@ -54,7 +258,7 @@ class Dotfile(object): print("Skipping \"%s\", already managed" % self.basename) return shutil.move(self.name, self.target) - os.symlink(self.target, self.name) + symlink(self.target, self.name) def remove(self): if self.status != '': |