diff --git a/Readme.md b/Readme.md index 88152cd..da3937f 100644 --- a/Readme.md +++ b/Readme.md @@ -55,6 +55,23 @@ $ ow internal-link ~/Nextcloud/test.md https://cloud.example.com/f/229 ``` +### Delete old calendar events + +Finds and deletes old calendar events. + +> ⚠️ DATA LOSS WARNING! ⚠️ +> +> This is a proof of concept only. +> The code to perform this action is largely naïve and untested. +> It requires complex input with a limited grammar. +> It uses undocumented APIs and employs only minimal error handling and input verification. + +Delete events older than 2 years on a calendar called "personal": + +```bash +ow delete-old-events 'calendar=personal,minimumAge=2y' +``` + ### Lock Lock a file. Requires the [Temporary files lock app](https://apps.nextcloud.com/apps/files_lock). @@ -123,6 +140,7 @@ ow [started](https://help.nextcloud.com/t/get-internal-link-for-a-file-in-nextcl ## Ideas +* add tests (unit tests, integration tests, end-to-end/acceptance tests, etc.) * add more features * post chat messages * add a task diff --git a/ow b/ow index f8e7ff9..1975130 100755 --- a/ow +++ b/ow @@ -18,6 +18,7 @@ import argparse import configparser +from datetime import datetime, timedelta import os import re import requests @@ -31,8 +32,8 @@ nextcloudWebdavRoot = 'remote.php/dav' parser = argparse.ArgumentParser() parser.add_argument('-d', '--debug', help='enable debug messages', action='store_true') -parser.add_argument('action', help='action to perform', choices=['da','dir-album','i','internal-link','l','lock','u','unlock']) -parser.add_argument('target', help='target to operate on. For dir-album, this is a remote path. For internal-link, lock, and unlock, this is a local path.') +parser.add_argument('action', help='action to perform', choices=['da','dir-album','doe','delete-old-events','i','internal-link','l','lock','u','unlock']) +parser.add_argument('target', help='target to operate on. For dir-album, this is a remote path. For delete-old-events, this is a spec such as "calendar=personal,minimumAge=2y" (deletes calendar events on the "personal" calendar older than 2 years). For internal-link, lock, and unlock, this is a local path.') args = parser.parse_args() defaultConfigLocation = os.path.expanduser('~/.config/ow/ow.ini') @@ -287,6 +288,75 @@ def lockOrUnlock(action, fileurl): debug(f'📝 HTTP response code {response.status_code}. Response text: {response.text}') +def buildTimeRange(minimumAge): + startTime = '20100101T000000Z' + y = int(minimumAge.split('y')[0]) + offset = timedelta(days=365 * y) + offsetTime = datetime.utcnow() - offset + endTime = offsetTime.strftime("%Y%m%dT%H%M%SZ") + return (startTime, endTime) + +def findOldEvents(calendar, minimumAge): + global config, _auth, args + debug(f'🔍 search for events in calendar {calendar} older than {minimumAge}...') + + (startTime,endTime) = buildTimeRange(minimumAge) + + _requestBody = f''' + + + + + + + + + + + +''' + + url = '/'.join([config['server']['baseUrl'], nextcloudWebdavRoot, 'calendars', config['server']['username'], calendar]) + + method = 'REPORT' + headers = {'Depth': '1'} + try: + response = requests.request(method, url, auth=_auth, data=_requestBody, headers=headers) + except requests.RequestException as e: + print(f'⛔ {method} request failed: {e}', file=sys.stderr) + sys.exit(1) + + # response status code must be between 200 and 400 to continue + # use overloaded __bool__() to check this + if not response: + print(f'⛔ HTTP response code {response.status_code}. Response text: {response.text}', file=sys.stderr) + sys.exit(1) + + if args.debug: + prettyOutputFilename = captureXmlResponse(response.text) + debug(f'📝 HTTP response code {response.status_code}. Response text saved in: {prettyOutputFilename}') + + eventDavPaths = [] + + root = cET.fromstring(response.text) + for href in root.findall('.//{DAV:}href'): + eventDavPaths.append(href.text) + + return eventDavPaths + +def deleteEvents(calendar, eventDavPaths): + global config, _auth + + method = 'DELETE' + for eventDavPath in eventDavPaths: + debug(f'🏃 delete event at {eventDavPath}...') + url = '/'.join([config['server']['baseUrl'], eventDavPath]) + try: + response = requests.request(method, url, auth=_auth) + except requests.RequestException as e: + print('⛔ {method} request failed: {}'.format(e), file=sys.stderr) + sys.exit(1) + if args.action in ['da','dir-album']: debug('🏃 make album from directory...') ncRelativeAlbumPath = args.target @@ -308,6 +378,18 @@ if args.action in ['da','dir-album']: debug('🖼️ success!') +if args.action in ['doe','delete-old-events']: + debug('🏃 delete old events...') + # parse "old events" spec + (arg1, arg2) = args.target.split(',') + calendar = arg1.split('=')[1] + minimumAge = arg2.split('=')[1] + # find events + eventDavPaths = findOldEvents(calendar, minimumAge) + # delete events + deleteEvents(calendar, eventDavPaths) + debug('🗑️ success!') + if args.action in ['i','internal-link']: debug('🏃 get internal link...') fileId = getFileId(getFilesUrl(normalizeLocalPath(args.target)))