Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 

 

# Copyright 2014-2018 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 os.path 

import sys 

import shutil 

import contextlib 

import enum 

 

from PyQt5.QtCore import QStandardPaths 

from PyQt5.QtWidgets import QApplication 

 

from qutebrowser.utils import log, debug, message, utils 

 

# The cached locations 

_locations = {} 

 

 

Location = enum.Enum('Location', ['config', 'auto_config', 

'data', 'system_data', 

'cache', 'download', 'runtime']) 

 

 

APPNAME = 'qutebrowser' 

 

 

class EmptyValueError(Exception): 

 

"""Error raised when QStandardPaths returns an empty value.""" 

 

 

@contextlib.contextmanager 

def _unset_organization(): 

"""Temporarily unset QApplication.organizationName(). 

 

This is primarily needed in config.py. 

""" 

qapp = QApplication.instance() 

if qapp is not None: 

orgname = qapp.organizationName() 

qapp.setOrganizationName(None) 

try: 

yield 

finally: 

if qapp is not None: 

qapp.setOrganizationName(orgname) 

 

 

def _init_config(args): 

"""Initialize the location for configs.""" 

typ = QStandardPaths.ConfigLocation 

overridden, path = _from_args(typ, args) 

if not overridden: 

73 ↛ 74line 73 didn't jump to line 74, because the condition on line 73 was never true if utils.is_windows: 

app_data_path = _writable_location( 

QStandardPaths.AppDataLocation) 

path = os.path.join(app_data_path, 'config') 

else: 

path = _writable_location(typ) 

_create(path) 

_locations[Location.config] = path 

_locations[Location.auto_config] = path 

 

# Override the normal (non-auto) config on macOS 

84 ↛ exitline 84 didn't return from function '_init_config', because the condition on line 84 was never false if utils.is_mac: 

overridden, path = _from_args(typ, args) 

if not overridden: # pragma: no branch 

path = os.path.expanduser('~/.' + APPNAME) 

_create(path) 

_locations[Location.config] = path 

 

 

def config(auto=False): 

"""Get the location for the config directory. 

 

If auto=True is given, get the location for the autoconfig.yml directory, 

which is different on macOS. 

""" 

if auto: 

return _locations[Location.auto_config] 

return _locations[Location.config] 

 

 

def _init_data(args): 

"""Initialize the location for data.""" 

typ = QStandardPaths.DataLocation 

overridden, path = _from_args(typ, args) 

if not overridden: 

108 ↛ 109line 108 didn't jump to line 109, because the condition on line 108 was never true if utils.is_windows: 

app_data_path = _writable_location(QStandardPaths.AppDataLocation) 

path = os.path.join(app_data_path, 'data') 

elif sys.platform.startswith('haiku'): 

# HaikuOS returns an empty value for AppDataLocation 

config_path = _writable_location(QStandardPaths.ConfigLocation) 

path = os.path.join(config_path, 'data') 

else: 

path = _writable_location(typ) 

_create(path) 

_locations[Location.data] = path 

 

# system_data 

_locations.pop(Location.system_data, None) # Remove old state 

122 ↛ 123line 122 didn't jump to line 123, because the condition on line 122 was never true if utils.is_linux: 

path = '/usr/share/' + APPNAME 

if os.path.exists(path): 

_locations[Location.system_data] = path 

 

 

def data(system=False): 

"""Get the data directory. 

 

If system=True is given, gets the system-wide (probably non-writable) data 

directory. 

""" 

if system: 

try: 

return _locations[Location.system_data] 

except KeyError: 

pass 

return _locations[Location.data] 

 

 

def _init_cache(args): 

"""Initialize the location for the cache.""" 

typ = QStandardPaths.CacheLocation 

overridden, path = _from_args(typ, args) 

if not overridden: 

147 ↛ 149line 147 didn't jump to line 149, because the condition on line 147 was never true if utils.is_windows: 

# Local, not Roaming! 

data_path = _writable_location(QStandardPaths.DataLocation) 

path = os.path.join(data_path, 'cache') 

else: 

path = _writable_location(typ) 

_create(path) 

_locations[Location.cache] = path 

 

 

def cache(): 

return _locations[Location.cache] 

 

 

def _init_download(args): 

"""Initialize the location for downloads. 

 

Note this is only the default directory as found by Qt. 

Therefore, we also don't create it. 

""" 

typ = QStandardPaths.DownloadLocation 

overridden, path = _from_args(typ, args) 

if not overridden: 

path = _writable_location(typ) 

_locations[Location.download] = path 

 

 

def download(): 

return _locations[Location.download] 

 

 

def _init_runtime(args): 

"""Initialize location for runtime data.""" 

180 ↛ 181line 180 didn't jump to line 181, because the condition on line 180 was never true if utils.is_linux: 

typ = QStandardPaths.RuntimeLocation 

else: 

# RuntimeLocation is a weird path on macOS and Windows. 

typ = QStandardPaths.TempLocation 

 

overridden, path = _from_args(typ, args) 

 

188 ↛ 207line 188 didn't jump to line 207, because the condition on line 188 was never false if not overridden: 

try: 

path = _writable_location(typ) 

except EmptyValueError: 

# Fall back to TempLocation when RuntimeLocation is misconfigured 

193 ↛ 195line 193 didn't jump to line 195, because the condition on line 193 was never false if typ == QStandardPaths.TempLocation: 

raise 

path = _writable_location(QStandardPaths.TempLocation) 

 

# This is generic, but per-user. 

# _writable_location makes sure we have a qutebrowser-specific subdir. 

# 

# 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... 

 

_create(path) 

_locations[Location.runtime] = path 

 

 

def runtime(): 

return _locations[Location.runtime] 

 

 

def _writable_location(typ): 

"""Wrapper around QStandardPaths.writableLocation. 

 

Arguments: 

typ: A QStandardPaths::StandardLocation member. 

""" 

typ_str = debug.qenum_key(QStandardPaths, typ) 

 

# Types we are sure we handle correctly below. 

assert typ in [ 

QStandardPaths.ConfigLocation, QStandardPaths.DataLocation, 

QStandardPaths.CacheLocation, QStandardPaths.DownloadLocation, 

QStandardPaths.RuntimeLocation, QStandardPaths.TempLocation, 

# FIXME old Qt 

getattr(QStandardPaths, 'AppDataLocation', object())], typ_str 

 

with _unset_organization(): 

path = QStandardPaths.writableLocation(typ) 

 

log.misc.debug("writable location for {}: {}".format(typ_str, path)) 

if not path: 

raise EmptyValueError("QStandardPaths returned an empty value!") 

 

# Qt seems to use '/' as path separator even on Windows... 

path = path.replace('/', os.sep) 

 

# Add the application name to the given path if needed. 

# This is in order for this to work without a QApplication (and thus 

# QStandardsPaths not knowing the application name), as well as a 

# workaround for https://bugreports.qt.io/browse/QTBUG-38872 

if (typ != QStandardPaths.DownloadLocation and 

path.split(os.sep)[-1] != APPNAME): 

path = os.path.join(path, APPNAME) 

 

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. 

""" 

basedir_suffix = { 

QStandardPaths.ConfigLocation: 'config', 

QStandardPaths.DataLocation: 'data', 

QStandardPaths.CacheLocation: 'cache', 

QStandardPaths.DownloadLocation: 'download', 

QStandardPaths.RuntimeLocation: 'runtime', 

} 

 

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.abspath(os.path.join(basedir, suffix))) 

else: 

return (False, None) 

 

 

def _create(path): 

"""Create the `path` directory. 

 

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. 

""" 

try: 

os.makedirs(path, 0o700) 

except FileExistsError: 

pass 

 

 

def _init_dirs(args=None): 

"""Create and cache standard directory locations. 

 

Mainly in a separate function because we need to call it in tests. 

""" 

_init_config(args) 

_init_data(args) 

_init_cache(args) 

_init_download(args) 

_init_runtime(args) 

 

 

def init(args): 

"""Initialize all standard dirs.""" 

if args is not None: 

# args can be None during tests 

log.init.debug("Base directory: {}".format(args.basedir)) 

 

_init_dirs(args) 

_init_cachedir_tag() 

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

if utils.is_mac: # pragma: no cover 

_move_macos() 

elif utils.is_windows: # pragma: no cover 

_move_windows() 

 

 

def _move_macos(): 

"""Move most config files to new location on macOS.""" 

old_config = config(auto=True) # ~/Library/Preferences/qutebrowser 

new_config = config() # ~/.qutebrowser 

for f in os.listdir(old_config): 

if f not in ['qsettings', 'autoconfig.yml']: 

_move_data(os.path.join(old_config, f), 

os.path.join(new_config, f)) 

 

 

def _move_windows(): 

"""Move the whole qutebrowser directory from Local to Roaming AppData.""" 

# %APPDATA%\Local\qutebrowser 

old_appdata_dir = _writable_location(QStandardPaths.DataLocation) 

# %APPDATA%\Roaming\qutebrowser 

new_appdata_dir = _writable_location(QStandardPaths.AppDataLocation) 

 

# data subfolder 

old_data = os.path.join(old_appdata_dir, 'data') 

new_data = os.path.join(new_appdata_dir, 'data') 

ok = _move_data(old_data, new_data) 

if not ok: # pragma: no cover 

return 

 

# config files 

new_config_dir = os.path.join(new_appdata_dir, 'config') 

_create(new_config_dir) 

for f in os.listdir(old_appdata_dir): 

if f != 'cache': 

_move_data(os.path.join(old_appdata_dir, f), 

os.path.join(new_config_dir, f)) 

 

 

def _init_cachedir_tag(): 

"""Create CACHEDIR.TAG if it doesn't exist. 

 

See http://www.brynosaurus.com/cachedir/spec.html 

""" 

cachedir_tag = os.path.join(cache(), '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/" 

"cachedir/\n") 

except OSError: 

log.init.exception("Failed to create CACHEDIR.TAG") 

 

 

def _move_data(old, new): 

"""Migrate data from an old to a new directory. 

 

If the old directory does not exist, the migration is skipped. 

If the new directory already exists, an error is shown. 

 

Return: True if moving succeeded, False otherwise. 

""" 

if not os.path.exists(old): 

return False 

 

log.init.debug("Migrating data from {} to {}".format(old, new)) 

 

if os.path.exists(new): 

if not os.path.isdir(new) or os.listdir(new): 

message.error("Failed to move data from {} as {} is non-empty!" 

.format(old, new)) 

return False 

os.rmdir(new) 

 

try: 

shutil.move(old, new) 

except OSError as e: 

message.error("Failed to move data from {} to {}: {}".format( 

old, new, e)) 

return False 

 

return True