Added code to Project to move deleted files/directories to the recycle bin falling back to removing them (os.remove), if send2trash cannot be imported due to missing dependencies.

Fri, 20 Jun 2014 16:05:45 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 20 Jun 2014 16:05:45 +0200
changeset 3644
a2c88b9b1d16
parent 3642
1a6f8d9b63a5
child 3646
cfbb47b6d885

Added code to Project to move deleted files/directories to the recycle bin falling back to removing them (os.remove), if send2trash cannot be imported due to missing dependencies.

Project/Project.py file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/LICENSE file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/__init__.py file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/send2trash/__init__.py file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/send2trash/compat.py file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/send2trash/plat_gio.py file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/send2trash/plat_osx.py file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/send2trash/plat_other.py file | annotate | diff | comparison | revisions
ThirdParty/Send2Trash/send2trash/plat_win.py file | annotate | diff | comparison | revisions
changelog file | annotate | diff | comparison | revisions
eric5.e4p file | annotate | diff | comparison | revisions
--- a/Project/Project.py	Thu Jun 19 10:00:53 2014 +0200
+++ b/Project/Project.py	Fri Jun 20 16:05:45 2014 +0200
@@ -1308,14 +1308,19 @@
         
         @param langFile the translation file to be removed (string)
         """
+        try:
+            from ThirdParty.Send2Trash.send2trash import send2trash as s2t
+        except ImportError:
+            s2t = os.remove
+        
         langFile = self.getRelativePath(langFile)
         qmFile = self.__binaryTranslationFile(langFile)
         
         try:
             fn = os.path.join(self.ppath, langFile)
             if os.path.exists(fn):
-                os.remove(fn)
-        except IOError:
+                s2t(fn)
+        except EnvironmentError:
             E5MessageBox.critical(
                 self.ui,
                 self.tr("Delete translation"),
@@ -1335,8 +1340,8 @@
                                      os.path.basename(qmFile)))
                 fn = os.path.join(self.ppath, qmFile)
                 if os.path.exists(fn):
-                    os.remove(fn)
-            except IOError:
+                    s2t(fn)
+            except EnvironmentError:
                 E5MessageBox.critical(
                     self.ui,
                     self.tr("Delete translation"),
@@ -1939,22 +1944,27 @@
         @return flag indicating success (boolean)
         """
         try:
-            os.remove(os.path.join(self.ppath, fn))
+            from ThirdParty.Send2Trash.send2trash import send2trash as s2t
+        except ImportError:
+            s2t = os.remove
+        
+        try:
+            s2t(os.path.join(self.ppath, fn))
             path, ext = os.path.splitext(fn)
             if ext == '.ui':
                 fn2 = os.path.join(self.ppath, '{0}.h'.format(fn))
                 if os.path.isfile(fn2):
-                    os.remove(fn2)
+                    s2t(fn2)
             head, tail = os.path.split(path)
             for ext in ['.pyc', '.pyo']:
                 fn2 = os.path.join(self.ppath, path + ext)
                 if os.path.isfile(fn2):
-                    os.remove(fn2)
+                    s2t(fn2)
                 pat = os.path.join(
                     self.ppath, head,
                     "__pycache__", "{0}.*{1}".format(tail, ext))
                 for f in glob.glob(pat):
-                    os.remove(f)
+                    s2t(f)
         except EnvironmentError:
             E5MessageBox.critical(
                 self.ui,
@@ -1979,7 +1989,11 @@
         if not os.path.isabs(dn):
             dn = os.path.join(self.ppath, dn)
         try:
-            shutil.rmtree(dn, True)
+            try:
+                from ThirdParty.Send2Trash.send2trash import send2trash
+                send2trash(dn)
+            except ImportError:
+                shutil.rmtree(dn, True)
         except EnvironmentError:
             E5MessageBox.critical(
                 self.ui,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/LICENSE	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,10 @@
+Copyright (c) 2013, Hardcoded Software, http://www.hardcoded.net
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+    * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/__init__.py	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2014 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package containing the send2trash package.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/send2trash/__init__.py	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,19 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+import sys
+
+if sys.platform == 'darwin':
+    from .plat_osx import send2trash
+elif sys.platform == 'win32':
+    from .plat_win import send2trash
+else:
+    try:
+        # If we can use gio, let's use it
+        from .plat_gio import send2trash
+    except ImportError:
+        # Oh well, let's fallback to our own Freedesktop trash implementation
+        from .plat_other import send2trash
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/send2trash/compat.py	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,13 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+import sys
+if sys.version < '3':
+    text_type = unicode
+    binary_type = str
+else:
+    text_type = str
+    binary_type = bytes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/send2trash/plat_gio.py	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,14 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+from gi.repository import GObject, Gio
+
+def send2trash(path):
+    try:
+        f = Gio.File.new_for_path(path)
+        f.trash(cancellable=None)
+    except GObject.GError as e:
+        raise OSError(e.message)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/send2trash/plat_osx.py	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,48 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+from __future__ import unicode_literals
+
+from ctypes import cdll, byref, Structure, c_char, c_char_p
+from ctypes.util import find_library
+
+from .compat import binary_type
+
+Foundation = cdll.LoadLibrary(find_library('Foundation'))
+CoreServices = cdll.LoadLibrary(find_library('CoreServices'))
+
+GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
+GetMacOSStatusCommentString.restype = c_char_p
+FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions
+FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync
+
+kFSPathMakeRefDefaultOptions = 0
+kFSPathMakeRefDoNotFollowLeafSymlink = 0x01
+
+kFSFileOperationDefaultOptions = 0
+kFSFileOperationOverwrite = 0x01
+kFSFileOperationSkipSourcePermissionErrors = 0x02
+kFSFileOperationDoNotMoveAcrossVolumes = 0x04
+kFSFileOperationSkipPreflight = 0x08
+
+class FSRef(Structure):
+    _fields_ = [('hidden', c_char * 80)]
+
+def check_op_result(op_result):
+    if op_result:
+        msg = GetMacOSStatusCommentString(op_result).decode('utf-8')
+        raise OSError(msg)
+
+def send2trash(path):
+    if not isinstance(path, binary_type):
+        path = path.encode('utf-8')
+    fp = FSRef()
+    opts = kFSPathMakeRefDoNotFollowLeafSymlink
+    op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None)
+    check_op_result(op_result)
+    opts = kFSFileOperationDefaultOptions
+    op_result = FSMoveObjectToTrashSync(byref(fp), None, opts)
+    check_op_result(op_result)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/send2trash/plat_other.py	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,167 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+# This is a reimplementation of plat_other.py with reference to the
+# freedesktop.org trash specification:
+#   [1] http://www.freedesktop.org/wiki/Specifications/trash-spec
+#   [2] http://www.ramendik.ru/docs/trashspec.html
+# See also:
+#   [3] http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+#
+# For external volumes this implementation will raise an exception if it can't
+# find or create the user's trash directory.
+
+from __future__ import unicode_literals
+
+import sys
+import os
+import os.path as op
+from datetime import datetime
+import stat
+try:
+    from urllib.parse import quote
+except ImportError:
+    # Python 2
+    from urllib import quote
+
+FILES_DIR = 'files'
+INFO_DIR = 'info'
+INFO_SUFFIX = '.trashinfo'
+
+# Default of ~/.local/share [3]
+XDG_DATA_HOME = op.expanduser(os.environ.get('XDG_DATA_HOME', '~/.local/share'))
+HOMETRASH = op.join(XDG_DATA_HOME, 'Trash')
+
+uid = os.getuid()
+TOPDIR_TRASH = '.Trash'
+TOPDIR_FALLBACK = '.Trash-' + str(uid)
+
+def is_parent(parent, path):
+    path = op.realpath(path) # In case it's a symlink
+    parent = op.realpath(parent)
+    return path.startswith(parent)
+
+def format_date(date):
+    return date.strftime("%Y-%m-%dT%H:%M:%S")
+
+def info_for(src, topdir):
+    # ...it MUST not include a ".."" directory, and for files not "under" that
+    # directory, absolute pathnames must be used. [2]
+    if topdir is None or not is_parent(topdir, src):
+        src = op.abspath(src)
+    else:
+        src = op.relpath(src, topdir)
+
+    info  = "[Trash Info]\n"
+    info += "Path=" + quote(src) + "\n"
+    info += "DeletionDate=" + format_date(datetime.now()) + "\n"
+    return info
+
+def check_create(dir):
+    # use 0700 for paths [3]
+    if not op.exists(dir):
+        os.makedirs(dir, 0o700)
+
+def trash_move(src, dst, topdir=None):
+    filename = op.basename(src)
+    filespath = op.join(dst, FILES_DIR)
+    infopath = op.join(dst, INFO_DIR)
+    base_name, ext = op.splitext(filename)
+
+    counter = 0
+    destname = filename
+    while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
+        counter += 1
+        destname = '%s %s%s' % (base_name, counter, ext)
+    
+    check_create(filespath)
+    check_create(infopath)
+    
+    os.rename(src, op.join(filespath, destname))
+    f = open(op.join(infopath, destname + INFO_SUFFIX), 'w')
+    f.write(info_for(src, topdir))
+    f.close()
+    
+    # added by detlev@die-offenbachs.de to update the Trash metadata file
+    metadata = op.join(dst, "metadata")
+    entriesCount = len(os.listdir(filespath))
+    f = open(metadata, 'w')
+    f.write("[Cached]\nSize={0}\n".format(entriesCount))
+    f.close()
+
+def find_mount_point(path):
+    # Even if something's wrong, "/" is a mount point, so the loop will exit.
+    # Use realpath in case it's a symlink
+    path = op.realpath(path) # Required to avoid infinite loop
+    while not op.ismount(path):
+        path = op.split(path)[0]
+    return path
+
+def find_ext_volume_global_trash(volume_root):
+    # from [2] Trash directories (1) check for a .Trash dir with the right
+    # permissions set.
+    trash_dir = op.join(volume_root, TOPDIR_TRASH)
+    if not op.exists(trash_dir):
+        return None
+    
+    mode = os.lstat(trash_dir).st_mode
+    # vol/.Trash must be a directory, cannot be a symlink, and must have the
+    # sticky bit set.
+    if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
+        return None
+
+    trash_dir = op.join(trash_dir, str(uid))
+    try:
+        check_create(trash_dir)
+    except OSError:
+        return None
+    return trash_dir
+
+def find_ext_volume_fallback_trash(volume_root):
+    # from [2] Trash directories (1) create a .Trash-$uid dir.
+    trash_dir = op.join(volume_root, TOPDIR_FALLBACK)
+    # Try to make the directory, if we can't the OSError exception will escape
+    # be thrown out of send2trash.
+    check_create(trash_dir)
+    return trash_dir
+
+def find_ext_volume_trash(volume_root):
+    trash_dir = find_ext_volume_global_trash(volume_root)
+    if trash_dir is None:
+        trash_dir = find_ext_volume_fallback_trash(volume_root)
+    return trash_dir
+
+# Pull this out so it's easy to stub (to avoid stubbing lstat itself)
+def get_dev(path):
+    return os.lstat(path).st_dev
+
+def send2trash(path):
+    if not isinstance(path, str):
+        path = str(path, sys.getfilesystemencoding())
+    if not op.exists(path):
+        raise OSError("File not found: %s" % path)
+    # ...should check whether the user has the necessary permissions to delete
+    # it, before starting the trashing operation itself. [2]
+    if not os.access(path, os.W_OK):
+        raise OSError("Permission denied: %s" % path)
+    # if the file to be trashed is on the same device as HOMETRASH we
+    # want to move it there.
+    path_dev = get_dev(path)
+    
+    # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
+    # home directory, and these paths will be created further on if needed.
+    trash_dev = get_dev(op.expanduser('~'))
+
+    if path_dev == trash_dev:
+        topdir = XDG_DATA_HOME
+        dest_trash = HOMETRASH
+    else:
+        topdir = find_mount_point(path)
+        trash_dev = get_dev(topdir)
+        if trash_dev != path_dev:
+            raise OSError("Couldn't find mount point for %s" % path)
+        dest_trash = find_ext_volume_trash(topdir)
+    trash_move(path, dest_trash, topdir)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ThirdParty/Send2Trash/send2trash/plat_win.py	Fri Jun 20 16:05:45 2014 +0200
@@ -0,0 +1,59 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+from __future__ import unicode_literals
+
+from ctypes import windll, Structure, byref, c_uint
+from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
+import os.path as op
+
+from .compat import text_type
+
+shell32 = windll.shell32
+SHFileOperationW = shell32.SHFileOperationW
+
+class SHFILEOPSTRUCTW(Structure):
+    _fields_ = [
+        ("hwnd", HWND),
+        ("wFunc", UINT),
+        ("pFrom", LPCWSTR),
+        ("pTo", LPCWSTR),
+        ("fFlags", c_uint),
+        ("fAnyOperationsAborted", BOOL),
+        ("hNameMappings", c_uint),
+        ("lpszProgressTitle", LPCWSTR),
+        ]
+
+FO_MOVE = 1
+FO_COPY = 2
+FO_DELETE = 3
+FO_RENAME = 4
+
+FOF_MULTIDESTFILES = 1
+FOF_SILENT = 4
+FOF_NOCONFIRMATION = 16
+FOF_ALLOWUNDO = 64
+FOF_NOERRORUI = 1024
+
+def send2trash(path):
+    if not isinstance(path, text_type):
+        path = text_type(path, 'mbcs')
+    if not op.isabs(path):
+        path = op.abspath(path)
+    fileop = SHFILEOPSTRUCTW()
+    fileop.hwnd = 0
+    fileop.wFunc = FO_DELETE
+    fileop.pFrom = LPCWSTR(path + '\0')
+    fileop.pTo = None
+    fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
+    fileop.fAnyOperationsAborted = 0
+    fileop.hNameMappings = 0
+    fileop.lpszProgressTitle = None
+    result = SHFileOperationW(byref(fileop))
+    if result:
+        msg = "Couldn't perform operation. Error code: %d" % result
+        raise OSError(msg)
+
--- a/changelog	Thu Jun 19 10:00:53 2014 +0200
+++ b/changelog	Fri Jun 20 16:05:45 2014 +0200
@@ -35,6 +35,9 @@
      'python2Compatible'
 - Project
   -- added support for JavaScript projects
+  -- added code to move deleted files/directories to the recycle bin falling
+     back to removing them (os.remove), if send2trash cannot be imported due
+     to missing dependencies
 - Version Control System Interfaces
   -- All
      --- made the status LED (right lower corner) clickable (depending on
--- a/eric5.e4p	Thu Jun 19 10:00:53 2014 +0200
+++ b/eric5.e4p	Fri Jun 20 16:05:45 2014 +0200
@@ -1141,6 +1141,13 @@
     <Source>Plugins/VcsPlugins/vcsMercurial/HgConflictsListDialog.py</Source>
     <Source>cleanupSource.py</Source>
     <Source>Utilities/MimeTypes.py</Source>
+    <Source>ThirdParty/Send2Trash/__init__.py</Source>
+    <Source>ThirdParty/Send2Trash/send2trash/plat_win.py</Source>
+    <Source>ThirdParty/Send2Trash/send2trash/__init__.py</Source>
+    <Source>ThirdParty/Send2Trash/send2trash/plat_osx.py</Source>
+    <Source>ThirdParty/Send2Trash/send2trash/plat_other.py</Source>
+    <Source>ThirdParty/Send2Trash/send2trash/compat.py</Source>
+    <Source>ThirdParty/Send2Trash/send2trash/plat_gio.py</Source>
   </Sources>
   <Forms>
     <Form>PyUnit/UnittestDialog.ui</Form>
@@ -1614,6 +1621,7 @@
     <Other>Helpviewer/OpenSearch/DefaultSearchEngines/Wiktionary.xml</Other>
     <Other>APIs/QSS/qss.api</Other>
     <Other>eric5.appdata.xml</Other>
+    <Other>ThirdParty/Send2Trash/LICENSE</Other>
   </Others>
   <MainScript>eric5.py</MainScript>
   <Vcs>

eric ide

mercurial