diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py index 6237fa0071..b15a0abbcb 100644 --- a/setup/installer/windows/freeze.py +++ b/setup/installer/windows/freeze.py @@ -41,6 +41,7 @@ DESCRIPTIONS = { 'calibre-server': 'Standalone calibre content server', 'calibre-parallel': 'calibre worker process', 'calibre-smtp' : 'Command line interface for sending books via email', + 'calibre-recycle' : 'Helper program for deleting to recycle bin', } def walk(dir): @@ -81,6 +82,7 @@ class Win32Freeze(Command, WixMixIn): self.initbase() self.build_launchers() + self.build_recycle() self.add_plugins() self.freeze() self.embed_manifests() @@ -218,7 +220,10 @@ class Win32Freeze(Command, WixMixIn): self.info('Adding calibre sources...') for x in glob.glob(self.j(self.SRC, '*')): - shutil.copytree(x, self.j(sp_dir, self.b(x))) + if os.path.isdir(x): + shutil.copytree(x, self.j(sp_dir, self.b(x))) + else: + shutil.copy(x, self.j(sp_dir, self.b(x))) for x in (r'calibre\manual', r'calibre\trac', 'pythonwin'): deld = self.j(sp_dir, x) @@ -534,6 +539,21 @@ class Win32Freeze(Command, WixMixIn): finally: os.chdir(cwd) + def build_recycle(self): + self.info('Building calibre-recycle.exe') + base = self.j(self.src_root, 'setup', 'installer', 'windows') + src = self.j(base, 'recycle.c') + obj = self.j(self.obj_dir, self.b(src)+'.obj') + cflags = '/c /EHsc /MD /W3 /Ox /nologo /D_UNICODE'.split() + if self.newer(obj, src): + cmd = [msvc.cc] + cflags + ['/Fo'+obj, '/Tc'+src] + self.run_builder(cmd, show_output=True) + exe = self.j(self.base, 'calibre-recycle.exe') + cmd = [msvc.linker] + ['/MACHINE:'+machine, + '/SUBSYSTEM:CONSOLE', '/RELEASE', + '/OUT:'+exe] + [self.embed_resources(exe), obj, 'Shell32.lib'] + self.run_builder(cmd) + def build_launchers(self, debug=False): if not os.path.exists(self.obj_dir): os.makedirs(self.obj_dir) diff --git a/setup/installer/windows/recycle.c b/setup/installer/windows/recycle.c new file mode 100644 index 0000000000..3e51bc07a1 --- /dev/null +++ b/setup/installer/windows/recycle.c @@ -0,0 +1,28 @@ +/* + * recycle.c + * Copyright (C) 2013 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "Windows.h" +#include "Shellapi.h" +/* #include */ + +int wmain(int argc, wchar_t *argv[ ]) { + wchar_t buf[512] = {0}; + SHFILEOPSTRUCTW op = {0}; + if (argc != 2) return 1; + if (wcsnlen_s(argv[1], 512) > 510) return 1; + if (wcscpy_s(buf, 512, argv[1]) != 0) return 1; + + op.wFunc = FO_DELETE; + op.pFrom = buf; + op.pTo = NULL; + op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI | FOF_SILENT | FOF_RENAMEONCOLLISION; + + /* wprintf(L"%ls\n", buf); */ + return SHFileOperationW(&op); +} + + diff --git a/src/calibre/test_build.py b/src/calibre/test_build.py index 62443f85ac..17bc829427 100644 --- a/src/calibre/test_build.py +++ b/src/calibre/test_build.py @@ -39,13 +39,6 @@ def test_winutil(): raise RuntimeError('win_pnp_drives returned no drives') print ('win_pnp_drives OK!') -def test_win32(): - from calibre.utils.winshell import desktop - d = desktop() - if not d: - raise RuntimeError('winshell failed') - print ('winshell OK! (%s is the desktop)'%d) - def test_sqlite(): import sqlite3 conn = sqlite3.connect(':memory:') @@ -121,7 +114,6 @@ def test(): test_woff() test_qt() if iswindows: - test_win32() test_winutil() test_wpd() diff --git a/src/calibre/utils/recycle_bin.py b/src/calibre/utils/recycle_bin.py index 7b1b38261b..9721b1e623 100644 --- a/src/calibre/utils/recycle_bin.py +++ b/src/calibre/utils/recycle_bin.py @@ -6,17 +6,109 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os, shutil, time -from functools import partial -from calibre import isbytestring +from calibre import isbytestring, force_unicode from calibre.constants import (iswindows, isosx, plugins, filesystem_encoding, islinux) recycle = None if iswindows: - import calibre.utils.winshell as winshell - recycle = partial(winshell.delete_file, silent=True, no_confirm=True) + import ctypes, subprocess, sys + from ctypes import POINTER, Structure + from ctypes.wintypes import HANDLE, LPVOID, WORD, DWORD, BOOL, ULONG, LPCWSTR + RECYCLE = force_unicode(os.path.join(os.path.dirname(sys.executable), 'calibre-recycle.exe'), filesystem_encoding) + LPDWORD = POINTER(DWORD) + LPHANDLE = POINTER(HANDLE) + ULONG_PTR = POINTER(ULONG) + CREATE_NO_WINDOW = 0x08000000 + INFINITE = 0xFFFFFFFF + WAIT_FAILED = 0xFFFFFFFF + + class SECURITY_ATTRIBUTES(Structure): + _fields_ = [("nLength", DWORD), + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL)] + LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES) + + class STARTUPINFO(Structure): + _fields_ = [("cb", DWORD), + ("lpReserved", LPCWSTR), + ("lpDesktop", LPCWSTR), + ("lpTitle", LPCWSTR), + ("dwX", DWORD), + ("dwY", DWORD), + ("dwXSize", DWORD), + ("dwYSize", DWORD), + ("dwXCountChars", DWORD), + ("dwYCountChars", DWORD), + ("dwFillAttribute", DWORD), + ("dwFlags", DWORD), + ("wShowWindow", WORD), + ("cbReserved2", WORD), + ("lpReserved2", LPVOID), + ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), + ("hStdError", HANDLE)] + LPSTARTUPINFO = POINTER(STARTUPINFO) + + class PROCESS_INFORMATION(Structure): + _fields_ = [("hProcess", HANDLE), + ("hThread", HANDLE), + ("dwProcessId", DWORD), + ("dwThreadId", DWORD)] + LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) + + CreateProcess = ctypes.windll.kernel32.CreateProcessW + CreateProcess.argtypes = [LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFO, + LPPROCESS_INFORMATION] + CreateProcess.restype = BOOL + + WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject + WaitForSingleObject.argtypes = [HANDLE, DWORD] + WaitForSingleObject.restype = DWORD + + GetExitCodeProcess = ctypes.windll.kernel32.GetExitCodeProcess + GetExitCodeProcess.argtypes = [HANDLE, LPDWORD] + GetExitCodeProcess.restype = BOOL + + CloseHandle = ctypes.windll.kernel32.CloseHandle + CloseHandle.argtypes = [HANDLE] + CloseHandle.restype = BOOL + + def recycle(path): + # We have to run the delete to recycle bin in a separate process as the + # morons who wrote SHFileOperation designed it to spin the event loop + # even when no UI is created. And there is no other way to send files + # to the recycle bin on windows. Le Sigh. We dont use subprocess since + # there is no way to pass unicode arguments with subprocess in 2.7 and + # the twit that maintains subprocess believes that this is not an + # bug but a request for a new feature. + if isinstance(path, bytes): + path = path.decode(filesystem_encoding) + si = STARTUPINFO() + si.cb = ctypes.sizeof(si) + pi = PROCESS_INFORMATION() + exit_code = DWORD() + cmd = subprocess.list2cmdline([RECYCLE, path]) + dwCreationFlags = CREATE_NO_WINDOW + if not CreateProcess(None, cmd, None, None, False, dwCreationFlags, + None, None, ctypes.byref(si), ctypes.byref(pi)): + raise ctypes.WinError() + try: + if WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED: + raise ctypes.WinError() + if not GetExitCodeProcess(pi.hProcess, ctypes.byref(exit_code)): + raise ctypes.WinError() + + finally: + CloseHandle(pi.hThread) + CloseHandle(pi.hProcess) + exit_code = exit_code.value + if exit_code != 0: + raise ctypes.WinError(exit_code) + elif isosx: u = plugins['usbobserver'][0] if hasattr(u, 'send2trash'): diff --git a/src/calibre/utils/winshell.py b/src/calibre/utils/winshell.py deleted file mode 100644 index 9769f52e4a..0000000000 --- a/src/calibre/utils/winshell.py +++ /dev/null @@ -1,400 +0,0 @@ -"""winshell - convenience functions to access Windows shell functionality - -Certain aspects of the Windows user interface are grouped by - Microsoft as Shell functions. These include the Desktop, shortcut - icons, special folders (such as My Documents) and a few other things. - -These are mostly available via the shell module of the win32all - extensions, but whenever I need to use them, I've forgotten the - various constants and so on. - -Several of the shell items have two variants: personal and common, - or User and All Users. These refer to systems with profiles in use: - anything from NT upwards, and 9x with Profiles turned on. Where - relevant, the Personal/User version refers to that owned by the - logged-on user and visible only to that user; the Common/All Users - version refers to that maintained by an Administrator and visible - to all users of the system. - -(c) Tim Golden 25th November 2003 -Licensed under the (GPL-compatible) MIT License: -http://www.opensource.org/licenses/mit-license.php - -9th Nov 2005 0.2 . License changed to MIT - . Added functionality using SHFileOperation -25th Nov 2003 0.1 . Initial release by Tim Golden -""" - -__VERSION__ = "0.2" - -import os -from win32com import storagecon -from win32com.shell import shell, shellcon -import pythoncom - -class x_winshell (Exception): - pass - -# -# Although this can be done in one call, Win9x didn't -# support it, so I added this workaround. -# -def get_path (folder_id): - return shell.SHGetPathFromIDList (shell.SHGetSpecialFolderLocation (0, folder_id)) - -def desktop (common=0): - "What folder is equivalent to the current desktop?" - return get_path ((shellcon.CSIDL_DESKTOP, shellcon.CSIDL_COMMON_DESKTOPDIRECTORY)[common]) - -def common_desktop (): -# -# Only here because already used in code -# - return desktop (common=1) - -def application_data (common=0): - "What folder holds application configuration files?" - return get_path ((shellcon.CSIDL_APPDATA, shellcon.CSIDL_COMMON_APPDATA)[common]) - -def favourites (common=0): - "What folder holds the Explorer favourites shortcuts?" - return get_path ((shellcon.CSIDL_FAVORITES, shellcon.CSIDL_COMMON_FAVORITES)[common]) -bookmarks = favourites - -def start_menu (common=0): - "What folder holds the Start Menu shortcuts?" - return get_path ((shellcon.CSIDL_STARTMENU, shellcon.CSIDL_COMMON_STARTMENU)[common]) - -def programs (common=0): - "What folder holds the Programs shortcuts (from the Start Menu)?" - return get_path ((shellcon.CSIDL_PROGRAMS, shellcon.CSIDL_COMMON_PROGRAMS)[common]) - -def startup (common=0): - "What folder holds the Startup shortcuts (from the Start Menu)?" - return get_path ((shellcon.CSIDL_STARTUP, shellcon.CSIDL_COMMON_STARTUP)[common]) - -def personal_folder (): - "What folder holds the My Documents files?" - return get_path (shellcon.CSIDL_PERSONAL) -my_documents = personal_folder - -def recent (): - "What folder holds the Documents shortcuts (from the Start Menu)?" - return get_path (shellcon.CSIDL_RECENT) - -def sendto (): - "What folder holds the SendTo shortcuts (from the Context Menu)?" - return get_path (shellcon.CSIDL_SENDTO) - -# -# Internally abstracted function to handle one -# of several shell-based file manipulation -# routines. Not all the possible parameters -# are covered which might be passed to the -# underlying SHFileOperation API call, but -# only those which seemed useful to me at -# the time. -# -def _file_operation ( - operation, - source_path, - target_path=None, - allow_undo=True, - no_confirm=False, - rename_on_collision=True, - silent=False, - hWnd=None -): - # - # At present the Python wrapper around SHFileOperation doesn't - # allow lists of files. Hopefully it will at some point, so - # take account of it here. - # If you pass this shell function a "/"-separated path with - # a wildcard, eg c:/temp/*.tmp, it gets confused. It's ok - # with a backslash, so convert here. - # - source_path = source_path or "" - if isinstance (source_path, basestring): - source_path = os.path.abspath (source_path) - else: - source_path = [os.path.abspath (i) for i in source_path] - - target_path = target_path or "" - if isinstance (target_path, basestring): - target_path = os.path.abspath (target_path) - else: - target_path = [os.path.abspath (i) for i in target_path] - - flags = 0 - if allow_undo: flags |= shellcon.FOF_ALLOWUNDO - if no_confirm: flags |= shellcon.FOF_NOCONFIRMATION - if rename_on_collision: flags |= shellcon.FOF_RENAMEONCOLLISION - if silent: flags |= shellcon.FOF_SILENT - - result, n_aborted = shell.SHFileOperation ( - (hWnd or 0, operation, source_path, target_path, flags, None, None) - ) - if result <> 0: - raise x_winshell, result - elif n_aborted: - raise x_winshell, "%d operations were aborted by the user" % n_aborted - -def copy_file ( - source_path, - target_path, - allow_undo=True, - no_confirm=False, - rename_on_collision=True, - silent=False, - hWnd=None -): - """Perform a shell-based file copy. Copying in - this way allows the possibility of undo, auto-renaming, - and showing the "flying file" animation during the copy. - - The default options allow for undo, don't automatically - clobber on a name clash, automatically rename on collision - and display the animation. - """ - _file_operation ( - shellcon.FO_COPY, - source_path, - target_path, - allow_undo, - no_confirm, - rename_on_collision, - silent, - hWnd - ) - -def move_file ( - source_path, - target_path, - allow_undo=True, - no_confirm=False, - rename_on_collision=True, - silent=False, - hWnd=None -): - """Perform a shell-based file move. Moving in - this way allows the possibility of undo, auto-renaming, - and showing the "flying file" animation during the copy. - - The default options allow for undo, don't automatically - clobber on a name clash, automatically rename on collision - and display the animation. - """ - _file_operation ( - shellcon.FO_MOVE, - source_path, - target_path, - allow_undo, - no_confirm, - rename_on_collision, - silent, - hWnd - ) - -def rename_file ( - source_path, - target_path, - allow_undo=True, - no_confirm=False, - rename_on_collision=True, - silent=False, - hWnd=None -): - """Perform a shell-based file rename. Renaming in - this way allows the possibility of undo, auto-renaming, - and showing the "flying file" animation during the copy. - - The default options allow for undo, don't automatically - clobber on a name clash, automatically rename on collision - and display the animation. - """ - _file_operation ( - shellcon.FO_RENAME, - source_path, - target_path, - allow_undo, - no_confirm, - rename_on_collision, - silent, - hWnd - ) - -def delete_file ( - source_path, - allow_undo=True, - no_confirm=False, - rename_on_collision=True, - silent=False, - hWnd=None -): - """Perform a shell-based file delete. Deleting in - this way uses the system recycle bin, allows the - possibility of undo, and showing the "flying file" - animation during the delete. - - The default options allow for undo, don't automatically - clobber on a name clash, automatically rename on collision - and display the animation. - """ - _file_operation ( - shellcon.FO_DELETE, - source_path, - None, - allow_undo, - no_confirm, - rename_on_collision, - silent, - hWnd - ) - -def CreateShortcut (Path, Target, Arguments = "", StartIn = "", Icon = ("",0), Description = ""): - """Create a Windows shortcut: - - Path - As what file should the shortcut be created? - Target - What command should the desktop use? - Arguments - What arguments should be supplied to the command? - StartIn - What folder should the command start in? - Icon - (filename, index) What icon should be used for the shortcut? - Description - What description should the shortcut be given? - - eg - CreateShortcut ( - Path=os.path.join (desktop (), "PythonI.lnk"), - Target=r"c:\python\python.exe", - Icon=(r"c:\python\python.exe", 0), - Description="Python Interpreter" - ) - """ - sh = pythoncom.CoCreateInstance ( - shell.CLSID_ShellLink, - None, - pythoncom.CLSCTX_INPROC_SERVER, - shell.IID_IShellLink - ) - - sh.SetPath (Target) - sh.SetDescription (Description) - sh.SetArguments (Arguments) - sh.SetWorkingDirectory (StartIn) - sh.SetIconLocation (Icon[0], Icon[1]) - - persist = sh.QueryInterface (pythoncom.IID_IPersistFile) - persist.Save (Path, 1) - -# -# Constants for structured storage -# -# These come from ObjIdl.h -FMTID_USER_DEFINED_PROPERTIES = "{F29F85E0-4FF9-1068-AB91-08002B27B3D9}" -FMTID_CUSTOM_DEFINED_PROPERTIES = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" - -PIDSI_TITLE = 0x00000002 -PIDSI_SUBJECT = 0x00000003 -PIDSI_AUTHOR = 0x00000004 -PIDSI_CREATE_DTM = 0x0000000c -PIDSI_KEYWORDS = 0x00000005 -PIDSI_COMMENTS = 0x00000006 -PIDSI_TEMPLATE = 0x00000007 -PIDSI_LASTAUTHOR = 0x00000008 -PIDSI_REVNUMBER = 0x00000009 -PIDSI_EDITTIME = 0x0000000a -PIDSI_LASTPRINTED = 0x0000000b -PIDSI_LASTSAVE_DTM = 0x0000000d -PIDSI_PAGECOUNT = 0x0000000e -PIDSI_WORDCOUNT = 0x0000000f -PIDSI_CHARCOUNT = 0x00000010 -PIDSI_THUMBNAIL = 0x00000011 -PIDSI_APPNAME = 0x00000012 -PROPERTIES = ( - PIDSI_TITLE, - PIDSI_SUBJECT, - PIDSI_AUTHOR, - PIDSI_CREATE_DTM, - PIDSI_KEYWORDS, - PIDSI_COMMENTS, - PIDSI_TEMPLATE, - PIDSI_LASTAUTHOR, - PIDSI_EDITTIME, - PIDSI_LASTPRINTED, - PIDSI_LASTSAVE_DTM, - PIDSI_PAGECOUNT, - PIDSI_WORDCOUNT, - PIDSI_CHARCOUNT, - PIDSI_APPNAME -) - -# -# This was taken from someone else's example, -# but I can't find where. If you know, please -# tell me so I can give due credit. -# -def structured_storage (filename): - """Pick out info from MS documents with embedded - structured storage (typically MS Word docs etc.) - - Returns a dictionary of information found - """ - - if not pythoncom.StgIsStorageFile (filename): - return {} - - flags = storagecon.STGM_READ | storagecon.STGM_SHARE_EXCLUSIVE - storage = pythoncom.StgOpenStorage (filename, None, flags) - try: - properties_storage = storage.QueryInterface (pythoncom.IID_IPropertySetStorage) - except pythoncom.com_error: - return {} - - property_sheet = properties_storage.Open (FMTID_USER_DEFINED_PROPERTIES) - try: - data = property_sheet.ReadMultiple (PROPERTIES) - finally: - property_sheet = None - - title, subject, author, created_on, keywords, comments, template_used, \ - updated_by, edited_on, printed_on, saved_on, \ - n_pages, n_words, n_characters, \ - application = data - - result = {} - if title: result['title'] = title - if subject: result['subject'] = subject - if author: result['author'] = author - if created_on: result['created_on'] = created_on - if keywords: result['keywords'] = keywords - if comments: result['comments'] = comments - if template_used: result['template_used'] = template_used - if updated_by: result['updated_by'] = updated_by - if edited_on: result['edited_on'] = edited_on - if printed_on: result['printed_on'] = printed_on - if saved_on: result['saved_on'] = saved_on - if n_pages: result['n_pages'] = n_pages - if n_words: result['n_words'] = n_words - if n_characters: result['n_characters'] = n_characters - if application: result['application'] = application - return result - -if __name__ == '__main__': - try: - print 'Desktop =>', desktop () - print 'Common Desktop =>', desktop (1) - print 'Application Data =>', application_data () - print 'Common Application Data =>', application_data (1) - print 'Bookmarks =>', bookmarks () - print 'Common Bookmarks =>', bookmarks (1) - print 'Start Menu =>', start_menu () - print 'Common Start Menu =>', start_menu (1) - print 'Programs =>', programs () - print 'Common Programs =>', programs (1) - print 'Startup =>', startup () - print 'Common Startup =>', startup (1) - print 'My Documents =>', my_documents () - print 'Recent =>', recent () - print 'SendTo =>', sendto () - finally: - raw_input ("Press enter...") -