~ubuntu-branches/debian/wheezy/calibre/wheezy

« back to all changes in this revision

Viewing changes to src/calibre/utils/linux_trash.py

  • Committer: Package Import Robot
  • Author(s): Martin Pitt
  • Date: 2012-01-07 11:22:54 UTC
  • mfrom: (29.4.10 precise)
  • Revision ID: package-import@ubuntu.com-20120107112254-n1syr437o46ds802
Tags: 0.8.34+dfsg-1
* New upstream version. (Closes: #654751)
* debian/rules: Do not install calibre copy of chardet; instead, add
  build/binary python-chardet dependency.
* Add disable_plugins.py: Disable plugin dialog. It uses a totally
  non-authenticated and non-trusted way of installing arbitrary code.
  (Closes: #640026)
* debian/rules: Install with POSIX locale, to avoid installing translated
  manpages into the standard locations. (Closes: #646674)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
 
3
from __future__ import (unicode_literals, division, absolute_import,
 
4
                        print_function)
 
5
 
 
6
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
 
7
 
 
8
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
 
9
# which should be included with this package. The terms are also available at
 
10
# http://www.hardcoded.net/licenses/bsd_license
 
11
 
 
12
# This is a reimplementation of plat_other.py with reference to the
 
13
# freedesktop.org trash specification:
 
14
#   [1] http://www.freedesktop.org/wiki/Specifications/trash-spec
 
15
#   [2] http://www.ramendik.ru/docs/trashspec.html
 
16
# See also:
 
17
#   [3] http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
 
18
#
 
19
# For external volumes this implementation will raise an exception if it can't
 
20
# find or create the user's trash directory.
 
21
 
 
22
import os, stat
 
23
import os.path as op
 
24
from datetime import datetime
 
25
from urllib import quote
 
26
 
 
27
FILES_DIR = 'files'
 
28
INFO_DIR = 'info'
 
29
INFO_SUFFIX = '.trashinfo'
 
30
 
 
31
# Default of ~/.local/share [3]
 
32
XDG_DATA_HOME = op.expanduser(os.environ.get('XDG_DATA_HOME', '~/.local/share'))
 
33
HOMETRASH = op.join(XDG_DATA_HOME, 'Trash')
 
34
 
 
35
uid = os.getuid()
 
36
TOPDIR_TRASH = '.Trash'
 
37
TOPDIR_FALLBACK = '.Trash-%s'%uid
 
38
 
 
39
def uniquote(raw):
 
40
    if isinstance(raw, unicode):
 
41
        raw = raw.encode('utf-8')
 
42
    return quote(raw).decode('utf-8')
 
43
 
 
44
def is_parent(parent, path):
 
45
    path = op.realpath(path) # In case it's a symlink
 
46
    parent = op.realpath(parent)
 
47
    return path.startswith(parent)
 
48
 
 
49
def format_date(date):
 
50
    return date.strftime("%Y-%m-%dT%H:%M:%S")
 
51
 
 
52
def info_for(src, topdir):
 
53
    # ...it MUST not include a ".."" directory, and for files not "under" that
 
54
    # directory, absolute pathnames must be used. [2]
 
55
    if topdir is None or not is_parent(topdir, src):
 
56
        src = op.abspath(src)
 
57
    else:
 
58
        src = op.relpath(src, topdir)
 
59
 
 
60
    info  = "[Trash Info]\n"
 
61
    info += "Path=" + uniquote(src) + "\n"
 
62
    info += "DeletionDate=" + format_date(datetime.now()) + "\n"
 
63
    return info
 
64
 
 
65
def check_create(dir):
 
66
    # use 0700 for paths [3]
 
67
    if not op.exists(dir):
 
68
        os.makedirs(dir, 0o700)
 
69
 
 
70
def trash_move(src, dst, topdir=None):
 
71
    filename = op.basename(src)
 
72
    filespath = op.join(dst, FILES_DIR)
 
73
    infopath = op.join(dst, INFO_DIR)
 
74
    base_name, ext = op.splitext(filename)
 
75
 
 
76
    counter = 0
 
77
    destname = filename
 
78
    while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
 
79
        counter += 1
 
80
        destname = '%s %s%s' % (base_name, counter, ext)
 
81
 
 
82
    check_create(filespath)
 
83
    check_create(infopath)
 
84
 
 
85
    os.rename(src, op.join(filespath, destname))
 
86
    with open(op.join(infopath, destname + INFO_SUFFIX), 'wb') as f:
 
87
        f.write(info_for(src, topdir))
 
88
 
 
89
def find_mount_point(path):
 
90
    # Even if something's wrong, "/" is a mount point, so the loop will exit.
 
91
    # Use realpath in case it's a symlink
 
92
    path = op.realpath(path) # Required to avoid infinite loop
 
93
    while not op.ismount(path):
 
94
        path = op.split(path)[0]
 
95
    return path
 
96
 
 
97
def find_ext_volume_global_trash(volume_root):
 
98
    # from [2] Trash directories (1) check for a .Trash dir with the right
 
99
    # permissions set.
 
100
    trash_dir = op.join(volume_root, TOPDIR_TRASH)
 
101
    if not op.exists(trash_dir):
 
102
        return None
 
103
 
 
104
    mode = os.lstat(trash_dir).st_mode
 
105
    # vol/.Trash must be a directory, cannot be a symlink, and must have the
 
106
    # sticky bit set.
 
107
    if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
 
108
        return None
 
109
 
 
110
    trash_dir = op.join(trash_dir, str(uid))
 
111
    try:
 
112
        check_create(trash_dir)
 
113
    except OSError:
 
114
        return None
 
115
    return trash_dir
 
116
 
 
117
def find_ext_volume_fallback_trash(volume_root):
 
118
    # from [2] Trash directories (1) create a .Trash-$uid dir.
 
119
    trash_dir = op.join(volume_root, TOPDIR_FALLBACK)
 
120
    # Try to make the directory, if we can't the OSError exception will escape
 
121
    # be thrown out of send2trash.
 
122
    check_create(trash_dir)
 
123
    return trash_dir
 
124
 
 
125
def find_ext_volume_trash(volume_root):
 
126
    trash_dir = find_ext_volume_global_trash(volume_root)
 
127
    if trash_dir is None:
 
128
        trash_dir = find_ext_volume_fallback_trash(volume_root)
 
129
    return trash_dir
 
130
 
 
131
# Pull this out so it's easy to stub (to avoid stubbing lstat itself)
 
132
def get_dev(path):
 
133
    return os.lstat(path).st_dev
 
134
 
 
135
def send2trash(path):
 
136
    if not op.exists(path):
 
137
        raise OSError("File not found: %s" % path)
 
138
    # ...should check whether the user has the necessary permissions to delete
 
139
    # it, before starting the trashing operation itself. [2]
 
140
    if not os.access(path, os.W_OK):
 
141
        raise OSError("Permission denied: %s" % path)
 
142
    # if the file to be trashed is on the same device as HOMETRASH we
 
143
    # want to move it there.
 
144
    path_dev = get_dev(path)
 
145
 
 
146
    # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
 
147
    # home directory, and these paths will be created further on if needed.
 
148
    trash_dev = get_dev(op.expanduser('~'))
 
149
 
 
150
    if path_dev == trash_dev:
 
151
        topdir = XDG_DATA_HOME
 
152
        dest_trash = HOMETRASH
 
153
    else:
 
154
        topdir = find_mount_point(path)
 
155
        trash_dev = get_dev(topdir)
 
156
        if trash_dev != path_dev:
 
157
            raise OSError("Couldn't find mount point for %s" % path)
 
158
        dest_trash = find_ext_volume_trash(topdir)
 
159
    trash_move(path, dest_trash, topdir)