diff --git a/.gitignore b/.gitignore index 0e13ddabf..ef9536948 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ crypto.c platform_darwin.c platform_freebsd.c platform_linux.c +platform_windows.c *.egg-info *.pyc *.pyo diff --git a/borg/archive.py b/borg/archive.py index b5a105b1a..feb5f1dfd 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -25,6 +25,8 @@ from .helpers import Chunk, Error, uid2user, user2uid, gid2group, group2gid, \ CompressionDecider1, CompressionDecider2, CompressionSpec from .repository import Repository from .platform import acl_get, acl_set +if sys.platform == 'win32': + from .platform import get_owner, set_owner from .chunker import Chunker from .hashindex import ChunkIndex, ChunkIndexEntry from .cache import ChunkListEntry @@ -423,6 +425,11 @@ Number of files: {0.stats.nfiles}'''.format( os.lchown(path, uid, gid) except OSError: pass + else: + try: + set_owner(path, item[b'user'], safe_decode(item[b'uid'])) + except OSError: + pass if sys.platform != 'win32': if fd: os.fchmod(fd, item[b'mode']) @@ -501,14 +508,26 @@ Number of files: {0.stats.nfiles}'''.format( del self.manifest.archives[self.name] def stat_attrs(self, st, path): - item = { - b'mode': st.st_mode, - b'uid': st.st_uid, b'user': uid2user(st.st_uid), - b'gid': st.st_gid, b'group': gid2group(st.st_gid), - b'atime': int_to_bigint(st.st_atime_ns), - b'ctime': int_to_bigint(st.st_ctime_ns), - b'mtime': int_to_bigint(st.st_mtime_ns), - } + item = {} + if sys.platform == 'win32': + owner = get_owner(path) + item = { + b'mode': st.st_mode, + b'uid': owner[1], b'user': owner[0], + b'gid': st.st_gid, b'group': gid2group(st.st_gid), + b'atime': int_to_bigint(st.st_atime_ns), + b'ctime': int_to_bigint(st.st_ctime_ns), + b'mtime': int_to_bigint(st.st_mtime_ns), + } + else: + item = { + b'mode': st.st_mode, + b'uid': st.st_uid, b'user': uid2user(st.st_uid), + b'gid': st.st_gid, b'group': gid2group(st.st_gid), + b'atime': int_to_bigint(st.st_atime_ns), + b'ctime': int_to_bigint(st.st_ctime_ns), + b'mtime': int_to_bigint(st.st_mtime_ns), + } if self.numeric_owner: item[b'user'] = item[b'group'] = None xattrs = xattr.get_all(path, follow_symlinks=False) diff --git a/borg/archiver.py b/borg/archiver.py index 7c378567e..4ff3e3547 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -735,7 +735,10 @@ class Archiver: elif args.short: format = "{path}{NL}" else: - format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" + if sys.platform == 'win32': + format = "{user:15} {size:8} {isomtime} {path}{extra}{NL}" + else: + format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" formatter = ItemFormatter(archive, format) if not hasattr(sys.stdout, 'buffer'): @@ -2009,7 +2012,8 @@ class Archiver: def prerun_checks(self, logger): check_extension_modules() - selftest(logger) + if sys.platform != 'win32': + selftest(logger) def run(self, args): os.umask(args.umask) # early, before opening files diff --git a/borg/constants.py b/borg/constants.py index 95b16c47a..105b447de 100644 --- a/borg/constants.py +++ b/borg/constants.py @@ -1,7 +1,7 @@ # this set must be kept complete, otherwise the RobustUnpacker might malfunction: ITEM_KEYS = set([b'path', b'source', b'rdev', b'chunks', b'hardlink_master', b'mode', b'user', b'group', b'uid', b'gid', b'mtime', b'atime', b'ctime', - b'xattrs', b'bsdflags', b'acl_nfs4', b'acl_access', b'acl_default', b'acl_extended', ]) + b'xattrs', b'bsdflags', b'acl_nfs4', b'acl_access', b'acl_default', b'acl_extended', b'win_dacl']) ARCHIVE_TEXT_KEYS = (b'name', b'comment', b'hostname', b'username', b'time', b'time_end') ITEM_TEXT_KEYS = (b'path', b'source', b'user', b'group') diff --git a/borg/platform.py b/borg/platform.py index 1bc8ee5e4..cb35121c0 100644 --- a/borg/platform.py +++ b/borg/platform.py @@ -6,6 +6,8 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only from .platform_freebsd import acl_get, acl_set, API_VERSION elif sys.platform == 'darwin': # pragma: darwin only from .platform_darwin import acl_get, acl_set, API_VERSION +elif sys.platform == 'win32': # pragma: windows only + from .platform_windows import acl_get, acl_set, API_VERSION, get_owner, set_owner else: # pragma: unknown platform only API_VERSION = 2 diff --git a/borg/platform_windows.pyx b/borg/platform_windows.pyx new file mode 100644 index 000000000..0d367667c --- /dev/null +++ b/borg/platform_windows.pyx @@ -0,0 +1,329 @@ +#cython: language_level=3 + +import json +from libc.stddef cimport wchar_t +from libc.stdint cimport uint16_t, uint32_t, uint64_t +cimport cpython.array +import array + +import platform +from .helpers import safe_decode, safe_encode + +API_VERSION = 2 + + +cdef extern from 'stdlib.h': + void free(void* ptr) + void* malloc(size_t) + void* calloc(size_t, size_t) + + +cdef extern from 'Python.h': + wchar_t* PyUnicode_AsWideCharString(object, Py_ssize_t *) + object PyUnicode_FromWideChar(const wchar_t*, Py_ssize_t) + void* PyMem_Malloc(int) + void PyMem_Free(void*) + + +cdef extern from 'windows.h': + ctypedef int HLOCAL + ctypedef wchar_t* LPCTSTR + ctypedef char BYTE + ctypedef int HLOCAL + ctypedef uint32_t DWORD + ctypedef DWORD* LPDWORD + ctypedef int BOOL + ctypedef BYTE* PSID + struct _ACL: + uint16_t AceCount + + HLOCAL LocalFree(HLOCAL) + DWORD GetLastError(); + void SetLastError(DWORD) + + DWORD FormatMessageW(DWORD, void*, DWORD, DWORD, wchar_t**, DWORD, void*) + + + BOOL InitializeSecurityDescriptor(BYTE*, DWORD) + + BOOL LookupAccountNameW(LPCTSTR, LPCTSTR, PSID, LPDWORD, LPCTSTR, LPDWORD, LPDWORD) + BOOL GetSecurityDescriptorDacl(PSID, BOOL*, _ACL**, BOOL*) + + cdef extern int ERROR_INSUFFICIENT_BUFFER + cdef extern int ERROR_INVALID_SID + cdef extern int ERROR_NONE_MAPPED + + cdef extern int OWNER_SECURITY_INFORMATION + cdef extern int GROUP_SECURITY_INFORMATION + cdef extern int DACL_SECURITY_INFORMATION + cdef extern int SACL_SECURITY_INFORMATION + cdef extern int LABEL_SECURITY_INFORMATION + cdef extern int ATTRIBUTE_SECURITY_INFORMATION + cdef extern int SCOPE_SECURITY_INFORMATION + cdef extern int BACKUP_SECURITY_INFORMATION + cdef extern int UNPROTECTED_SACL_SECURITY_INFORMATION + cdef extern int UNPROTECTED_DACL_SECURITY_INFORMATION + cdef extern int PROTECTED_SACL_SECURITY_INFORMATION + cdef extern int PROTECTED_DACL_SECURITY_INFORMATION + + cdef extern int SECURITY_DESCRIPTOR_MIN_LENGTH + + cdef extern int FORMAT_MESSAGE_ALLOCATE_BUFFER + cdef extern int FORMAT_MESSAGE_FROM_SYSTEM + cdef extern int FORMAT_MESSAGE_IGNORE_INSERTS + + +cdef extern from 'accctrl.h': + ctypedef enum _SE_OBJECT_TYPE: + SE_FILE_OBJECT + ctypedef _SE_OBJECT_TYPE SE_OBJECT_TYPE + struct _TRUSTEE_W: + uint16_t TrusteeForm + uint16_t TrusteeType + LPCTSTR ptstrName + + struct _EXPLICIT_ACCESS_W: + DWORD grfAccessPermissions + uint16_t grfAccessMode + DWORD grfInheritance + _TRUSTEE_W Trustee + + cdef extern uint16_t TRUSTEE_IS_SID + cdef extern uint16_t TRUSTEE_IS_NAME + cdef extern uint16_t TRUSTEE_BAD_FORM + + DWORD GetExplicitEntriesFromAclW(_ACL*, uint32_t*, _EXPLICIT_ACCESS_W**) + + +cdef extern from 'Sddl.h': + ctypedef int* LPBOOL + + BOOL GetFileSecurityW(LPCTSTR, int, PSID, DWORD, LPDWORD) + BOOL GetSecurityDescriptorOwner(PSID, PSID*, LPBOOL) + BOOL LookupAccountSidW(LPCTSTR, PSID, LPCTSTR, LPDWORD, LPCTSTR, LPDWORD, uint16_t*) + BOOL ConvertSidToStringSidW(PSID, LPCTSTR*) + BOOL ConvertStringSidToSidW(LPCTSTR, PSID*) + BOOL ConvertSecurityDescriptorToStringSecurityDescriptorW(BYTE*, DWORD, int, LPCTSTR*, int*) + + cdef extern int SDDL_REVISION_1 + + +cdef extern from 'Aclapi.h': + ctypedef void* PACL + DWORD GetNamedSecurityInfoW(LPCTSTR, SE_OBJECT_TYPE, DWORD, PSID*, PSID*, PACL*, PACL*, _ACL**) + DWORD SetNamedSecurityInfoW(LPCTSTR, int, int, PSID, PSID, PACL, PACL) + DWORD SetEntriesInAclW(unsigned int, _EXPLICIT_ACCESS_W*, PACL, _ACL**) + + +def raise_error(api, path=''): + cdef wchar_t *error_message + error = GetLastError() + if not error: + return + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, error, 0, &error_message, 0, NULL) + error_string = PyUnicode_FromWideChar(error_message, -1) + LocalFree(error_message) + error_string = api + ': ' + error_string + if path: + raise OSError(error, error_string, path) + else: + raise OSError(error, error_string) + + +cdef PSID _get_file_security(filename, int request): + cdef DWORD length = 0 + # N.B. This query may fail with ERROR_INVALID_FUNCTION + # for some filesystems. + cdef wchar_t* wcharfilename = PyUnicode_AsWideCharString(filename, NULL) + GetFileSecurityW(wcharfilename, request, NULL, 0, &length) + if GetLastError() == ERROR_INSUFFICIENT_BUFFER: + SetLastError(0) + else: + raise_error('GetFileSecurityW', filename) + return NULL + cdef BYTE* sd = malloc((length) * sizeof(BYTE)) + GetFileSecurityW(wcharfilename, request, sd, length, &length) + PyMem_Free(wcharfilename) + return sd + + +cdef PSID _get_security_descriptor_owner(PSID sd): + cdef PSID sid + cdef BOOL sid_defaulted + GetSecurityDescriptorOwner(sd, &sid, &sid_defaulted) + return (sid) + + +cdef _look_up_account_sid(PSID sid): + cdef int SIZE = 256 + cdef wchar_t* name = malloc((SIZE) * sizeof(wchar_t)) + cdef wchar_t* domain = malloc((SIZE) * sizeof(wchar_t)) + cdef DWORD cch_name = SIZE + cdef DWORD cch_domain = SIZE + cdef uint16_t sid_type = 0 + + cdef BOOL ret = LookupAccountSidW(NULL, sid, name, &cch_name, domain, &cch_domain, &sid_type) + if ret == 0: + lasterror = GetLastError() + if lasterror == ERROR_NONE_MAPPED: + # Unknown (removed?) user or file from another windows installation + free(name) + free(domain) + return 'unknown', 'unknown', 0 + else: + raise_error('LookupAccountSidW') + + pystrName = PyUnicode_FromWideChar(name, -1) + pystrDomain = PyUnicode_FromWideChar(domain, -1) + + free(name) + free(domain) + return pystrName, pystrDomain, sid_type + + +cdef sid2string(PSID sid): + cdef wchar_t* sidstr + ConvertSidToStringSidW(sid, &sidstr) + ret = PyUnicode_FromWideChar(sidstr, -1) + LocalFree(sidstr) + return ret + + +def get_owner(path): + cdef int request = OWNER_SECURITY_INFORMATION + cdef BYTE* sd = _get_file_security(path, request) + if sd == NULL: + return 'unknown', 'S-1-0-0' + cdef PSID sid = _get_security_descriptor_owner(sd) + if sid == NULL: + return 'unknown', 'S-1-0-0' + name, domain, sid_type = _look_up_account_sid(sid) + free(sd) + if domain and domain.lower() != platform.node().lower() and domain != 'BUILTIN': + return '{0}\\{1}'.format(domain, name), sid2string(sid) + else: + return name, sid2string(sid) + + +def set_owner(path, owner, sidstring = None): + cdef PSID newsid + cdef wchar_t* temp + cdef DWORD sid_type = 0 + cdef DWORD length = 0 + cdef DWORD domainlength = 0 + if sidstring is not None: + temp = PyUnicode_AsWideCharString(sidstring, NULL) + ConvertStringSidToSidW(temp, &newsid) + if sidstring is None or GetLastError() == ERROR_INVALID_SID: + temp = PyUnicode_AsWideCharString(owner, NULL) + + LookupAccountNameW(NULL, temp, NULL, &length, NULL, &domainlength, &sid_type) + + newsid = malloc((length) * sizeof(BYTE)) + SetLastError(0) + domainlength = 0 + LookupAccountNameW(NULL, temp, newsid, &length, NULL, &domainlength, &sid_type) + if GetLastError() != 0: + raise_error('LookupAccountNameW', owner) + PyMem_Free(temp) + return + + PyMem_Free(temp) + + cdef wchar_t* cstrPath = PyUnicode_AsWideCharString(path, NULL) + SetNamedSecurityInfoW(cstrPath, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, newsid, NULL, NULL, NULL) + PyMem_Free(cstrPath) + if length == 0: + LocalFree(newsid) + else: + free(newsid) + + +def acl_get(path, item, st, numeric_owner=False): + cdef int request = DACL_SECURITY_INFORMATION + + cdef BYTE* SD = _get_file_security(path, request) + if SD == NULL: + return + + cdef BOOL daclFound + cdef _ACL* DACL + cdef BOOL DACLDefaulted + GetSecurityDescriptorDacl(SD, &daclFound, &DACL, &DACLDefaulted) + + cdef uint32_t length + cdef _EXPLICIT_ACCESS_W* ACEs + + GetExplicitEntriesFromAclW(DACL, &length, &ACEs) + + pyDACL = [] + cdef PSID newsid + cdef uint32_t domainlength + cdef uint32_t sid_type + for i in range(length): + permissions = None + name = "" + sidstr = "" + if ACEs[i].Trustee.TrusteeForm == TRUSTEE_IS_SID: + name, domain, type = _look_up_account_sid((ACEs[i].Trustee.ptstrName)) + sidstr = sid2string((ACEs[i].Trustee.ptstrName)) + + elif ACEs[i].Trustee.TrusteeForm == TRUSTEE_IS_NAME: + sid_type = 0 + domainlength = 0 + LookupAccountNameW(NULL, ACEs[i].Trustee.ptstrName, NULL, &(length), NULL, &domainlength, &sid_type) + + newsid = malloc((length) * sizeof(BYTE)) + domainlength = 0 + LookupAccountNameW(NULL, ACEs[i].Trustee.ptstrName, newsid, &length, NULL, &domainlength, &sid_type) + trusteeName, domain, type = _look_up_account_sid(newsid) + + name = trusteeName + sidstr = sid2string(newsid) + free(newsid) + + elif ACEs[i].Trustee.TrusteeForm == TRUSTEE_BAD_FORM: + continue + permissions = {'user': {'name': name, 'sid': sidstr}, 'permissions': (ACEs[i].grfAccessPermissions, ACEs[i].grfAccessMode, ACEs[i].grfInheritance)} + pyDACL.append(permissions) + item[b'win_dacl'] = safe_encode(json.dumps(pyDACL)) + + free(SD) + LocalFree(ACEs) + + +def acl_set(path, item, numeric_owner=False): + if b'win_dacl' not in item: + return + + pyDACL = json.loads(safe_decode(item[b'win_dacl'])) + cdef _EXPLICIT_ACCESS_W* ACEs = <_EXPLICIT_ACCESS_W*>calloc(sizeof(_EXPLICIT_ACCESS_W), len(pyDACL)) + cdef wchar_t* temp + cdef PSID newsid + for i in range(len(pyDACL)): + if pyDACL[i]['user']['name'] == '' or numeric_owner: + ACEs[i].Trustee.TrusteeForm = TRUSTEE_IS_SID + temp = PyUnicode_AsWideCharString(pyDACL[i]['user']['sid'], NULL) + ConvertStringSidToSidW(temp, &newsid) + ACEs[i].Trustee.ptstrName = newsid + PyMem_Free(temp) + else: + ACEs[i].Trustee.TrusteeForm = TRUSTEE_IS_NAME + ACEs[i].Trustee.ptstrName = PyUnicode_AsWideCharString(pyDACL[i]['user']['name'], NULL) + ACEs[i].grfAccessPermissions = pyDACL[i]['permissions'][0] + ACEs[i].grfAccessMode = pyDACL[i]['permissions'][1] + ACEs[i].grfInheritance = pyDACL[i]['permissions'][2] + cdef _ACL* newDACL + SetEntriesInAclW(len(pyDACL), ACEs, NULL, &newDACL) + cdef wchar_t* cstrPath = PyUnicode_AsWideCharString(path, NULL) + SetNamedSecurityInfoW(cstrPath, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, newDACL, NULL) + + for i in range(len(pyDACL)): + if pyDACL[i]['user']['name'] == '' or numeric_owner: + LocalFree(ACEs[i].Trustee.ptstrName) + else: + PyMem_Free(ACEs[i].Trustee.ptstrName) + free(ACEs) + PyMem_Free(cstrPath) + LocalFree(newDACL) diff --git a/setup.py b/setup.py index 5ad58c0a5..a06d7c6d3 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ hashindex_source = 'borg/hashindex.pyx' platform_linux_source = 'borg/platform_linux.pyx' platform_darwin_source = 'borg/platform_darwin.pyx' platform_freebsd_source = 'borg/platform_freebsd.pyx' +platform_windows_source = 'borg/platform_windows.pyx' try: from Cython.Distutils import build_ext @@ -52,7 +53,8 @@ try: class Sdist(sdist): def __init__(self, *args, **kwargs): for src in glob('borg/*.pyx'): - cython_compiler.compile(src, cython_compiler.default_options) + options = cython_compiler.default_options + cython_compiler.compile(src, options) super().__init__(*args, **kwargs) def make_distribution(self): @@ -64,6 +66,7 @@ try: 'borg/platform_linux.c', 'borg/platform_freebsd.c', 'borg/platform_darwin.c', + 'borg/platform_windows.c', ]) super().make_distribution() @@ -79,10 +82,11 @@ except ImportError: platform_linux_source = platform_linux_source.replace('.pyx', '.c') platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c') platform_darwin_source = platform_darwin_source.replace('.pyx', '.c') + platform_windows_source = platform_windows_source.replace('.pyx', '.c') from distutils.command.build_ext import build_ext if not on_rtd and not all(os.path.exists(path) for path in [ compress_source, crypto_source, chunker_source, hashindex_source, - platform_linux_source, platform_freebsd_source]): + platform_linux_source, platform_freebsd_source, platform_windows_source]): raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.') @@ -312,6 +316,8 @@ if not on_rtd: ext_modules.append(Extension('borg.platform_freebsd', [platform_freebsd_source])) elif sys.platform == 'darwin': ext_modules.append(Extension('borg.platform_darwin', [platform_darwin_source])) + elif sys.platform == 'win32': + ext_modules.append(Extension('borg.platform_windows', [platform_windows_source])) def parse(root, describe_command=None):