summaryrefslogtreecommitdiff
path: root/qutebrowser/utils/standarddir.py
blob: 87effe6e0a37fa44aeeab2edbb85784c96a67309 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.

"""Utilities to get and initialize data/config paths."""

import os
import sys
import os.path

from PyQt5.QtCore import QCoreApplication, QStandardPaths

from qutebrowser.utils import log, qtutils, debug


# The argparse namespace passed to init()
_args = None


def config():
    """Get a location for configs."""
    typ = QStandardPaths.ConfigLocation
    overridden, path = _from_args(typ, _args)
    if not overridden:
        path = _writable_location(typ)
        appname = QCoreApplication.instance().applicationName()
        if path.split(os.sep)[-1] != appname:  # pragma: no branch
            # WORKAROUND - see
            # https://bugreports.qt.io/browse/QTBUG-38872
            path = os.path.join(path, appname)
    _maybe_create(path)
    return path


def data():
    """Get a location for data."""
    typ = QStandardPaths.DataLocation
    overridden, path = _from_args(typ, _args)
    if not overridden:
        path = _writable_location(typ)
        if os.name == 'nt':
            # Under windows, config/data might end up in the same directory.
            data_path = QStandardPaths.writableLocation(
                QStandardPaths.DataLocation)
            config_path = QStandardPaths.writableLocation(
                QStandardPaths.ConfigLocation)
            if data_path == config_path:
                path = os.path.join(path, 'data')
    _maybe_create(path)
    return path


def system_data():
    """Get a location for system-wide data. This path may be read-only."""
    if sys.platform.startswith('linux'):
        path = "/usr/share/qutebrowser"
        if not os.path.exists(path):
            path = data()
    else:
        path = data()
    return path


def cache():
    """Get a location for the cache."""
    typ = QStandardPaths.CacheLocation
    overridden, path = _from_args(typ, _args)
    if not overridden:
        path = _writable_location(typ)
    _maybe_create(path)
    return path


def download():
    """Get a location for downloads."""
    typ = QStandardPaths.DownloadLocation
    overridden, path = _from_args(typ, _args)
    if not overridden:
        path = _writable_location(typ)
    _maybe_create(path)
    return path


def runtime():
    """Get a location for runtime data."""
    if sys.platform.startswith('linux'):
        typ = QStandardPaths.RuntimeLocation
    else:  # pragma: no cover
        # RuntimeLocation is a weird path on OS X and Windows.
        typ = QStandardPaths.TempLocation
    overridden, path = _from_args(typ, _args)
    if not overridden:
        path = _writable_location(typ)
        # This is generic, but per-user.
        #
        # For TempLocation:
        # "The returned value might be application-specific, shared among
        # other applications for this user, or even system-wide."
        #
        # Unfortunately this path could get too long for sockets (which have a
        # maximum length of 104 chars), so we don't add the username here...
        appname = QCoreApplication.instance().applicationName()
        path = os.path.join(path, appname)
    _maybe_create(path)
    return path


def _writable_location(typ):
    """Wrapper around QStandardPaths.writableLocation."""
    with qtutils.unset_organization():
        path = QStandardPaths.writableLocation(typ)
    typ_str = debug.qenum_key(QStandardPaths, typ)
    log.misc.debug("writable location for {}: {}".format(typ_str, path))
    if not path:
        raise ValueError("QStandardPaths returned an empty value!")
    # Qt seems to use '/' as path separator even on Windows...
    path = path.replace('/', os.sep)
    return path


def _from_args(typ, args):
    """Get the standard directory from an argparse namespace.

    Args:
        typ: A member of the QStandardPaths::StandardLocation enum
        args: An argparse namespace or None.

    Return:
        A (override, path) tuple.
            override: boolean, if the user did override the path
            path: The overridden path, or None to turn off storage.
    """
    # pylint: disable=too-many-return-statements
    typ_to_argparse_arg = {
        QStandardPaths.ConfigLocation: 'confdir',
        QStandardPaths.DataLocation: 'datadir',
        QStandardPaths.CacheLocation: 'cachedir',
    }
    basedir_suffix = {
        QStandardPaths.ConfigLocation: 'config',
        QStandardPaths.DataLocation: 'data',
        QStandardPaths.CacheLocation: 'cache',
        QStandardPaths.DownloadLocation: 'download',
        QStandardPaths.RuntimeLocation: 'runtime',
    }

    if args is None:
        return (False, None)

    if getattr(args, 'basedir', None) is not None:
        basedir = args.basedir

        try:
            suffix = basedir_suffix[typ]
        except KeyError:  # pragma: no cover
            return (False, None)
        return (True, os.path.join(basedir, suffix))

    try:
        argname = typ_to_argparse_arg[typ]
    except KeyError:
        return (False, None)
    arg_value = getattr(args, argname)
    if arg_value is None:
        return (False, None)
    elif arg_value == '':
        return (True, None)
    else:
        return (True, arg_value)


def _maybe_create(path):
    """Create the `path` directory if path is not None.

    From the XDG basedir spec:
        If, when attempting to write a file, the destination directory is
        non-existent an attempt should be made to create it with permission
        0700. If the destination directory exists already the permissions
        should not be changed.
    """
    if path is not None:
        try:
            os.makedirs(path, 0o700)
        except FileExistsError:
            pass


def init(args):
    """Initialize all standard dirs."""
    global _args
    if args is not None:
        # args can be None during tests
        log.init.debug("Base directory: {}".format(args.basedir))
    _args = args
    _init_cachedir_tag()


def _init_cachedir_tag():
    """Create CACHEDIR.TAG if it doesn't exist.

    See http://www.brynosaurus.com/cachedir/spec.html
    """
    cache_dir = cache()
    if cache_dir is None:
        return
    cachedir_tag = os.path.join(cache_dir, 'CACHEDIR.TAG')
    if not os.path.exists(cachedir_tag):
        try:
            with open(cachedir_tag, 'w', encoding='utf-8') as f:
                f.write("Signature: 8a477f597d28d172789f06886806bc55\n")
                f.write("# This file is a cache directory tag created by "
                        "qutebrowser.\n")
                f.write("# For information about cache directory tags, see:\n")
                f.write("#  http://www.brynosaurus.com/"  # pragma: no branch
                        "cachedir/\n")
        except OSError:
            log.init.exception("Failed to create CACHEDIR.TAG")