From 37a1eb2fdb8c7d92a083fb9885f43f6d38452b0a Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 2 Jul 2017 18:42:55 +0200 Subject: [PATCH] Remove remoteStorage (#648) Fix #647 --- CHANGELOG.rst | 5 + Makefile | 4 +- docs/config.rst | 18 -- scripts/make_travisconf.py | 14 +- setup.py | 1 - tests/storage/test_remotestorage.py | 35 ---- vdirsyncer/cli/utils.py | 4 - vdirsyncer/storage/remotestorage.py | 266 ---------------------------- 8 files changed, 10 insertions(+), 337 deletions(-) delete mode 100644 tests/storage/test_remotestorage.py delete mode 100644 vdirsyncer/storage/remotestorage.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 29de82c..5950ed0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,11 @@ Package maintainers and users who have to manually update their installation may want to subscribe to `GitHub's tag feed `_. +Version 0.16.1 +============== + +- Removed remoteStorage support, see :gh:`647`. + Version 0.16.0 ============== diff --git a/Makefile b/Makefile index e848b1c..6199cac 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ # See the documentation on how to run the tests. export DAV_SERVER := skip -export REMOTESTORAGE_SERVER := skip export REQUIREMENTS := release export TESTSERVER_BASE := ./tests/storage/servers/ export CI := false @@ -42,7 +41,7 @@ all: install-servers: set -ex; \ - for server in $(DAV_SERVER) $(REMOTESTORAGE_SERVER); do \ + for server in $(DAV_SERVER); do \ if [ ! "$$(ls $(TESTSERVER_BASE)$$server/)" ]; then \ git submodule update --init -- "$(TESTSERVER_BASE)$$server"; \ fi; \ @@ -90,7 +89,6 @@ release: install-dev: pip install -e . - [ "$(REMOTESTORAGE_SERVER)" = "skip" ] || pip install -e .[remotestorage] [ "$(ETESYNC_TESTS)" = "false" ] || pip install -e .[etesync] set -xe && if [ "$(REQUIREMENTS)" = "devel" ]; then \ pip install -U --force-reinstall \ diff --git a/docs/config.rst b/docs/config.rst index 3cc3bfd..00b41f1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -208,24 +208,6 @@ or write anything to it. .. autostorage:: vdirsyncer.storage.google.GoogleContactsStorage -remoteStorage -+++++++++++++ - -`remoteStorage `_ is an open per-user data storage -protocol. Vdirsyncer contains **highly experimental support** for it. - -.. note:: - - Do not use this storage if you're not prepared for data-loss and breakage. - -To use them, you need to install some optional dependencies with:: - - pip install vdirsyncer[remotestorage] - -.. autostorage:: vdirsyncer.storage.remotestorage.RemoteStorageContacts - -.. autostorage:: vdirsyncer.storage.remotestorage.RemoteStorageCalendars - EteSync +++++++ diff --git a/scripts/make_travisconf.py b/scripts/make_travisconf.py index 6834ee3..49f1b0f 100644 --- a/scripts/make_travisconf.py +++ b/scripts/make_travisconf.py @@ -51,25 +51,19 @@ for python, requirements in itertools.product(python_versions, else: dav_servers = ("radicale", "xandikos") - rs_servers = () if python == latest_python and requirements == "release": dav_servers += ("owncloud", "nextcloud", "baikal", "davical", "fastmail") - for server_type, server in itertools.chain( - (("REMOTESTORAGE", x) for x in rs_servers), - (("DAV", x) for x in dav_servers) - ): - - build_prs = server not in ("fastmail", "davical", "icloud") + for dav_server in dav_servers: + build_prs = dav_server not in ("fastmail", "davical", "icloud") matrix.append({ 'python': python, 'env': ("BUILD=test " - "{server_type}_SERVER={server} " + "DAV_SERVER={dav_server} " "REQUIREMENTS={requirements} " "BUILD_PRS={build_prs} " - .format(server_type=server_type, - server=server, + .format(dav_server=dav_server, requirements=requirements, build_prs=build_prs and "true" or "false")) }) diff --git a/setup.py b/setup.py index d0ff448..3002fef 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,6 @@ setup( # Optional dependencies extras_require={ - 'remotestorage': ['requests-oauthlib'], 'google': ['requests-oauthlib'], 'etesync': ['etesync'] }, diff --git a/tests/storage/test_remotestorage.py b/tests/storage/test_remotestorage.py deleted file mode 100644 index f14bdd0..0000000 --- a/tests/storage/test_remotestorage.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- - - -import os - -import pytest - -from vdirsyncer.storage.remotestorage import \ - RemoteStorageCalendars, RemoteStorageContacts - -from . import StorageTests, get_server_mixin - -remotestorage_server = os.environ['REMOTESTORAGE_SERVER'] -ServerMixin = get_server_mixin(remotestorage_server) - - -class RemoteStorageTests(ServerMixin, StorageTests): - remotestorage_server = remotestorage_server - - -class TestCalendars(RemoteStorageTests): - storage_class = RemoteStorageCalendars - - @pytest.fixture(params=['VTODO', 'VEVENT']) - def item_type(self, request): - return request.param - - -class TestContacts(RemoteStorageTests): - storage_class = RemoteStorageContacts - supports_collections = False - - @pytest.fixture(params=['VCARD']) - def item_type(self, request): - return request.param diff --git a/vdirsyncer/cli/utils.py b/vdirsyncer/cli/utils.py index 30f1a83..381e18f 100644 --- a/vdirsyncer/cli/utils.py +++ b/vdirsyncer/cli/utils.py @@ -34,10 +34,6 @@ class _StorageIndex(object): filesystem='vdirsyncer.storage.filesystem.FilesystemStorage', http='vdirsyncer.storage.http.HttpStorage', singlefile='vdirsyncer.storage.singlefile.SingleFileStorage', - remotestorage_contacts=( - 'vdirsyncer.storage.remotestorage.RemoteStorageContacts'), - remotestorage_calendars=( - 'vdirsyncer.storage.remotestorage.RemoteStorageCalendars'), google_calendar='vdirsyncer.storage.google.GoogleCalendarStorage', google_contacts='vdirsyncer.storage.google.GoogleContactsStorage', etesync_calendars='vdirsyncer.storage.etesync.EtesyncCalendars', diff --git a/vdirsyncer/storage/remotestorage.py b/vdirsyncer/storage/remotestorage.py deleted file mode 100644 index 2cd5e77..0000000 --- a/vdirsyncer/storage/remotestorage.py +++ /dev/null @@ -1,266 +0,0 @@ -''' -A storage type for accessing contact and calendar data from `remoteStorage -`_. It is highly experimental. - -A few things are hardcoded for now so the user doesn't have to specify those -things, and plugging in an account "just works". -''' - -import logging -from urllib.parse import quote as urlquote, urljoin - -import click - -from .base import Storage, normalize_meta_value -from .http import HTTP_STORAGE_PARAMETERS, prepare_client_cert, \ - prepare_verify -from .. import exceptions, utils, http -from ..vobject import Item - -REDIRECT_URI = 'https://vdirsyncer.5apps.com/' -CLIENT_ID = 'https://vdirsyncer.5apps.com' -DRAFT_VERSION = '05' - -logger = logging.getLogger(__name__) - - -def _ensure_slash(dir): - return dir.rstrip('/') + '/' - - -def _iter_listing(json): - new_listing = '@context' in json # draft-02 and beyond - if new_listing: - json = json['items'] - for name, info in json.items(): - if not new_listing: - info = {'ETag': info} - yield name, info - - -class Session(object): - - def __init__(self, account, scope, verify=True, verify_fingerprint=None, - auth_cert=None, access_token=None, collection=None): - from oauthlib.oauth2 import MobileApplicationClient - from requests_oauthlib import OAuth2Session - - self.user, self.host = account.split('@') - - self._settings = { - 'cert': prepare_client_cert(auth_cert) - } - self._settings.update(prepare_verify(verify, verify_fingerprint)) - - self.scope = scope + ':rw' - self._session = OAuth2Session( - CLIENT_ID, client=MobileApplicationClient(CLIENT_ID), - scope=self.scope, - redirect_uri=REDIRECT_URI, - token={'access_token': access_token}, - ) - - subpath = scope - if collection: - subpath = urljoin(_ensure_slash(scope), - _ensure_slash(urlquote(collection))) - - self._discover_endpoints(subpath) - - if not access_token: - self._get_access_token() - - def request(self, method, path, **kwargs): - url = self.endpoints['storage'] - if path: - url = urljoin(url, path) - - settings = dict(self._settings) - settings.update(kwargs) - - return http.request(method, url, session=self._session, **settings) - - def _get_access_token(self): - authorization_url, state = \ - self._session.authorization_url(self.endpoints['oauth']) - - click.echo('Opening {} ...'.format(authorization_url)) - try: - utils.open_graphical_browser(authorization_url) - except Exception as e: - logger.warning(str(e)) - - click.echo('Follow the instructions on the page.') - raise exceptions.UserError('Aborted!') - - def _discover_endpoints(self, subpath): - r = http.request( - 'GET', 'https://{host}/.well-known/webfinger?resource=acct:{user}' - .format(host=self.host, user=self.user), - **self._settings - ) - j = r.json() - for link in j['links']: - if 'remotestorage' in link['rel']: - break - - storage = urljoin(_ensure_slash(link['href']), - _ensure_slash(subpath)) - props = link['properties'] - oauth = props['http://tools.ietf.org/html/rfc6749#section-4.2'] - self.endpoints = dict(storage=storage, oauth=oauth) - - -class RemoteStorage(Storage): - __doc__ = ''' - :param account: remoteStorage account, ``"user@example.com"``. - ''' + HTTP_STORAGE_PARAMETERS + ''' - ''' - - storage_name = None - item_mimetype = None - fileext = None - - def __init__(self, account, verify=True, verify_fingerprint=None, - auth_cert=None, access_token=None, **kwargs): - super(RemoteStorage, self).__init__(**kwargs) - self.session = Session( - account=account, - verify=verify, - verify_fingerprint=verify_fingerprint, - auth_cert=auth_cert, - access_token=access_token, - collection=self.collection, - scope=self.scope) - - @classmethod - def discover(cls, **base_args): - if base_args.pop('collection', None) is not None: - raise TypeError('collection argument must not be given.') - - session_args, _ = utils.split_dict(base_args, lambda key: key in ( - 'account', 'verify', 'auth', 'verify_fingerprint', 'auth_cert', - 'access_token' - )) - - session = Session(scope=cls.scope, **session_args) - - try: - r = session.request('GET', '') - except exceptions.NotFoundError: - return - - for name, _info in _iter_listing(r.json()): - if not name.endswith('/'): - continue # not a folder - - newargs = dict(base_args) - newargs['collection'] = name.rstrip('/') - yield newargs - - @classmethod - def create_collection(cls, collection, **kwargs): - # remoteStorage folders are autocreated. - assert collection - assert '/' not in collection - kwargs['collection'] = collection - return kwargs - - def list(self): - try: - r = self.session.request('GET', '') - except exceptions.NotFoundError: - return - - for name, info in _iter_listing(r.json()): - if not name.endswith(self.fileext): - continue - - etag = info['ETag'] - etag = '"' + etag + '"' - yield name, etag - - def _put(self, href, item, etag): - headers = {'Content-Type': self.item_mimetype + '; charset=UTF-8'} - if etag is None: - headers['If-None-Match'] = '*' - else: - headers['If-Match'] = etag - - response = self.session.request( - 'PUT', - href, - data=item.raw.encode('utf-8'), - headers=headers - ) - return href, response.headers['etag'] - - def update(self, href, item, etag): - assert etag - href, etag = self._put(href, item, etag) - return etag - - def upload(self, item): - href = utils.generate_href(item.ident) + self.fileext - return self._put(href, item, None) - - def delete(self, href, etag): - headers = {'If-Match': etag} - self.session.request('DELETE', href, headers=headers) - - def get(self, href): - response = self.session.request('GET', href) - return Item(response.text), response.headers['etag'] - - def get_meta(self, key): - try: - return normalize_meta_value(self.session.request('GET', key).text) - except exceptions.NotFoundError: - return u'' - - def set_meta(self, key, value): - self.session.request( - 'PUT', - key, - data=normalize_meta_value(value).encode('utf-8'), - headers={'Content-Type': 'text/plain; charset=utf-8'} - ) - - -class RemoteStorageContacts(RemoteStorage): - __doc__ = ''' - remoteStorage contacts. Uses the `vdir_contacts` scope. - ''' + RemoteStorage.__doc__ - - storage_name = 'remotestorage_contacts' - fileext = '.vcf' - item_mimetype = 'text/vcard' - scope = 'vdir_contacts' - - def __init__(self, **kwargs): - if kwargs.get('collection'): - raise ValueError( - 'No collections allowed for contacts, ' - 'there is only one addressbook. ' - 'Use the vcard groups construct to categorize your contacts ' - 'into groups.' - ) - - super(RemoteStorageContacts, self).__init__(**kwargs) - - -class RemoteStorageCalendars(RemoteStorage): - __doc__ = ''' - remoteStorage calendars. Uses the `vdir_calendars` scope. - ''' + RemoteStorage.__doc__ - - storage_name = 'remotestorage_calendars' - fileext = '.ics' - item_mimetype = 'text/calendar' - scope = 'vdir_calendars' - - def __init__(self, **kwargs): - if not kwargs.get('collection'): - raise exceptions.CollectionRequired() - - super(RemoteStorageCalendars, self).__init__(**kwargs)