--- /dev/null
+###############################################################################
+# #
+# Pakfire - The IPFire package management system #
+# Copyright (C) 2025 Pakfire development team #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###############################################################################
+
+from . import base
+from . import database
+
+import sqlalchemy
+from sqlalchemy import Column, DateTime, PickleType, Text
+
+cache = sqlalchemy.Table(
+ "cache", database.Base.metadata,
+
+ # Key
+ Column("key", Text, nullable=False),
+
+ # Value
+ Column("value", PickleType, nullable=False),
+
+ # Timestamp
+ Column("created_at", DateTime(timezone=False), nullable=False,
+ server_default=sqlalchemy.func.current_timestamp()),
+
+ # Expires At
+ Column("expires_at", DateTime(timezone=False), nullable=False),
+)
+
+class Cache(base.Object):
+ async def get(self, key):
+ """
+ Fetches an item from the cache by its key
+ """
+ stmt = (
+ sqlalchemy
+ .select(
+ cache.c.value
+ )
+ .where(
+ cache.c.key == key,
+
+ # The entry must have no expiry time or not be expired, yet
+ sqlalchemy.or_(
+ cache.c.expires_at == None,
+ cache.c.expires_at > sqlalchemy.func.current_timestamp(),
+ )
+ )
+ )
+
+ # Fetch the item
+ return await self.db.select_one(stmt, "value")
+
+ async def set(self, key, value, expires_at=None):
+ """
+ Stores an item in the cache
+ """
+ if expires_at:
+ if not isinstance(expires_at, datetime.timedelta):
+ expires_at = datetime.timedelta(seconds=expires_at)
+
+ # Make it an absolute timestamp
+ expires_at = sqlalchemy.func.current_timestamp() + expires_at
+
+ # Create a new entry to the database
+ insert_stmt = (
+ sqlalchemy.dialects.postgresql
+ .insert(
+ cache,
+ )
+ .values({
+ "key" : key,
+ "value" : value,
+ "expires_at" : expires_at,
+ })
+ )
+
+ # If the entry exist already, we just update the value and expiry time
+ upsert_stmt = insert_stmt.on_conflict_do_update(
+ index_elements = [
+ "key",
+ ],
+ set_ = {
+ "value" : cache.c.value,
+ "expires_at" : cache.c.expires_at,
+ },
+ )
+
+ # Run the query
+ await self.db.execute(upsert_stmt)
+
+ async def cleanup(self):
+ """
+ Called to cleanup the cache from expired entries
+ """
+ # Delete everything that has expired in the past
+ stmt = (
+ cache
+ .delete()
+ .where(
+ cache.c.expires_at <= sqlalchemy.func.current_timestamp(),
+ )
+ )
+
+ # Run the query
+ async with await self.db.transaction():
+ await self.db.execute(stmt)
ALTER SEQUENCE public.builds_id_seq OWNED BY public.builds.id;
+--
+-- Name: cache; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.cache (
+ key text NOT NULL,
+ value bytea NOT NULL,
+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ expires_at timestamp without time zone
+);
+
+
--
-- Name: distributions; Type: TABLE; Schema: public; Owner: -
--
ADD CONSTRAINT builds_pkey PRIMARY KEY (id);
+--
+-- Name: cache cache_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.cache
+ ADD CONSTRAINT cache_pkey PRIMARY KEY (key);
+
+
--
-- Name: distributions distributions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--