initial attempt with ctypes

This commit is contained in:
acheronfail 2018-04-13 07:36:57 +10:00
parent 1dded4f572
commit 3eb9fa0eda
1 changed files with 111 additions and 2 deletions

View File

@ -6,18 +6,21 @@
from __future__ import unicode_literals
from ctypes import cdll, byref, Structure, c_char, c_char_p
from ctypes import cdll, byref, Structure, c_char, c_char_p, c_void_p, c_int64, sizeof
from ctypes.util import find_library
from .compat import binary_type
Foundation = cdll.LoadLibrary(find_library('Foundation'))
CoreServices = cdll.LoadLibrary(find_library('CoreServices'))
objc = cdll.LoadLibrary(find_library('objc'))
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
GetMacOSStatusCommentString.restype = c_char_p
FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions
FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync
AEGetParamDesc = CoreServices.AEGetParamDesc
AESendMessage = CoreServices.AESendMessage
kFSPathMakeRefDefaultOptions = 0
kFSPathMakeRefDoNotFollowLeafSymlink = 0x01
@ -36,9 +39,12 @@ def check_op_result(op_result):
msg = GetMacOSStatusCommentString(op_result).decode('utf-8')
raise OSError(msg)
def send2trash(path):
def send2trash(path, with_put_back=False):
if not isinstance(path, binary_type):
path = path.encode('utf-8')
_with_put_back(path) if with_put_back else _normally(path)
def _normally(path):
fp = FSRef()
opts = kFSPathMakeRefDoNotFollowLeafSymlink
op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None)
@ -46,3 +52,106 @@ def send2trash(path):
opts = kFSFileOperationDefaultOptions
op_result = FSMoveObjectToTrashSync(byref(fp), None, opts)
check_op_result(op_result)
# Everything below here is required for getting the "put back" feature
# in the macOS trash - we attempt to ask Finder the trash the file for us.
objc.objc_getClass.restype = c_void_p
objc.sel_registerName.restype = c_void_p
objc.objc_msgSend.restype = c_void_p
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
_msg = objc.objc_msgSend
_cls = objc.objc_getClass
_sel = objc.sel_registerName
kAEWaitReply = 0x3
kAnyTransactionID = 0
kAEDefaultTimeout = -1
kAutoGenerateReturnID = -1
NSUTF8StringEncoding = 0x4
# TODO: this is probably incorrect/or changes depending on arch, etc
typeKernelProcessID = 0x6b706964
# TODO: not sure exactly what these should be
keyDirectObject = '----'
typeWildCard = '****'
class AEDataStorage(Structure):
_fields_ = []
# AppleEvent seems to be an alias of AEDesc
class AEDesc(Structure):
# DescType is an unsigned int ?
# AEDataStorage is something else ?
_fields_ = [('descriptorType', c_int64), ('dataHandle', AEDataStorage)]
# This was inspired by https://github.com/ali-rantakari/trash.
# See https://github.com/ali-rantakari/trash/blob/master/trash.m#L263-L341
# and also https://stackoverflow.com/a/1490644/5552584.
def _with_put_back(path):
# Create an autorelease pool.
NSAutoreleasePool = _cls('NSAutoreleasePool')
pool = _msg(NSAutoreleasePool, _sel('alloc'))
pool = _msg(pool, _sel('init'))
try:
# Generate list descriptor containing the file URL
NSAppleEventDescriptor = _cls('NSAppleEventDescriptor')
url_list_descr = _msg(NSAppleEventDescriptor, _sel('listDescriptor'))
url = _msg(_cls('NSURL'), _sel('fileURLWithPath'), path)
url_str = _msg(url, _sel('absoluteString'))
data = _msg(url_str, _sel('dataUsingEncoding:'), NSUTF8StringEncoding)
descr = _msg(NSAppleEventDescriptor,
_sel('descriptorWithDescriptorType:'), 'furl',
_sel('data:'), data)
_msg(url_list_descr,
_sel('insertDescriptor:'), descr,
_sel('atIndex:'), 1)
# Generate the 'top level' "delete" descriptor
finder_pid = _get_finder_pid()
target_descr = _msg(NSAppleEventDescriptor,
_sel('descriptorWithDescriptorType:'), typeKernelProcessID,
_sel('bytes:'), byref(finder_pid),
_sel('length:'), sizeof(finder_pid))
descriptor = _msg(NSAppleEventDescriptor,
_sel('appleEventWithEventClass:'), 'core',
_sel('eventID:'), 'delo',
_sel('targetDescriptor:'), target_descr,
_sel('returnID:'), kAutoGenerateReturnID,
_sel('transactionID:'), kAnyTransactionID)
# add the list of file URLs as argument
_msg(descriptor,
_sel('setDescriptor:'), url_list_descr,
_sel('forKeyword:'), '----')
# send the Apple Event synchronously
reply_event = AEDesc()
op_result = AESendMessage(_msg(descriptor, _sel('asDesc')),
byref(reply_event), kAEWaitReply, kAEDefaultTimeout)
check_op_result(op_result)
# check reply in order to determine return value
reply_ae_descr = AEDesc()
op_result = AEGetParamDesc(byref(reply_event), keyDirectObject,
typeWildCard, byref(reply_ae_descr))
check_op_result(op_result)
reply_descr = _msg(_msg(_msg(NSAppleEventDescriptor, _sel('alloc')),
_sel('initWithAEDescNoCopy:'), byref(reply_ae_descr)), _sel('autorelease'))
if _msg(reply_descr, _sel('numberOfItems')) == 0:
raise Exception('file could not be trashed')
finally:
_msg(pool, _sel('release'))
def _get_finder_pid():
import subprocess
child = subprocess.Popen(['pgrep', '-f', 'Finder'],
stdout=subprocess.PIPE, shell=False)
response = child.communicate()[0]
# TODO: assumed that pid_t is a c_int64 (should depend on arch)
return c_int64(int(response.split()[0]))