diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index f559fb96a..2bbc9fdb8 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -41,6 +41,12 @@ show it. *-c* 'CONFDIR', *--confdir* 'CONFDIR':: Set config directory (empty for no config storage). +*--datadir* 'DATADIR':: + Set data directory (empty for no data storage). + +*--cachedir* 'CACHEDIR':: + Set cache directory (empty for no cache storage). + *-V*, *--version*:: Show version and quit. diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 26d846074..573939d8b 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -27,7 +27,7 @@ import zipfile from qutebrowser.config import config from qutebrowser.utils import objreg, standarddir, log, message -from qutebrowser.commands import cmdutils +from qutebrowser.commands import cmdutils, cmdexc def guess_zip_filename(zf): @@ -90,12 +90,18 @@ class HostBlocker: self.blocked_hosts = set() self._in_progress = [] self._done_count = 0 - self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts') + data_dir = standarddir.data() + if data_dir is None: + self._hosts_file = None + else: + self._hosts_file = os.path.join(data_dir, 'blocked-hosts') objreg.get('config').changed.connect(self.on_config_changed) def read_hosts(self): """Read hosts from the existing blocked-hosts file.""" self.blocked_hosts = set() + if self._hosts_file is None: + return if os.path.exists(self._hosts_file): try: with open(self._hosts_file, 'r', encoding='utf-8') as f: @@ -111,6 +117,8 @@ class HostBlocker: @cmdutils.register(instance='host-blocker', win_id='win_id') def adblock_update(self, win_id): """Update the adblock block lists.""" + if self._hosts_file is None: + raise cmdexc.CommandError("No data storage is configured!") self.blocked_hosts = set() self._done_count = 0 urls = config.get('content', 'host-block-lists') diff --git a/qutebrowser/browser/cache.py b/qutebrowser/browser/cache.py index 9b7a8de05..03f6c7a2e 100644 --- a/qutebrowser/browser/cache.py +++ b/qutebrowser/browser/cache.py @@ -21,6 +21,7 @@ import os.path +from PyQt5.QtCore import pyqtSlot from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData from qutebrowser.config import config @@ -29,23 +30,41 @@ from qutebrowser.utils import utils, standarddir, objreg class DiskCache(QNetworkDiskCache): - """Disk cache which sets correct cache dir and size.""" + """Disk cache which sets correct cache dir and size. + + Attributes: + _activated: Whether the cache should be used. + """ def __init__(self, parent=None): super().__init__(parent) - self.setCacheDirectory(os.path.join(standarddir.cache(), 'http')) + cache_dir = standarddir.cache() + if config.get('general', 'private-browsing') or cache_dir is None: + self._activated = False + else: + self._activated = True + self.setCacheDirectory(os.path.join(standarddir.cache(), 'http')) self.setMaximumCacheSize(config.get('storage', 'cache-size')) - objreg.get('config').changed.connect(self.cache_size_changed) + objreg.get('config').changed.connect(self.on_config_changed) def __repr__(self): return utils.get_repr(self, size=self.cacheSize(), maxsize=self.maximumCacheSize(), path=self.cacheDirectory()) - @config.change_filter('storage', 'cache-size') - def cache_size_changed(self): - """Update cache size if the config was changed.""" - self.setMaximumCacheSize(config.get('storage', 'cache-size')) + @pyqtSlot() + def on_config_changed(self, section, option): + """Update cache size/activated if the config was changed.""" + if (section, option) == ('storage', 'cache-size'): + self.setMaximumCacheSize(config.get('storage', 'cache-size')) + elif (section, option) == ('general', 'private-browsing'): + if (config.get('general', 'private-browsing') or + standarddir.cache() is None): + self._activated = False + else: + self._activated = True + self.setCacheDirectory( + os.path.join(standarddir.cache(), 'http')) def cacheSize(self): """Return the current size taken up by the cache. @@ -53,10 +72,10 @@ class DiskCache(QNetworkDiskCache): Return: An int. """ - if config.get('general', 'private-browsing'): - return 0 - else: + if self._activated: return super().cacheSize() + else: + return 0 def fileMetaData(self, filename): """Return the QNetworkCacheMetaData for the cache file filename. @@ -67,10 +86,10 @@ class DiskCache(QNetworkDiskCache): Return: A QNetworkCacheMetaData object. """ - if config.get('general', 'private-browsing'): - return QNetworkCacheMetaData() - else: + if self._activated: return super().fileMetaData(filename) + else: + return QNetworkCacheMetaData() def data(self, url): """Return the data associated with url. @@ -81,10 +100,10 @@ class DiskCache(QNetworkDiskCache): return: A QIODevice or None. """ - if config.get('general', 'private-browsing'): - return None - else: + if self._activated: return super().data(url) + else: + return None def insert(self, device): """Insert the data in device and the prepared meta data into the cache. @@ -92,10 +111,10 @@ class DiskCache(QNetworkDiskCache): Args: device: A QIODevice. """ - if config.get('general', 'private-browsing'): - return - else: + if self._activated: super().insert(device) + else: + return None def metaData(self, url): """Return the meta data for the url url. @@ -106,10 +125,10 @@ class DiskCache(QNetworkDiskCache): Return: A QNetworkCacheMetaData object. """ - if config.get('general', 'private-browsing'): - return QNetworkCacheMetaData() - else: + if self._activated: return super().metaData(url) + else: + return QNetworkCacheMetaData() def prepare(self, meta_data): """Return the device that should be populated with the data. @@ -120,10 +139,10 @@ class DiskCache(QNetworkDiskCache): Return: A QIODevice or None. """ - if config.get('general', 'private-browsing'): - return None - else: + if self._activated: return super().prepare(meta_data) + else: + return None def remove(self, url): """Remove the cache entry for url. @@ -131,10 +150,10 @@ class DiskCache(QNetworkDiskCache): Return: True on success, False otherwise. """ - if config.get('general', 'private-browsing'): - return False - else: + if self._activated: return super().remove(url) + else: + return False def updateMetaData(self, meta_data): """Update the cache meta date for the meta_data's url to meta_data. @@ -142,14 +161,14 @@ class DiskCache(QNetworkDiskCache): Args: meta_data: A QNetworkCacheMetaData object. """ - if config.get('general', 'private-browsing'): - return - else: + if self._activated: super().updateMetaData(meta_data) + else: + return def clear(self): """Remove all items from the cache.""" - if config.get('general', 'private-browsing'): - return - else: + if self._activated: super().clear() + else: + return diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index b3370d0c1..2ffbf70c1 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -83,6 +83,28 @@ class WebHistory(QWebHistoryInterface): self._lineparser = lineparser.AppendLineParser( standarddir.data(), 'history', parent=self) self._history_dict = collections.OrderedDict() + self._read_history() + self._new_history = [] + self._saved_count = 0 + objreg.get('save-manager').add_saveable( + 'history', self.save, self.item_added) + + def __repr__(self): + return utils.get_repr(self, length=len(self)) + + def __getitem__(self, key): + return self._new_history[key] + + def __iter__(self): + return iter(self._history_dict.values()) + + def __len__(self): + return len(self._history_dict) + + def _read_history(self): + """Read the initial history.""" + if standarddir.data() is None: + return with self._lineparser.open(): for line in self._lineparser: data = line.rstrip().split(maxsplit=1) @@ -108,22 +130,6 @@ class WebHistory(QWebHistoryInterface): # list of atimes. self._history_dict[url] = HistoryEntry(atime, url) self._history_dict.move_to_end(url) - self._new_history = [] - self._saved_count = 0 - objreg.get('save-manager').add_saveable( - 'history', self.save, self.item_added) - - def __repr__(self): - return utils.get_repr(self, length=len(self)) - - def __getitem__(self, key): - return self._new_history[key] - - def __iter__(self): - return iter(self._history_dict.values()) - - def __len__(self): - return len(self._history_dict) def get_recent(self): """Get the most recent history entries.""" diff --git a/qutebrowser/config/parsers/ini.py b/qutebrowser/config/parsers/ini.py index e8d24d249..a430ff454 100644 --- a/qutebrowser/config/parsers/ini.py +++ b/qutebrowser/config/parsers/ini.py @@ -47,11 +47,15 @@ class ReadConfigParser(configparser.ConfigParser): self.optionxform = lambda opt: opt # be case-insensitive self._configdir = configdir self._fname = fname + if self._configdir is None: + self._configfile = None + return self._configfile = os.path.join(self._configdir, fname) if not os.path.isfile(self._configfile): return log.init.debug("Reading config from {}".format(self._configfile)) - self.read(self._configfile, encoding='utf-8') + if self._configfile is not None: + self.read(self._configfile, encoding='utf-8') def __repr__(self): return utils.get_repr(self, constructor=True, @@ -64,6 +68,8 @@ class ReadWriteConfigParser(ReadConfigParser): def save(self): """Save the config file.""" + if self._configdir is None: + return if not os.path.exists(self._configdir): os.makedirs(self._configdir, 0o755) log.destroy.debug("Saving config to {}".format(self._configfile)) diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index b89fbd3c5..117abbb26 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -377,16 +377,20 @@ MAPPINGS = { def init(): """Initialize the global QWebSettings.""" - if config.get('general', 'private-browsing'): + cache_path = standarddir.cache() + data_path = standarddir.data() + if config.get('general', 'private-browsing') or cache_path is None: QWebSettings.setIconDatabasePath('') else: - QWebSettings.setIconDatabasePath(standarddir.cache()) - QWebSettings.setOfflineWebApplicationCachePath( - os.path.join(standarddir.cache(), 'application-cache')) - QWebSettings.globalSettings().setLocalStoragePath( - os.path.join(standarddir.data(), 'local-storage')) - QWebSettings.setOfflineStoragePath( - os.path.join(standarddir.data(), 'offline-storage')) + QWebSettings.setIconDatabasePath(cache_path) + if cache_path is not None: + QWebSettings.setOfflineWebApplicationCachePath( + os.path.join(cache_path, 'application-cache')) + if data_path is not None: + QWebSettings.globalSettings().setLocalStoragePath( + os.path.join(data_path, 'local-storage')) + QWebSettings.setOfflineStoragePath( + os.path.join(data_path, 'offline-storage')) for sectname, section in MAPPINGS.items(): for optname, mapping in section.items(): @@ -402,11 +406,12 @@ def init(): def update_settings(section, option): """Update global settings when qwebsettings changed.""" + cache_path = standarddir.cache() if (section, option) == ('general', 'private-browsing'): - if config.get('general', 'private-browsing'): + if config.get('general', 'private-browsing') or cache_path is None: QWebSettings.setIconDatabasePath('') else: - QWebSettings.setIconDatabasePath(standarddir.cache()) + QWebSettings.setIconDatabasePath(cache_path) else: try: mapping = MAPPINGS[section][option] diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 76e14edd9..ca5350c9e 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -63,7 +63,10 @@ class CrashHandler(QObject): def handle_segfault(self): """Handle a segfault from a previous run.""" - logname = os.path.join(standarddir.data(), 'crash.log') + data_dir = None + if data_dir is None: + return + logname = os.path.join(data_dir, 'crash.log') try: # First check if an old logfile exists. if os.path.exists(logname): @@ -118,7 +121,10 @@ class CrashHandler(QObject): def _init_crashlogfile(self): """Start a new logfile and redirect faulthandler to it.""" - logname = os.path.join(standarddir.data(), 'crash.log') + data_dir = standarddir.data() + if data_dir is None: + return + logname = os.path.join(data_dir, 'crash.log') try: self._crash_log_file = open(logname, 'w', encoding='ascii') except OSError: diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 5cd6304f5..c34760217 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -35,7 +35,7 @@ class BaseLineParser(QObject): """A LineParser without any real data. Attributes: - _configdir: The directory to read the config from. + _configdir: Directory to read the config from, or None. _configfile: The config file path. _fname: Filename of the config. _binary: Whether to open the file in binary mode. @@ -57,7 +57,10 @@ class BaseLineParser(QObject): """ super().__init__(parent) self._configdir = configdir - self._configfile = os.path.join(self._configdir, fname) + if self._configdir is None: + self._configfile = None + else: + self._configfile = os.path.join(self._configdir, fname) self._fname = fname self._binary = binary self._opened = False @@ -68,10 +71,17 @@ class BaseLineParser(QObject): binary=self._binary) def _prepare_save(self): - """Prepare saving of the file.""" + """Prepare saving of the file. + + Return: + True if the file should be saved, False otherwise. + """ + if self._configdir is None: + return False log.destroy.debug("Saving to {}".format(self._configfile)) if not os.path.exists(self._configdir): os.makedirs(self._configdir, 0o755) + return True @contextlib.contextmanager def _open(self, mode): @@ -80,6 +90,7 @@ class BaseLineParser(QObject): Args: mode: The mode to use ('a'/'r'/'w') """ + assert self._configfile is not None if self._opened: raise IOError("Refusing to double-open AppendLineParser.") self._opened = True @@ -159,7 +170,9 @@ class AppendLineParser(BaseLineParser): return data def save(self): - self._prepare_save() + do_save = self._prepare_save() + if not do_save: + return with self._open('a') as f: self._write(f, self.new_data) self.new_data = [] @@ -182,7 +195,7 @@ class LineParser(BaseLineParser): binary: Whether to open the file in binary mode. """ super().__init__(configdir, fname, binary=binary, parent=parent) - if not os.path.isfile(self._configfile): + if configdir is None or not os.path.isfile(self._configfile): self.data = [] else: log.init.debug("Reading {}".format(self._configfile)) @@ -206,8 +219,11 @@ class LineParser(BaseLineParser): """Save the config file.""" if self._opened: raise IOError("Refusing to double-open AppendLineParser.") + do_save = self._prepare_save() + if not do_save: + return self._opened = True - self._prepare_save() + assert self._configfile is not None with qtutils.savefile_open(self._configfile, self._binary) as f: self._write(f, self.data) self._opened = False @@ -226,14 +242,14 @@ class LimitLineParser(LineParser): """Constructor. Args: - configdir: Directory to read the config from. + configdir: Directory to read the config from, or None. fname: Filename of the config file. limit: Config tuple (section, option) which contains a limit. binary: Whether to open the file in binary mode. """ super().__init__(configdir, fname, binary=binary, parent=parent) self._limit = limit - if limit is not None: + if limit is not None and configdir is not None: objreg.get('config').changed.connect(self.cleanup_file) def __repr__(self): @@ -244,6 +260,7 @@ class LimitLineParser(LineParser): @pyqtSlot(str, str) def cleanup_file(self, section, option): """Delete the file if the limit was changed to 0.""" + assert self._configfile is not None if (section, option) != self._limit: return value = config.get(section, option) @@ -256,6 +273,9 @@ class LimitLineParser(LineParser): limit = config.get(*self._limit) if limit == 0: return - self._prepare_save() + do_save = self._prepare_save() + if not do_save: + return + assert self._configfile is not None with qtutils.savefile_open(self._configfile, self._binary) as f: self._write(f, self.data[-limit:]) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 2d9cc222d..bd75c634b 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -82,10 +82,14 @@ class SessionManager(QObject): def __init__(self, parent=None): super().__init__(parent) self._current = None - self._base_path = os.path.join(standarddir.data(), 'sessions') + data_dir = standarddir.data() + if data_dir is None: + self._base_path = None + else: + self._base_path = os.path.join(standarddir.data(), 'sessions') self._last_window_session = None self.did_load = False - if not os.path.exists(self._base_path): + if self._base_path is not None and not os.path.exists(self._base_path): os.mkdir(self._base_path) def _get_session_path(self, name, check_exists=False): @@ -100,6 +104,11 @@ class SessionManager(QObject): if os.path.isabs(path) and ((not check_exists) or os.path.exists(path)): return path + elif self._base_path is None: + if check_exists: + raise SessionNotFoundError(name) + else: + return None else: path = os.path.join(self._base_path, name + '.yml') if check_exists and not os.path.exists(path): @@ -194,6 +203,8 @@ class SessionManager(QObject): else: name = 'default' path = self._get_session_path(name) + if path is None: + raise SessionError("No data storage configured.") log.sessions.debug("Saving session {} to {}...".format(name, path)) if last_window: @@ -289,6 +300,8 @@ class SessionManager(QObject): def list_sessions(self): """Get a list of all session names.""" sessions = [] + if self._base_path is None: + return sessions for filename in os.listdir(self._base_path): base, ext = os.path.splitext(filename) if ext == '.yml': diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 51b82604c..1ae9f60e2 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -48,6 +48,10 @@ def get_argparser(): description=qutebrowser.__description__) parser.add_argument('-c', '--confdir', help="Set config directory (empty " "for no config storage).") + parser.add_argument('--datadir', help="Set data directory (empty for " + "no data storage).") + parser.add_argument('--cachedir', help="Set cache directory (empty for " + "no cache storage).") parser.add_argument('-V', '--version', help="Show version and quit.", action='store_true') parser.add_argument('-s', '--set', help="Set a temporary setting for " diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index f2a765513..aeba60b77 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -80,7 +80,9 @@ def _from_args(typ, args): path: The overridden path, or None to turn off storage. """ typ_to_argparse_arg = { - QStandardPaths.ConfigLocation: 'confdir' + QStandardPaths.ConfigLocation: 'confdir', + QStandardPaths.DataLocation: 'datadir', + QStandardPaths.CacheLocation: 'cachedir', } if args is None: return (False, None) @@ -135,8 +137,18 @@ def init(args): """Initialize all standard dirs.""" global _args _args = args - # http://www.brynosaurus.com/cachedir/spec.html - cachedir_tag = os.path.join(cache(), 'CACHEDIR.TAG') + _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: diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 636d89c9b..123b2a412 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -280,7 +280,7 @@ class TestConfigInit: def test_config_none(self, monkeypatch): """Test initializing with config path set to None.""" - args = types.SimpleNamespace(confdir='') + args = types.SimpleNamespace(confdir='', datadir='', cachedir='') for k, v in self.env.items(): monkeypatch.setenv(k, v) standarddir.init(args) diff --git a/tests/misc/test_lineparser.py b/tests/misc/test_lineparser.py index c473eda50..7dfb364e8 100644 --- a/tests/misc/test_lineparser.py +++ b/tests/misc/test_lineparser.py @@ -69,6 +69,7 @@ class LineParserWrapper: def _prepare_save(self): """Keep track if _prepare_save has been called.""" self._test_save_prepared = True + return True class TestableAppendLineParser(LineParserWrapper, lineparser.AppendLineParser): diff --git a/tests/utils/test_standarddir.py b/tests/utils/test_standarddir.py index e98ab80f9..4e1519ded 100644 --- a/tests/utils/test_standarddir.py +++ b/tests/utils/test_standarddir.py @@ -22,6 +22,7 @@ import os import os.path import sys +import types from PyQt5.QtWidgets import QApplication import pytest @@ -114,3 +115,29 @@ class TestGetStandardDirWindows: """Test cache dir.""" expected = ['qutebrowser_test', 'cache'] assert standarddir.cache().split(os.sep)[-2:] == expected + + +class TestArguments: + + """Tests with confdir/cachedir/datadir arguments.""" + + @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) + def test_confdir(self, arg, expected): + """Test --confdir.""" + args = types.SimpleNamespace(confdir=arg, cachedir=None, datadir=None) + standarddir.init(args) + assert standarddir.config() == expected + + @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) + def test_confdir(self, arg, expected): + """Test --cachedir.""" + args = types.SimpleNamespace(confdir=None, cachedir=arg, datadir=None) + standarddir.init(args) + assert standarddir.cache() == expected + + @pytest.mark.parametrize('arg, expected', [('', None), ('foo', 'foo')]) + def test_datadir(self, arg, expected): + """Test --datadir.""" + args = types.SimpleNamespace(confdir=None, cachedir=None, datadir=arg) + standarddir.init(args) + assert standarddir.data() == expected