diff --git a/mackup/constants.py b/mackup/constants.py index 68cf73c25..def0aeeba 100644 --- a/mackup/constants.py +++ b/mackup/constants.py @@ -5,6 +5,7 @@ # Support platforms PLATFORM_DARWIN = 'Darwin' PLATFORM_LINUX = 'Linux' +PLATFORM_WINDOWS = 'Windows' # Directory containing the application configs APPS_DIR = 'applications' diff --git a/mackup/mackup.py b/mackup/mackup.py index a91a1e135..d1d9807ed 100644 --- a/mackup/mackup.py +++ b/mackup/mackup.py @@ -9,10 +9,17 @@ import os.path import shutil import tempfile +import sys +import platform from . import utils from . import config from . import appsdb +from . import constants + +if platform.system() == constants.PLATFORM_WINDOWS: + from ctypes import Structure, c_ulong, c_ulonglong, c_void_p, byref + from ctypes import windll class Mackup(object): @@ -26,12 +33,80 @@ def __init__(self): self.mackup_folder = self._config.fullpath self.temp_folder = tempfile.mkdtemp(prefix="mackup_tmp_") + def is_link_privilege_enabled(self): + """Check if symbolic link creation privilege is enabled.""" + TOKEN_ALL_ACCESS = c_ulong(0x000f01ff) + SE_CREATE_SYMBOLIC_LINK_NAME = 'SeCreateSymbolicLinkPrivilege' + + class LUID_AND_ATTRIBUTES(Structure): + _fields_ = [ + ("Luid", c_ulonglong), + ("Attributes", c_ulong)] + + class PRIVILEGE_SET(Structure): + _fields_ = [ + ("PrivilegeCount", c_ulong), + ("Control", c_ulong), + ("Privilege", LUID_AND_ATTRIBUTES)] + + try: + token = c_void_p(None) + ret = windll.advapi32.OpenProcessToken( + windll.kernel32.GetCurrentProcess(), + TOKEN_ALL_ACCESS, + byref(token)) + if ret == 0: + return False + + luid = c_ulonglong(0) + ret = windll.advapi32.LookupPrivilegeValueW( + None, SE_CREATE_SYMBOLIC_LINK_NAME, byref(luid)) + if ret == 0: + return False + + enabled = c_ulong(0) + priv_set = PRIVILEGE_SET(1, 1, LUID_AND_ATTRIBUTES(luid, 2)) + ret = windll.advapi32.PrivilegeCheck( + token, byref(priv_set), byref(enabled)) + return ret != 0 and enabled.value > 0 + + except OSError: + return False + + def check_link_support_on_windows(self): + """Check if symbolic links can be created on Windows.""" + # Symbolic link support was introduced in Windows 6.0 + if sys.getwindowsversion()[0] < 6: + utils.error("Mackup can only run on Windows 6.0 (Vista)" + " or higher.") + + # Added support for Windows symbolic links in version 3.2 + if sys.version_info[0] < 3 or sys.version_info[1] < 2: + utils.error("Mackup need Python version 3.2 or highter.") + + if not self.is_link_privilege_enabled(): + utils.error("Mackup can not work without the" + " SeCreateSymbolicLinkPrivilege.\n" + "To grant this privilege, you have two choices:\n" + " 1. Log in with an administrator account and" + " launch mackup within a command\n" + " prompt running as administrator.\n" + " 2. Use a standard account, and assign the" + " SeCreateSymbolicLinkPrivilege to\n" + " your user or group via the local group policy" + " editor.") + def check_for_usable_environment(self): """Check if the current env is usable and has everything's required.""" - # Do not let the user run Mackup as root - if os.geteuid() == 0: - utils.error("Running Mackup as a superuser is useless and" - " dangerous. Don't do it!") + + if platform.system() == constants.PLATFORM_WINDOWS: + # Symbolic link support is silly on Windows, so check it first. + self.check_link_support_on_windows() + else: + # Do not let the user run Mackup as root + if os.geteuid() == 0: + utils.error("Running Mackup as a superuser is useless and" + " dangerous. Don't do it!") # Do we have a folder to put the Mackup folder ? if not os.path.isdir(self._config.path): diff --git a/mackup/utils.py b/mackup/utils.py index e20d33a72..a6ee57ed8 100644 --- a/mackup/utils.py +++ b/mackup/utils.py @@ -142,7 +142,13 @@ def link(target, link_to): chmod(target) # Create the link to target - os.symlink(target, link_to) + # target_is_directory should be specified on Windows, and is ignored on + # non-Windows platform. This feature is added in Python 3.2 + if sys.version_info[0] > 3 and sys.version_info[1] > 2: + isdir = os.path.isdir(target) + os.symlink(target, link_to, target_is_directory=isdir) + else: + os.symlink(target, link_to) def chmod(target): @@ -200,7 +206,12 @@ def get_dropbox_folder_location(): Returns: (str) Full path to the current Dropbox folder """ - host_db_path = os.path.join(os.environ['HOME'], '.dropbox/host.db') + host_db_path = os.path.expanduser('~/.dropbox/host.db') + if (not os.path.isfile(host_db_path) + and platform.system() == constants.PLATFORM_WINDOWS): + roaming = os.path.expanduser('~/AppData/Roaming/Dropbox/host.db') + local = os.path.expanduser('~/AppData/Local/Dropbox/host.db') + host_db_path = local if os.path.isfile(local) else roaming try: with open(host_db_path, 'r') as f_hostdb: data = f_hostdb.read().split() @@ -377,6 +388,13 @@ def remove_immutable_attribute(path): elif (platform.system() == constants.PLATFORM_LINUX and os.path.isfile('/usr/bin/chattr')): subprocess.call(['/usr/bin/chattr', '-R', '-i', path]) + elif platform.system() == constants.PLATFORM_WINDOWS: + # It's impossible to process a nonempty folder + # and its children all at once + subprocess.call(['attrib', '-H', '-R', path]) + if os.path.isdir(path) and os.listdir(path): + subprocess.call([ + 'attrib', '-H', '-R', os.path.join(path, '*'), '/S', '/D']) def can_file_be_synced_on_current_platform(path): @@ -385,7 +403,7 @@ def can_file_be_synced_on_current_platform(path): Check if it makes sense to sync the file at the given path on the current platform. - For now we don't sync any file in the ~/Library folder on GNU/Linux. + For now we only sync files in the ~/Library folder on OS X. There might be other exceptions in the future. Args: @@ -407,7 +425,7 @@ def can_file_be_synced_on_current_platform(path): # not any file/folder named LibrarySomething library_path = os.path.join(os.environ['HOME'], 'Library/') - if platform.system() == constants.PLATFORM_LINUX: + if platform.system() != constants.PLATFORM_DARWIN: if fullpath.startswith(library_path): can_be_synced = False