Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MoveFileEx or other atomic rename #57

Merged
merged 12 commits into from
Feb 7, 2016
9 changes: 9 additions & 0 deletions pywincffi/core/cdefs/headers/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@
#define PIPE_TYPE_BYTE ...
#define PIPE_TYPE_MESSAGE ...

// Flags for MoveFileEx
#define MOVEFILE_COPY_ALLOWED ...
#define MOVEFILE_CREATE_HARDLINK ...
#define MOVEFILE_DELAY_UNTIL_REBOOT ...
#define MOVEFILE_FAIL_IF_NOT_TRACKABLE ...
#define MOVEFILE_REPLACE_EXISTING ...
#define MOVEFILE_WRITE_THROUGH ...

// General security
#define SECURITY_ANONYMOUS ...
#define SECURITY_CONTEXT_TRACKING ...
Expand All @@ -131,6 +139,7 @@
#define ERROR_INVALID_PARAMETER ...
#define ERROR_ACCESS_DENIED ...
#define ERROR_ALREADY_EXISTS ...
#define ERROR_NOT_SAME_DEVICE ...

// For the moment, we can't define this here. When cffi
// parses the header this returns -1 and cffi seems to
Expand Down
1 change: 1 addition & 0 deletions pywincffi/core/cdefs/headers/functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ BOOL SetNamedPipeHandleState(HANDLE, LPDWORD, LPDWORD, LPDWORD);
// Files
BOOL WriteFile(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED);
BOOL ReadFile(HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED);
BOOL MoveFileEx(LPCTSTR, LPCTSTR, DWORD);

// Handles
HANDLE handle_from_fd(int);
Expand Down
2 changes: 1 addition & 1 deletion pywincffi/kernel32/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Our kernel32 package is broken into several submodules. The functions
# we're wrapping are imported here so it's easier to access and because
# it's close to the way Windows would present them (as a single module)
from pywincffi.kernel32.file import ReadFile, WriteFile
from pywincffi.kernel32.file import ReadFile, WriteFile, MoveFileEx
from pywincffi.kernel32.handle import (
CloseHandle, GetStdHandle, WaitForSingleObject, handle_from_file)
from pywincffi.kernel32.pipe import (
Expand Down
47 changes: 46 additions & 1 deletion pywincffi/kernel32/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
A module containing common Windows file functions for working with files.
"""

from six import integer_types
from six import integer_types, string_types

from pywincffi.core import dist
from pywincffi.core.checks import Enums, input_check, error_check
from pywincffi.util import string_to_cdata


def WriteFile(hFile, lpBuffer, lpOverlapped=None):
Expand Down Expand Up @@ -119,3 +120,47 @@ def ReadFile(hFile, nNumberOfBytesToRead, lpOverlapped=None):
)
error_check("ReadFile", code=code, expected=Enums.NON_ZERO)
return ffi.string(lpBuffer)


def MoveFileEx(lpExistingFileName, lpNewFileName, dwFlags=None):
"""
Moves an existing file or directory, including its children,
see the MSDN documentation for full options.

.. seealso::

https://msdn.microsoft.com/en-us/library/aa365240

:param str lpExistingFileName:
Name of the file or directory to perform the operation on.

:param str lpNewFileName:
Optional new name of the path or directory. This value may be
``None``.

:keyword int dwFlags:
Parameters which control the operation of :func:`MoveFileEx`. See
the MSDN documentation for full details. By default
``MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH`` is used.
"""
ffi, library = dist.load()

if dwFlags is None:
dwFlags = \
library.MOVEFILE_REPLACE_EXISTING | library.MOVEFILE_WRITE_THROUGH

input_check("lpExistingFileName", lpExistingFileName, string_types)
input_check("dwFlags", dwFlags, integer_types)

if lpNewFileName is not None:
input_check("lpNewFileName", lpNewFileName, string_types)
lpNewFileName = string_to_cdata(lpNewFileName)
else:
lpNewFileName = ffi.NULL

code = library.MoveFileEx(
string_to_cdata(lpExistingFileName),
lpNewFileName,
ffi.cast("DWORD", dwFlags)
)
error_check("MoveFileEx", code=code, expected=Enums.NON_ZERO)
68 changes: 68 additions & 0 deletions tests/test_kernel32/test_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import os
import ctypes
import tempfile
from os.path import isfile

from pywincffi.core import dist
from pywincffi.dev.testutil import TestCase
from pywincffi.exceptions import WindowsAPIError
from pywincffi.kernel32 import MoveFileEx


class TestMoveFileEx(TestCase):
"""
Tests for :func:`pywincffi.kernel32.MoveFileEx`
"""
def test_replaces_file(self):
# Destination file exists, this should replace it.
file_contents = self.random_string(12)
fd, path1 = tempfile.mkstemp()

with os.fdopen(fd, "w") as file_:
file_.write(file_contents)

fd, path2 = tempfile.mkstemp()
self.addCleanup(os.remove, path2)
os.close(fd)

MoveFileEx(path1, path2)

with open(path2, "r") as file_:
self.assertEqual(file_.read(), file_contents)

self.assertFalse(isfile(path1))

def test_renames_file(self):
# Destination file does not exist, this should create it.
file_contents = self.random_string(12)
fd, path1 = tempfile.mkstemp()
path2 = path1 + ".new"

with os.fdopen(fd, "w") as file_:
file_.write(file_contents)

MoveFileEx(path1, path2)

with open(path2, "r") as file_:
self.assertEqual(file_.read(), file_contents)

self.assertFalse(isfile(path1))

def test_run_delete_after_reboot(self):
fd, path = tempfile.mkstemp()
os.close(fd)

_, library = dist.load()
try:
MoveFileEx(path, None, dwFlags=library.MOVEFILE_DELAY_UNTIL_REBOOT)
except WindowsAPIError as error:
# If we're not an administrator then we don't
# have the permissions to perform this kind of
# action.
if error.errno == library.ERROR_ACCESS_DENIED:
self.assertFalse(ctypes.windll.shell32.IsUserAnAdmin())
return

raise
else:
self.assertTrue(isfile(path))