From 86e03c0d4e8b0a2411489770e7475dcd8b0d3c48 Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 25 Nov 2024 16:35:16 +0100 Subject: [PATCH 1/9] make docker image build work - Depend on a python version Debian actually ships with in pyproject.toml and update the poetry.lock accordingly. - Fix poetry bundle complaining about no python by installing python-is-python3. Also remove the python flag for poetry bundle then. --- Dockerfile | 4 ++-- poetry.lock | 6 +++--- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4c53c6c..4bf6c59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ FROM docker.io/library/debian:12-slim AS builder RUN apt-get update && \ - apt-get install --no-install-suggests --no-install-recommends --yes pipx + apt-get install --no-install-suggests --no-install-recommends --yes pipx python-is-python3 ENV PATH="/root/.local/bin:${PATH}" RUN pipx install poetry RUN pipx inject poetry poetry-plugin-bundle WORKDIR /src COPY . . -RUN poetry bundle venv --python=/usr/bin/python3 --only=main /venv +RUN poetry bundle venv --only=main /venv FROM gcr.io/distroless/python3-debian12 COPY --from=builder /venv /venv diff --git a/poetry.lock b/poetry.lock index 480a7b7..f882a16 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "humanize" @@ -26,5 +26,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.12" -content-hash = "0b022231d09549e564a7cec69a9169cbd5a27e550b5a10a8b57ef2bbe251b6fe" +python-versions = "^3.11" +content-hash = "bc794458508ceedfc18e9635ad18b7113d5118c414698ebc12762c782ed3670d" diff --git a/pyproject.toml b/pyproject.toml index 6e8fa3b..af2182b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Stefan Bethke "] readme = "README.md" [tool.poetry.dependencies] -python = "^3.12" +python = "^3.11" pgsql = "^2.2" humanize = "^4.9.0" -- 2.47.0 From 62cd667db5ce9e24aee697862b6b1dab6ff14589 Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 25 Nov 2024 19:02:19 +0100 Subject: [PATCH 2/9] add option for excluding notes or revisions from the action This is useful, if one just wants to either expire notes or revisions. --- hedgedoc-expire.py | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/hedgedoc-expire.py b/hedgedoc-expire.py index fbcc805..e4fd8a0 100644 --- a/hedgedoc-expire.py +++ b/hedgedoc-expire.py @@ -26,6 +26,7 @@ class Config: self.verbose = False self.revision_age = timedelta(days=14) self.note_age = timedelta(days=95) + self.exclude = [] self.postgres_hostname = getenv('POSTGRES_HOSTNAME', 'localhost') self.postgres_username = getenv('POSTGRES_USERNAME', 'hedgedoc') @@ -262,13 +263,30 @@ class HedgedocExpire: def cmd_check(self) -> None: with pgsql.Connection((self.config.postgres_hostname, self.config.postgres_port), self.config.postgres_username, self.config.postgres_password) as db: - print(self.check_revisions_to_be_expired(db) + - self.check_notes_to_be_expired(db)) + if 'revision' not in self.config.exclude: + print(self.check_revisions_to_be_expired(db)) + elif self.config.verbose: + print("Revisions were excluded from check, not checking.\n") + + if 'note' not in self.config.exclude: + print(self.check_notes_to_be_expired(db)) + elif self.config.verbose: + print("Notes were excluded from check, not checking.\n") def cmd_emailcheck(self) -> None: with pgsql.Connection((self.config.postgres_hostname, self.config.postgres_port), self.config.postgres_username, self.config.postgres_password) as db: - report = self.check_revisions_to_be_expired(db) + self.check_notes_to_be_expired(db) + report = '' + + if 'revision' not in self.config.exclude: + report += self.check_revisions_to_be_expired(db) + else: + report += "Revisions were excluded from check.\n" + + if 'note' not in self.config.exclude: + report += self.check_notes_to_be_expired(db) + else: + report += "Notes were excluded from check.\n" msg = MIMEMultipart() msg['From'] = self.email_sender.mail_from msg['To'] = self.email_sender.mail_from @@ -285,8 +303,15 @@ class HedgedocExpire: def cmd_expire(self) -> None: with pgsql.Connection((self.config.postgres_hostname, self.config.postgres_port), self.config.postgres_username, self.config.postgres_password) as db: - self.expire_old_revisions(db) - self.expire_old_notes(db) + if 'revision' not in self.config.exclude: + self.expire_old_revisions(db) + elif self.config.verbose: + print("Revisions were excluded from action, not expiring.\n") + + if 'note' not in self.config.exclude: + self.expire_old_notes(db) + elif self.config.verbose: + print("Notes were excluded from action, not expiring.\n") def main(): @@ -317,12 +342,17 @@ def main(): help='action to perform') parser.add_argument('-v', '--verbose', action='store_true', default=False, help='print more info while running') + parser.add_argument('--exclude', nargs="+", choices=['revision', 'note'], + help='Type ("revision" or "note") to exclude from the action.') args = parser.parse_args() config = Config() config.note_age = timedelta(days=args.notes) config.revision_age = timedelta(days=args.revisions) config.verbose = args.verbose + if (args.exclude): + config.exclude = args.exclude + print(config.exclude) mail = EmailSender(config.smtp_hostname, config.smtp_port, config.smtp_username, config.smtp_password, config.smtp_from) hedgedoc_expire = HedgedocExpire(config, mail) -- 2.47.0 From b471bbd4d3201aafb6d6ade8f0bba45bf3234d45 Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 2 Dec 2024 19:49:05 +0100 Subject: [PATCH 3/9] switch to another postgresql python library (from pgsql to psycopg) Switch to a library which is more well-known. I also thought the library was the problem for not being able to template in a JSON path, but that is just a PostgreSQL problem it seems. --- README.md | 22 ++++---- docker-compose.yaml | 2 +- hedgedoc-expire.py | 116 ++++++++++++++++-------------------------- poetry.lock | 121 +++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 2 +- 5 files changed, 170 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index f600f46..aab267f 100644 --- a/README.md +++ b/README.md @@ -85,19 +85,15 @@ To configure the Postgres database connection and the SMTP parameters to send ma For the SMTP connection, the code assumes a standard submission protocol setup with enable StartTLS and authentication, so you will need to configure a username and password. -| Variable | Default | Description | -|-------------------|-----------------------|-------------------------------------| -| POSTGRES_DATABASE | hedgedoc | database to connect to | -| POSTGRES_HOSTNAME | localhost | host of the database server | -| POSTGRES_PASSWORD | geheim | password for the database | -| POSTGRES_PORT | 5432 | port number of the database server | -| POSTGRES_USERNAME | hedgedoc | username for the database | -| SMTP_FROM | | sender address for the expiry mails | -| SMTP_HOSTNAME | localhost | mail server hostname | -| SMTP_PASSWORD | | SMTP password | -| SMTP_PORT | 587 | port to connect to | -| SMTP_USERNAME | | SMTP username | -| URL | http://localhost:3000 | base URL for linking to notes | +| Variable | Default | Description | +|---------------------|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| POSTGRES_CONNSTRING | postgresql://hedgedoc:geheim@localhost:5432/hedgedoc | [PostgreSQL connection string](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) specifying where and how to connnect to the database | +| SMTP_FROM | | sender address for the expiry mails | +| SMTP_HOSTNAME | localhost | mail server hostname | +| SMTP_PASSWORD | | SMTP password | +| SMTP_PORT | 587 | port to connect to | +| SMTP_USERNAME | | SMTP username | +| URL | http://localhost:3000 | base URL for linking to notes | ## Local Development Setup diff --git a/docker-compose.yaml b/docker-compose.yaml index a43750a..f6fbd4d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -48,7 +48,7 @@ services: image: hedgedoc-expire command: "-v -r .001 -n .001 check" environment: - - "POSTGRES_HOSTNAME=database" + - "POSTGRES_CONNSTRING=postgres://hedgedoc:geheim@database:5432/hedgedoc" depends_on: - database diff --git a/hedgedoc-expire.py b/hedgedoc-expire.py index e4fd8a0..57a2762 100644 --- a/hedgedoc-expire.py +++ b/hedgedoc-expire.py @@ -14,7 +14,8 @@ from textwrap import dedent from time import sleep import humanize -import pgsql +import psycopg +from psycopg.rows import dict_row class Config: @@ -28,11 +29,7 @@ class Config: self.note_age = timedelta(days=95) self.exclude = [] - self.postgres_hostname = getenv('POSTGRES_HOSTNAME', 'localhost') - self.postgres_username = getenv('POSTGRES_USERNAME', 'hedgedoc') - self.postgres_password = getenv('POSTGRES_PASSWORD', 'geheim') - self.postgres_database = getenv('POSTGRES_DATABASE', 'hedgedoc') - self.postgres_port = int(getenv('POSTGRES_PORT', '5432')) + self.postgres_connection_string = getenv('POSTGRES_CONNSTRING', 'postgresql://hedgedoc:geheim@localhost:5432/hedgedoc') self.smtp_hostname = getenv('SMTP_HOSTNAME', 'localhost') self.smtp_port = int(getenv('SMTP_PORT', '587')) @@ -95,14 +92,14 @@ class HedgedocExpire: profile = json.loads(row['profile']) return profile['emails'][0] - def notes_to_be_expired(self, db) -> list[any]: + def notes_to_be_expired(self, conn) -> list[any]: """ Get a list of all notes to be expired. :return: """ - notes = [] cutoff = datetime.now(timezone.utc) - self.config.note_age - with db.prepare('''SELECT + with conn.cursor(row_factory=dict_row) as cur: + cur.execute('''SELECT "Notes"."alias", "Notes"."content", "Notes"."createdAt", @@ -114,34 +111,21 @@ class HedgedocExpire: "Users"."email", "Users"."profile" FROM "Notes", "Users" - WHERE "Notes"."updatedAt" < $1 + WHERE "Notes"."updatedAt" < %s AND "Notes"."ownerId" = "Users"."id" ORDER BY "Notes"."updatedAt" - ''') as notes_older_than: - for row in notes_older_than(cutoff): - notes.append({ - 'alias': row.alias if row.alias is not None else row.shortid, - 'content': row.content, - 'createdAt': row.createdAt, - 'email': row.email, - "id": row.id, - 'ownerId': row.ownerId, - 'profile': row.profile, - 'shortid': row.shortid, - 'title': row.title, - 'updatedAt': row.updatedAt - }) - return notes + ''', [cutoff]) + return cur.fetchall() - def revisions_to_be_expired(self, db) -> list[any]: + def revisions_to_be_expired(self, conn) -> list[any]: """ Obtain a list of revisions to be expired. - :param db: the database connection + :param conn: the database connection :return: """ - revisions = [] cutoff = datetime.now(timezone.utc) - self.config.revision_age - with db.prepare('''SELECT + with conn.cursor(row_factory=dict_row) as cur: + cur.execute('''SELECT "Notes"."alias", "Revisions"."createdAt", "Users"."email", @@ -151,40 +135,29 @@ class HedgedocExpire: "Notes"."shortid" as "shortid", "Notes"."title" FROM "Revisions", "Notes", "Users" - WHERE "Revisions"."createdAt" < $1 + WHERE "Revisions"."createdAt" < %s AND "Revisions"."noteId" = "Notes"."id" AND "Notes"."ownerId" = "Users"."id" ORDER BY "Notes"."createdAt", "Revisions"."createdAt" - ''') as revs_older_than: - for row in revs_older_than(cutoff): - revisions.append({ - 'alias': row.alias, - 'createdAt': row.createdAt, - 'email': row.email, - 'noteId': row.noteId, - 'profile': row.profile, - 'revisionId': row.revisionId, - 'shortid': row.shortid, - 'title': row.title - }) - return revisions + ''', [cutoff]) + return cur.fetchall() - def check_notes_to_be_expired(self, db) -> str: + def check_notes_to_be_expired(self, conn) -> str: """ Return a list of notes that will be expired. - :param db: the database connection + :param conn: the database connection :return: a multi-line text suitable for humans to read """ r = '' cutoff = datetime.now(timezone.utc) - self.config.note_age r += f'Notes to be deleted not changed since {cutoff} ({humanize.naturaldelta(self.config.note_age)}):\n' - for note in self.notes_to_be_expired(db): - age = datetime.now(timezone.utc) - datetime.fromisoformat(note['updatedAt']) + for note in self.notes_to_be_expired(conn): + age = datetime.now(timezone.utc) - note['updatedAt'] url = self.config.url + '/' + (note["alias"] if note["alias"] is not None else note["shortid"]) r += f' {self.email_from_email_or_profile(note)} ({humanize.naturaldelta(age)}) {url}: {note["title"]}\n' return r - def check_revisions_to_be_expired(self, db) -> str: + def check_revisions_to_be_expired(self, conn) -> str: """ Return a list of revisions that will be expired. :return: a multi-line text suitable for humans to read @@ -193,8 +166,8 @@ class HedgedocExpire: cutoff = datetime.now(timezone.utc) - self.config.revision_age r += f'Revisions to be deleted created before {cutoff} ({humanize.naturaldelta(self.config.revision_age)}):\n' notes = {} - for row in self.revisions_to_be_expired(db): - row['age'] = datetime.now(timezone.utc) - datetime.fromisoformat(row['createdAt']) + for row in self.revisions_to_be_expired(conn): + row['age'] = datetime.now(timezone.utc) - row['createdAt'] if row['noteId'] not in notes: notes[row['noteId']] = [] notes[row['noteId']].append(row) @@ -207,16 +180,16 @@ class HedgedocExpire: r += f' {humanize.naturaldelta(rev["age"])}: {rev["revisionId"]}\n' return r - def expire_old_notes(self, db) -> None: + def expire_old_notes(self, conn) -> None: """ Email old notes to their owners, then delete them. - :param db: the database connection + :param conn: the database connection :return: """ - with db.prepare('DELETE FROM "Notes" WHERE "id" = $1') as delete_statement: - for note in self.notes_to_be_expired(db): + with conn.cursor() as cur: + for note in self.notes_to_be_expired(conn): try: - note_age = datetime.now(timezone.utc) - datetime.fromisoformat(note['updatedAt']) + note_age = datetime.now(timezone.utc) - note['updatedAt'] msg = MIMEMultipart() msg['From'] = self.email_sender.mail_from msg['To'] = self.email_from_email_or_profile(note) @@ -240,7 +213,8 @@ class HedgedocExpire: self.email_sender.send(msg) # email backup of the note sent, now we can delete it - delete_statement(note["id"]) + cur.execute('DELETE FROM "Notes" WHERE "id" = %s', [note["id"]]) + conn.commit() if self.config.verbose: url = self.config.url + '/' + (note["alias"] if note["alias"] is not None else note["shortid"]) @@ -248,43 +222,42 @@ class HedgedocExpire: except Exception as e: print(f'Unable to send email to {self.email_from_email_or_profile(note)}: {e}', file=sys.stderr) - def expire_old_revisions(self, db) -> None: + def expire_old_revisions(self, conn) -> None: """ Removes all revision on all notes that have been modified earlier than age. - :param db: the database connection + :param conn: the database connection :return: """ cutoff = datetime.now(timezone.utc) - self.config.revision_age - with db.prepare('DELETE FROM "Revisions" WHERE "createdAt" < $1 RETURNING id') as delete: - rows = list(delete(cutoff)) + with conn.cursor() as cur: + rows = list(cur.execute('DELETE FROM "Revisions" WHERE "createdAt" < %s RETURNING id', [cutoff])) if self.config.verbose: print(f'Deleted {len(rows)} old revisions') + conn.commit() def cmd_check(self) -> None: - with pgsql.Connection((self.config.postgres_hostname, self.config.postgres_port), - self.config.postgres_username, self.config.postgres_password) as db: + with psycopg.connect(self.config.postgres_connection_string) as conn: if 'revision' not in self.config.exclude: - print(self.check_revisions_to_be_expired(db)) + print(self.check_revisions_to_be_expired(conn)) elif self.config.verbose: print("Revisions were excluded from check, not checking.\n") if 'note' not in self.config.exclude: - print(self.check_notes_to_be_expired(db)) + print(self.check_notes_to_be_expired(conn)) elif self.config.verbose: print("Notes were excluded from check, not checking.\n") def cmd_emailcheck(self) -> None: - with pgsql.Connection((self.config.postgres_hostname, self.config.postgres_port), - self.config.postgres_username, self.config.postgres_password) as db: + with psycopg.connect(self.config.postgres_connection_string) as conn: report = '' if 'revision' not in self.config.exclude: - report += self.check_revisions_to_be_expired(db) + report += self.check_revisions_to_be_expired(conn) else: report += "Revisions were excluded from check.\n" if 'note' not in self.config.exclude: - report += self.check_notes_to_be_expired(db) + report += self.check_notes_to_be_expired(conn) else: report += "Notes were excluded from check.\n" msg = MIMEMultipart() @@ -301,15 +274,14 @@ class HedgedocExpire: self.email_sender.send(msg) def cmd_expire(self) -> None: - with pgsql.Connection((self.config.postgres_hostname, self.config.postgres_port), - self.config.postgres_username, self.config.postgres_password) as db: + with psycopg.connect(self.config.postgres_connection_string) as conn: if 'revision' not in self.config.exclude: - self.expire_old_revisions(db) + self.expire_old_revisions(conn) elif self.config.verbose: print("Revisions were excluded from action, not expiring.\n") if 'note' not in self.config.exclude: - self.expire_old_notes(db) + self.expire_old_notes(conn) elif self.config.verbose: print("Notes were excluded from action, not expiring.\n") diff --git a/poetry.lock b/poetry.lock index f882a16..b5948ef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -15,16 +15,125 @@ files = [ tests = ["freezegun", "pytest", "pytest-cov"] [[package]] -name = "pgsql" -version = "2.2" -description = "PostgreSQL client library for Python 3" +name = "psycopg" +version = "3.2.3" +description = "PostgreSQL database adapter for Python" optional = false -python-versions = ">=3.11" +python-versions = ">=3.8" files = [ - {file = "pgsql-2.2-py3-none-any.whl", hash = "sha256:12d3360645089d2ee6ab04dd8b9a03aeb7092e3459323f9553252bf4b8bdd2ed"}, + {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, + {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, +] + +[package.dependencies] +psycopg-binary = {version = "3.2.3", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.3)"] +c = ["psycopg-c (==3.2.3)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.2.3" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "bc794458508ceedfc18e9635ad18b7113d5118c414698ebc12762c782ed3670d" +content-hash = "6ef91abe2e2acfa0b7355dca99bfd6595e18bf90609c323f1857f13a1df73aaf" diff --git a/pyproject.toml b/pyproject.toml index af2182b..7da3881 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,8 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.11" -pgsql = "^2.2" humanize = "^4.9.0" +psycopg = {extras = ["binary"], version = "^3.2.3"} [build-system] -- 2.47.0 From 48ba4ef7ca21db600371873d8329a4c97f7f30f4 Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 2 Dec 2024 19:46:28 +0100 Subject: [PATCH 4/9] add logic for removing deleted notes from users histories --- hedgedoc-expire.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/hedgedoc-expire.py b/hedgedoc-expire.py index 57a2762..e4c1cba 100644 --- a/hedgedoc-expire.py +++ b/hedgedoc-expire.py @@ -1,5 +1,7 @@ #!/bin/env python import argparse +import base64 +import binascii import email import json import smtplib @@ -219,6 +221,41 @@ class HedgedocExpire: if self.config.verbose: url = self.config.url + '/' + (note["alias"] if note["alias"] is not None else note["shortid"]) print(f'Note "{note["title"]}" ({url}) emailed to {msg["To"]}') + + try: + with conn.cursor(row_factory=dict_row) as user_history_cur: + # Calculate the urlid of the note from its id. + # See: + # - https://github.com/hedgedoc/hedgedoc/blob/380587b7fd65bc1eb71eef51a3aab324f9877650/lib/models/note.js#L167-L172 + # - https://git.cccv.de/infra/ansible/roles/hedgedoc/-/blob/d69cef4bf6c7fe4e67570363659e4c20b0e102af/files/hedgedoc-util.py#L84 + urlid = base64.urlsafe_b64encode(binascii.unhexlify(f"{note['id']}".replace('-', ''))).decode().replace('=', '') + + # Get all users with note in history. + user_history_cur.execute('''SELECT + "Users"."id", + "Users"."history", + "Users"."email", + "Users"."profile" + FROM "Users" + WHERE jsonb_path_exists("Users"."history"::jsonb, '$[*] ? (@.id == $urlid)', jsonb_build_object('urlid', %s::text)) + ''', [urlid]) + users_with_note = user_history_cur.fetchall() + + for user in users_with_note: + history = json.loads(user["history"]) + history_without_note = json.dumps([ entry for entry in history if entry["id"] != urlid ]) + user_history_cur.execute('''UPDATE "Users" + SET "history" = %s + WHERE "id" = %s + ''', [history_without_note, user["id"]]) + + conn.commit() + if self.config.verbose: + for user in users_with_note: + print(f' deleted history entry for {self.email_from_email_or_profile(user)}') + except Exception as e: + conn.rollback() + print(f'An error occured while trying to delete {note["id"]} from the users history: {e}', file=sys.stderr) except Exception as e: print(f'Unable to send email to {self.email_from_email_or_profile(note)}: {e}', file=sys.stderr) -- 2.47.0 From 1911694baf99b5eb51589a3c351c2cba08528357 Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 9 Dec 2024 15:15:13 +0100 Subject: [PATCH 5/9] only login to SMTP server, if username and password set --- hedgedoc-expire.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hedgedoc-expire.py b/hedgedoc-expire.py index e4c1cba..30567a3 100644 --- a/hedgedoc-expire.py +++ b/hedgedoc-expire.py @@ -64,7 +64,8 @@ class EmailSender: smtp_server = smtplib.SMTP(self.hostname, port=self.port) context = ssl.create_default_context() smtp_server.starttls(context=context) - smtp_server.login(self.username, self.password) + if self.username != "" and self.password != "": + smtp_server.login(self.username, self.password) smtp_server.send_message(message) except Exception as e: print(f'Unable to send mail through {self}: {e}') -- 2.47.0 From 4bee60b8dddbf6af5408eeb7596bcaadc7a9e94c Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 9 Dec 2024 17:39:14 +0100 Subject: [PATCH 6/9] include date header in mail as some mail servers require that --- hedgedoc-expire.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hedgedoc-expire.py b/hedgedoc-expire.py index 30567a3..64ed766 100644 --- a/hedgedoc-expire.py +++ b/hedgedoc-expire.py @@ -196,6 +196,7 @@ class HedgedocExpire: msg = MIMEMultipart() msg['From'] = self.email_sender.mail_from msg['To'] = self.email_from_email_or_profile(note) + msg['Date'] = email.utils.formatdate() msg['Subject'] = f'Your HedgeDoc Note "{note["title"]}" has expired' msg.attach(MIMEText(dedent(f'''\ You created the note titled "{note["title"]}" on {note["createdAt"]}. @@ -301,6 +302,7 @@ class HedgedocExpire: msg = MIMEMultipart() msg['From'] = self.email_sender.mail_from msg['To'] = self.email_sender.mail_from + msg['Date'] = email.utils.formatdate() msg['Subject'] = f'Hedgedoc Expire: Report' msg.attach(MIMEText(dedent(f'''\ This report shows which notes and revisions would be deleted if expire would be run now. -- 2.47.0 From 07ef29225e6e8f73653b6a2bcfdb187aecad739e Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 9 Dec 2024 17:50:26 +0100 Subject: [PATCH 7/9] remove the exclude arg and instead have the revs and notes args be opt. And then only act on notes/revisions, if the relevant argument got specified. This makes the CLI cleaner. --- README.md | 24 ++++++++++++++++++++---- hedgedoc-expire.py | 38 ++++++++++++++++---------------------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index aab267f..b38355e 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,16 @@ Currently, it only works with Postgres. ### Expiring old revisions +Using the `-r` or `--revisions` argument. + All revisions that have been created before the specified age will be removed. If all revisions are expired, the note remains available, it just won't have any revisions to revert to. Once you continue editing it, new revisions will be added. ### Expiring old notes +Using the `-n` or `--notes` argument. + Notes that are being expired will be emailed to the account that initially created the note. This allows that user to restore the note, if necessary. Expiring a note will also remove all existing revisions for the note. @@ -28,10 +32,22 @@ be recovered. ## Running `hedgedoc-expire.py` -Locally from the command line: +Locally from the command line expiring revisions and notes older than 7 days: ```shell -poetry run python ./hedgedoc-expire.py ... +poetry run python ./hedgedoc-expire.py expire -n 7 -r 7 +``` + +Locally from the command line only expiring revisions older than 7 days: + +```shell +poetry run python ./hedgedoc-expire.py expire -r 7 +``` + +Locally from the command line only expiring notes older than 7 days: + +```shell +poetry run python ./hedgedoc-expire.py expire -n 7 ``` From Docker Compose: @@ -65,8 +81,8 @@ Without it, the expiry action will be taken. | Option | Default | Description | |--------|---------|-------------------------------------------------------------------| -| -n | 90 | remove all notes not changed in these many days | -| -r | 7 | remove all revisions created more than these many days ago | +| -n | None | remove all notes not changed in these many days | +| -r | None | remove all revisions created more than these many days ago | | -v | false | Print info on current action during `cron` and `expire` commandds | Command is one of: diff --git a/hedgedoc-expire.py b/hedgedoc-expire.py index 64ed766..9a2e0c1 100644 --- a/hedgedoc-expire.py +++ b/hedgedoc-expire.py @@ -29,7 +29,6 @@ class Config: self.verbose = False self.revision_age = timedelta(days=14) self.note_age = timedelta(days=95) - self.exclude = [] self.postgres_connection_string = getenv('POSTGRES_CONNSTRING', 'postgresql://hedgedoc:geheim@localhost:5432/hedgedoc') @@ -276,29 +275,29 @@ class HedgedocExpire: def cmd_check(self) -> None: with psycopg.connect(self.config.postgres_connection_string) as conn: - if 'revision' not in self.config.exclude: + if self.config.revision_age is not None: print(self.check_revisions_to_be_expired(conn)) elif self.config.verbose: - print("Revisions were excluded from check, not checking.\n") + print("Revisions weren't included in the check, not checking.\n") - if 'note' not in self.config.exclude: + if self.config.note_age is not None: print(self.check_notes_to_be_expired(conn)) elif self.config.verbose: - print("Notes were excluded from check, not checking.\n") + print("Notes weren't included in the check, not checking.\n") def cmd_emailcheck(self) -> None: with psycopg.connect(self.config.postgres_connection_string) as conn: report = '' - if 'revision' not in self.config.exclude: + if self.config.revision_age is not None: report += self.check_revisions_to_be_expired(conn) else: - report += "Revisions were excluded from check.\n" + report += "Revisions weren't included in the check.\n" - if 'note' not in self.config.exclude: + if self.config.note_age is not None: report += self.check_notes_to_be_expired(conn) else: - report += "Notes were excluded from check.\n" + report += "Notes weren't included in the check.\n" msg = MIMEMultipart() msg['From'] = self.email_sender.mail_from msg['To'] = self.email_sender.mail_from @@ -315,15 +314,15 @@ class HedgedocExpire: def cmd_expire(self) -> None: with psycopg.connect(self.config.postgres_connection_string) as conn: - if 'revision' not in self.config.exclude: + if self.config.revision_age is not None: self.expire_old_revisions(conn) elif self.config.verbose: - print("Revisions were excluded from action, not expiring.\n") + print("Revisions weren't included in the expire action, not expiring.\n") - if 'note' not in self.config.exclude: + if self.config.note_age is not None: self.expire_old_notes(conn) elif self.config.verbose: - print("Notes were excluded from action, not expiring.\n") + print("Notes weren't included in the expire action, not expiring.\n") def main(): @@ -346,25 +345,20 @@ def main(): See https://git.hamburg.ccc.de/CCCHH/hedgedoc-expire ''') ) - parser.add_argument('-n', '--notes', metavar='DAYS', type=float, default=95, + parser.add_argument('-n', '--notes', metavar='DAYS', type=float, help='remove all notes not changed in these many days') - parser.add_argument('-r', '--revisions', metavar='DAYS', type=float, default=14, + parser.add_argument('-r', '--revisions', metavar='DAYS', type=float, help='remove all revisions created more than these many days ago') parser.add_argument('command', choices=['check', 'cron', 'emailcheck', 'expire'], default='check', nargs='?', help='action to perform') parser.add_argument('-v', '--verbose', action='store_true', default=False, help='print more info while running') - parser.add_argument('--exclude', nargs="+", choices=['revision', 'note'], - help='Type ("revision" or "note") to exclude from the action.') args = parser.parse_args() config = Config() - config.note_age = timedelta(days=args.notes) - config.revision_age = timedelta(days=args.revisions) + config.note_age = timedelta(days=args.notes) if args.notes is not None else None + config.revision_age = timedelta(days=args.revisions) if args.revisions is not None else None config.verbose = args.verbose - if (args.exclude): - config.exclude = args.exclude - print(config.exclude) mail = EmailSender(config.smtp_hostname, config.smtp_port, config.smtp_username, config.smtp_password, config.smtp_from) hedgedoc_expire = HedgedocExpire(config, mail) -- 2.47.0 From 5569119fcc56e125974ec7c0e07f2ee87dbd0f73 Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 9 Dec 2024 18:22:27 +0100 Subject: [PATCH 8/9] improve docs with better infos, format and consistency For the README: - Improve the format, getting rid of the textwidth-based linebreaks and instead putting each sentence on its own line. - Have an example for a local check execution to have the examples be more diverse. - Describe the docker compose example and have it be up-to-date. - Better describe the local setup example. - Reformat the section on commands and arguments, first describing the commands and then the arguments, as this makes more sense structurally. - Also change the title of the arguments and environment variables section to reflect its content on commands. For the help output mirror the command descriptions of the README and improve the format. --- README.md | 69 +++++++++++++++++++++++----------------------- hedgedoc-expire.py | 10 +++---- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b38355e..5ae22a8 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,35 @@ # hedgedoc-expire - remove old notes -A Python app that can be run regularly against a Postgres Hedgedoc database to remove old notes. Notes that are expired -can be emailed to the author as a backup. +A Python app that can be run regularly against a Postgres Hedgedoc database to remove old notes. +Notes that are expired can be emailed to the author as a backup. ## Expiring old notes and revisions -Hedgedoc keeps notes and revisions (versions) of those notes forever. This might not be desirable, for example because -of data protection reasons. With this utility, you can remove old revisions and old notes from the -database. `hedgedoc-expire` works by talking directly to a Postgres database; no API access to Hedgedoc is required. +Hedgedoc keeps notes and revisions (versions) of those notes forever. +This might not be desirable, for example because of data protection reasons. +With this utility, you can remove old revisions and old notes from the database. +`hedgedoc-expire` works by talking directly to a Postgres database; no API access to Hedgedoc is required. Currently, it only works with Postgres. ### Expiring old revisions Using the `-r` or `--revisions` argument. -All revisions that have been created before the specified age will be removed. If all revisions are expired, the note -remains available, it just won't have any revisions to revert to. Once you continue editing it, new revisions will be -added. +All revisions that have been created before the specified age will be removed. +If all revisions are expired, the note remains available, it just won't have any revisions to revert to. +Once you continue editing it, new revisions will be added. ### Expiring old notes Using the `-n` or `--notes` argument. -Notes that are being expired will be emailed to the account that initially created the note. This allows that user to -restore the note, if necessary. Expiring a note will also remove all existing revisions for the note. +Notes that are being expired will be emailed to the account that initially created the note. +This allows that user to restore the note, if necessary. +Expiring a note will also remove all existing revisions for the note. -You will need to configure your environment for `hedgedoc-expire` to be able to send mail. If the mail is not accepted -by the mail server, the note will not be removed. Note however that this utility has no idea if the mail server has -successfully delivered that mail to the intended recipient; if the mail gets lost somewhere on the way, the note cannot -be recovered. +You will need to configure your environment for `hedgedoc-expire` to be able to send mail. +If the mail is not accepted by the mail server, the note will not be removed. +Note however that this utility has no idea if the mail server has successfully delivered that mail to the intended recipient; if the mail gets lost somewhere on the way, the note cannot be recovered. ## Running `hedgedoc-expire.py` @@ -44,25 +45,25 @@ Locally from the command line only expiring revisions older than 7 days: poetry run python ./hedgedoc-expire.py expire -r 7 ``` -Locally from the command line only expiring notes older than 7 days: +Locally from the command line checking which notes older than 7 days would be expired (and only checking notes): ```shell -poetry run python ./hedgedoc-expire.py expire -n 7 +poetry run python ./hedgedoc-expire.py check -n 7 ``` -From Docker Compose: +From Docker Compose checking which notes older than 95 and which revisions older than 14 days would be expired: ```yaml hedgedoc-expire: image: hedgedoc-expire - command: "-c -r 14 -n 95" + command: "check -r 14 -n 95" environment: - "POSTGRES_HOSTNAME=database" depends_on: - database ``` -Running against a local setup with one note, with times set to a fraction of a day: +Running a check against a local setup with one note, with times set to a fraction of a day: ```shell $ poetry run python ./hedgedoc-expire.py -n .001 -r .001 @@ -74,26 +75,26 @@ Notes to be deleted not changed since 2024-05-20 09:02:46.416294+00:00 (a minute foo@example.com (a day) http://localhost:3000/foo: hedgedoc-expire - remove old notes ``` -## Arguments and Environment Variables +## Commands, Arguments and Environment Variables -There are two main modes to run `hedgedoc-require`: check and expire. With `-c`, a report is generated on standard out. -Without it, the expiry action will be taken. +`hedgedoc-expire` provides several different commands with check being the default: + +| Command | Description | +|------------|--------------------------------------------------------------------------------------------------------| +| check | Print a list of revisions and notes that would be expired, based on the given arguments `-n` and `-r`. | +| cron | Run `expire` at 2 am local time each day. Will run until killed. | +| emailcheck | Send an email from the configured sender to themselves with the the check report. | +| expire | Expire old revisions and notes, based on the given arguments `-n` and `-r`. | + +Additionally the following arguments are available. +`-n` and `-r` are crucial in determining whether or not and how many notes/revisions should be targeted. | Option | Default | Description | |--------|---------|-------------------------------------------------------------------| -| -n | None | remove all notes not changed in these many days | -| -r | None | remove all revisions created more than these many days ago | +| -n | None | target all notes not changed in these many days | +| -r | None | target all revisions created more than these many days ago | | -v | false | Print info on current action during `cron` and `expire` commandds | -Command is one of: - -| Command | Description | -|------------|------------------------------------------------------------------------------------------------------------| -| check | Print a list of revisions and notes that would be expired, based on the given parameters for `-n` and `-r` | -| cron | Run `expire` at 2 am local time each day. Will run until killed. | -| emailcheck | Send an email from the configured sender to themselves with the the check report. | -| expire | Expire old revisions and notes. | - ### Environment variables To configure the Postgres database connection and the SMTP parameters to send mail, set these variables. @@ -122,4 +123,4 @@ You will need to create a user using the command line: ```sh docker compose exec -it hedgedoc bin/manage_users --add foo@example.com --pass geheim -``` \ No newline at end of file +``` diff --git a/hedgedoc-expire.py b/hedgedoc-expire.py index 9a2e0c1..54478f7 100644 --- a/hedgedoc-expire.py +++ b/hedgedoc-expire.py @@ -336,11 +336,11 @@ def main(): Revisions of notes that have been created before the specified time will be deleted. '''), epilog=dedent('''\ - command is one of: - - check: print a list of revisions and notes to be expired - - cron: run expire every 24 hours - - emailcheck: send am email from the configured sender to themselves with the the check report - - expire: expire old revisions and untouched notes + command is one of, which check being the default: + - check: Print a list of revisions and notes that would be expired, based on the given arguments -n and -r. + - cron: Run `expire` at 2 am local time each day. Will run until killed. + - emailcheck: Send an email from the configured sender to themselves with the the check report. + - expire: Expire old revisions and notes, based on the given arguments -n and -r. See https://git.hamburg.ccc.de/CCCHH/hedgedoc-expire ''') -- 2.47.0 From 8ccb28a5fa3bb6ae9a0eeb5d8920b25e84af94c2 Mon Sep 17 00:00:00 2001 From: Julian Schacher Date: Mon, 9 Dec 2024 18:25:16 +0100 Subject: [PATCH 9/9] bump version to 0.2.0 as a lot changed, especially the CLI and config --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7da3881..aae072a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hedgedoc-expire" -version = "0.1.0" +version = "0.2.0" description = "Remove old Hedgedoc notes" authors = ["Stefan Bethke "] readme = "README.md" -- 2.47.0