Skip to content

Commit 2e7e31f

Browse files
committed
storage/http: add support for filter_hook
This allows users to process fetched items through a filter command, to fix malformed webcal items as they are imported. In my case, my provider adds the export time to the description and random sequence numbers to all events. This caused the whole collection to be invalidated and propagated at each sync. I use the filter to remove those, canonicalising the items.
1 parent 616d7aa commit 2e7e31f

File tree

3 files changed

+33
-2
lines changed

3 files changed

+33
-2
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Version 0.19.3
1818
- Require matching ``BEGIN`` and ``END`` lines in vobjects. :gh:`1103`
1919
- A Docker environment for Vdirsyncer has been added `Vdirsyncer DOCKERIZED <https://github.com/Bleala/Vdirsyncer-DOCKERIZED>`_.
2020
- Implement digest auth. :gh:`1137`
21+
- Add ``filter_hook`` parameter to :storage:`http`. :gh:`1136`
2122

2223
Version 0.19.2
2324
==============

docs/config.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ leads to an error.
484484
[storage holidays_remote]
485485
type = "http"
486486
url = https://example.com/holidays_from_hicksville.ics
487+
#filter_hook = null
487488

488489
Too many WebCAL providers generate UIDs of all ``VEVENT``-components
489490
on-the-fly, i.e. all UIDs change every time the calendar is downloaded.
@@ -508,3 +509,8 @@ leads to an error.
508509
:param auth_cert: Optional. Either a path to a certificate with a client
509510
certificate and the key or a list of paths to the files with them.
510511
:param useragent: Default ``vdirsyncer``.
512+
:param filter_hook: Optional. A filter command to call for each fetched
513+
item, passed in raw form to stdin and returned via stdout.
514+
If nothing is returned by the filter command, the item is skipped.
515+
This can be used to alter fields as needed when dealing with providers
516+
generating malformed events.

vdirsyncer/storage/http.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import logging
4+
import subprocess
35
import urllib.parse as urlparse
46

57
import aiohttp
@@ -14,6 +16,8 @@
1416
from ..vobject import split_collection
1517
from .base import Storage
1618

19+
logger = logging.getLogger(__name__)
20+
1721

1822
class HttpStorage(Storage):
1923
storage_name = "http"
@@ -34,6 +38,7 @@ def __init__(
3438
useragent=USERAGENT,
3539
verify_fingerprint=None,
3640
auth_cert=None,
41+
filter_hook=None,
3742
*,
3843
connector,
3944
**kwargs,
@@ -56,6 +61,7 @@ def __init__(
5661
self.useragent = useragent
5762
assert connector is not None
5863
self.connector = connector
64+
self._filter_hook = filter_hook
5965

6066
collection = kwargs.get("collection")
6167
if collection is not None:
@@ -66,6 +72,19 @@ def __init__(
6672
def _default_headers(self):
6773
return {"User-Agent": self.useragent}
6874

75+
def _run_filter_hook(self, raw_item):
76+
try:
77+
result = subprocess.run(
78+
[self._filter_hook],
79+
input=raw_item,
80+
capture_output=True,
81+
encoding="utf-8",
82+
)
83+
return result.stdout
84+
except OSError as e:
85+
logger.warning(f"Error executing external command: {str(e)}")
86+
return raw_item
87+
6988
async def list(self):
7089
async with aiohttp.ClientSession(
7190
connector=self.connector,
@@ -82,8 +101,13 @@ async def list(self):
82101
)
83102
self._items = {}
84103

85-
for item in split_collection((await r.read()).decode("utf-8")):
86-
item = Item(item)
104+
for raw_item in split_collection((await r.read()).decode("utf-8")):
105+
if self._filter_hook:
106+
raw_item = self._run_filter_hook(raw_item)
107+
if not raw_item:
108+
continue
109+
110+
item = Item(raw_item)
87111
if self._ignore_uids:
88112
item = item.with_uid(item.hash)
89113

0 commit comments

Comments
 (0)