From 34b4ebb357d6be904479e9413a87e2e76bdcfedf Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 19 Dec 2012 11:45:04 -0500 Subject: Add support for Windows symlinks --- dotfiles/core.py | 204 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 4 deletions(-) diff --git a/dotfiles/core.py b/dotfiles/core.py index 4e08073..4da593b 100644 --- a/dotfiles/core.py +++ b/dotfiles/core.py @@ -8,6 +8,7 @@ This module provides the basic functionality of dotfiles. """ import os +import os.path import shutil import fnmatch @@ -16,6 +17,201 @@ __version__ = '0.5.4' __author__ = 'Jon Bernard' __license__ = 'ISC' +if hasattr(os, 'symlink'): + symlink = os.symlink + 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 + CreateSymbolicLinkW = windll.kernel32.CreateSymbolicLinkW + 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) = %s"%(name,target,is_dir, stat) + + 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): + return islink(path) and \ + realpath(path) == os.path.normpath(target) class Dotfile(object): @@ -29,12 +225,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 +240,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 +250,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 != '': -- cgit v1.2.3 From 2076f14f7eacfe4f8bb26ba60a1a927b4a5388f5 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 19 Dec 2012 18:17:00 -0500 Subject: Add missing islink assignment --- dotfiles/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dotfiles/core.py b/dotfiles/core.py index 4da593b..dfa2278 100644 --- a/dotfiles/core.py +++ b/dotfiles/core.py @@ -19,6 +19,7 @@ __license__ = 'ISC' if hasattr(os, 'symlink'): symlink = os.symlink + islink = os.path.islink realpath = os.path.realpath else: # Windows symlinks -- ctypes version -- cgit v1.2.3 From 9056cabc1aed8bc31194022f5848573273492399 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 19 Dec 2012 22:12:03 -0500 Subject: Improve error handling for Windows symlinks --- dotfiles/core.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dotfiles/core.py b/dotfiles/core.py index dfa2278..52c26cc 100644 --- a/dotfiles/core.py +++ b/dotfiles/core.py @@ -55,7 +55,9 @@ else: 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 @@ -81,7 +83,10 @@ else: else: stat = CreateSymbolicLinkA(name, target, is_dir) if win32_verbose: - print "CreateSymbolicLink(name=%s, target=%s, is_dir=%d) = %s"%(name,target,is_dir, stat) + 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) @@ -211,8 +216,10 @@ else: def is_link_to(path, target): + def normalize(path): + return os.path.normcase(os.path.normpath(path)) return islink(path) and \ - realpath(path) == os.path.normpath(target) + normalize(realpath(path)) == normalize(target) class Dotfile(object): -- cgit v1.2.3 From 51186fc507c9ec6185408f2f3ed6c11487cbf0e9 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 19 Dec 2012 22:20:28 -0500 Subject: Add Gary Oberbrunner as a contributor --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) 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 - Daniel Harding +- Gary Oberbrunner -- cgit v1.2.3 From 3a930d0490a146824241dbb314b7d68b91c3ac8c Mon Sep 17 00:00:00 2001 From: Jon Bernard Date: Fri, 4 Jan 2013 12:29:20 -0500 Subject: Update history and bump version number --- HISTORY.rst | 5 +++++ dotfiles/core.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) 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 52c26cc..93e45f1 100644 --- a/dotfiles/core.py +++ b/dotfiles/core.py @@ -13,7 +13,7 @@ import shutil import fnmatch -__version__ = '0.5.4' +__version__ = '0.5.5' __author__ = 'Jon Bernard' __license__ = 'ISC' -- cgit v1.2.3