Commit 6fe26bd3 authored by Kevin Christopher Henry's avatar Kevin Christopher Henry
Browse files

Fixed #19373 -- Ported Windows file locking from PyWin32 to ctypes

There wasn't any file locking under Windows unless PyWin32 was
installed. This removes that (undocumented) dependency by using ctypes
instead.

Thanks to Anatoly Techtonik for writing the ctypes port upon which this
is based.
parent 07ae47f7
Loading
Loading
Loading
Loading

django/core/files/locks.py

100644 → 100755
+89 −46
Original line number Diff line number Diff line
"""
Portable file locking utilities.

Based partially on example by Jonathan Feignberg <jdf@pobox.com> in the Python
Cookbook, licensed under the Python Software License.
Based partially on an example by Jonathan Feignberg in the Python
Cookbook [1] (licensed under the Python Software License) and a ctypes port by
Anatoly Techtonik for Roundup [2] (license [3]).

    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65203
[1] http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65203
[2] http://sourceforge.net/p/roundup/code/ci/default/tree/roundup/backends/portalocker.py
[3] http://sourceforge.net/p/roundup/code/ci/default/tree/COPYING.txt

Example Usage::

@@ -13,58 +16,98 @@ Example Usage::
    ...     locks.lock(f, locks.LOCK_EX)
    ...     f.write('Django')
"""
import os

__all__ = ('LOCK_EX', 'LOCK_SH', 'LOCK_NB', 'lock', 'unlock')

system_type = None

try:
    import win32con
    import win32file
    import pywintypes
    LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
    LOCK_SH = 0
    LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
    __overlapped = pywintypes.OVERLAPPED()
    system_type = 'nt'
except (ImportError, AttributeError):
    pass

try:
    import fcntl
    LOCK_EX = fcntl.LOCK_EX
    LOCK_SH = fcntl.LOCK_SH
    LOCK_NB = fcntl.LOCK_NB
    system_type = 'posix'
except (ImportError, AttributeError):
    pass


def fd(f):
def _fd(f):
    """Get a filedescriptor from something which could be a file or an fd."""
    return f.fileno() if hasattr(f, 'fileno') else f

if system_type == 'nt':
    def lock(file, flags):
        hfile = win32file._get_osfhandle(fd(file))
        win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped)

    def unlock(file):
        hfile = win32file._get_osfhandle(fd(file))
        win32file.UnlockFileEx(hfile, 0, -0x10000, __overlapped)
elif system_type == 'posix':
    def lock(file, flags):
        fcntl.lockf(fd(file), flags)
if os.name == 'nt':
    import msvcrt
    from ctypes import (sizeof, c_ulong, c_void_p, c_int64,
                        Structure, Union, POINTER, windll, byref)
    from ctypes.wintypes import BOOL, DWORD, HANDLE

    def unlock(file):
        fcntl.lockf(fd(file), fcntl.LOCK_UN)
    LOCK_SH = 0  # the default
    LOCK_NB = 0x1  # LOCKFILE_FAIL_IMMEDIATELY
    LOCK_EX = 0x2  # LOCKFILE_EXCLUSIVE_LOCK

    # --- Adapted from the pyserial project ---
    # detect size of ULONG_PTR
    if sizeof(c_ulong) != sizeof(c_void_p):
        ULONG_PTR = c_int64
    else:
    # File locking is not supported.
    LOCK_EX = LOCK_SH = LOCK_NB = None
        ULONG_PTR = c_ulong
    PVOID = c_void_p

    # --- Union inside Structure by stackoverflow:3480240 ---
    class _OFFSET(Structure):
        _fields_ = [
            ('Offset', DWORD),
            ('OffsetHigh', DWORD)]

    class _OFFSET_UNION(Union):
        _anonymous_ = ['_offset']
        _fields_ = [
            ('_offset', _OFFSET),
            ('Pointer', PVOID)]

    class OVERLAPPED(Structure):
        _anonymous_ = ['_offset_union']
        _fields_ = [
            ('Internal', ULONG_PTR),
            ('InternalHigh', ULONG_PTR),
            ('_offset_union', _OFFSET_UNION),
            ('hEvent', HANDLE)]

    LPOVERLAPPED = POINTER(OVERLAPPED)

    # --- Define function prototypes for extra safety ---
    LockFileEx = windll.kernel32.LockFileEx
    LockFileEx.restype = BOOL
    LockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, DWORD, LPOVERLAPPED]
    UnlockFileEx = windll.kernel32.UnlockFileEx
    UnlockFileEx.restype = BOOL
    UnlockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, LPOVERLAPPED]

    def lock(f, flags):
        hfile = msvcrt.get_osfhandle(_fd(f))
        overlapped = OVERLAPPED()
        ret = LockFileEx(hfile, flags, 0, 0, 0xFFFF0000, byref(overlapped))
        return bool(ret)

    def unlock(f):
        hfile = msvcrt.get_osfhandle(_fd(f))
        overlapped = OVERLAPPED()
        ret = UnlockFileEx(hfile, 0, 0, 0xFFFF0000, byref(overlapped))
        return bool(ret)

elif os.name == 'posix':
    import fcntl
    LOCK_SH = fcntl.LOCK_SH  # shared lock
    LOCK_NB = fcntl.LOCK_NB  # non-blocking
    LOCK_EX = fcntl.LOCK_EX

    def lock(f, flags):
        ret = fcntl.flock(_fd(f), flags)
        return (ret == 0)

    def unlock(f):
        ret = fcntl.flock(_fd(f), fcntl.LOCK_UN)
        return (ret == 0)

else:  # File locking is not supported.
    LOCK_EX = LOCK_SH = LOCK_NB = 0

    # Dummy functions that don't do anything.
    def lock(file, flags):
        pass
    def lock(f, flags):
        # File is not locked
        return False

    def unlock(file):
        pass
    def unlock(f):
        # File is unlocked
        return True
+8 −0
Original line number Diff line number Diff line
@@ -415,6 +415,14 @@ Email
* The SMTP :class:`~django.core.mail.backends.smtp.EmailBackend` now accepts a
  :attr:`~django.core.mail.backends.smtp.EmailBackend.timeout` parameter.

File Storage
^^^^^^^^^^^^

* File locking on Windows previously depended on the PyWin32 package; if it
  wasn't installed, file locking failed silently. That dependency has been
  removed, and file locking is now implemented natively on both Windows
  and Unix.

File Uploads
^^^^^^^^^^^^