summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pypaste/__init__.py4
-rw-r--r--pypaste/__main__.py14
-rw-r--r--pypaste/sqlite/__init__.py103
3 files changed, 119 insertions, 2 deletions
diff --git a/pypaste/__init__.py b/pypaste/__init__.py
index 0206e9e..4932f76 100644
--- a/pypaste/__init__.py
+++ b/pypaste/__init__.py
@@ -92,6 +92,10 @@ class Storage:
connection: aiosqlite.Connection
@abstractmethod
+ async def setup(self) -> None:
+ pass
+
+ @abstractmethod
async def insert(self, paste: Paste) -> None:
pass
diff --git a/pypaste/__main__.py b/pypaste/__main__.py
index c7704d5..5215d15 100644
--- a/pypaste/__main__.py
+++ b/pypaste/__main__.py
@@ -17,8 +17,9 @@ import sys
import os
import asyncio
import aiosqlite
-from pypaste import App, AppConfig, log_error, log_info
+from pypaste import App, AppConfig, Storage, log_error, log_info
from pypaste.s3 import S3
+from pypaste.sqlite import Sqlite
from socket import socket, AF_UNIX, SOCK_STREAM
from argparse import ArgumentParser
from aiohttp import web
@@ -48,6 +49,8 @@ async def main() -> int:
s3parser.add_argument("--access-key", required=True)
s3parser.add_argument("--secret-key", type=Path, required=True)
+ subparsers.add_parser("sqlite")
+
args = parser.parse_args()
try:
@@ -99,7 +102,7 @@ async def main() -> int:
log_error(f"failed to load secret key from {args.secret_key}: {e}")
return 1
- storage = S3(
+ storage: Storage = S3(
connection,
args.endpoint,
args.region,
@@ -109,6 +112,13 @@ async def main() -> int:
)
await storage.setup()
+ case "sqlite":
+ storage = Sqlite(connection)
+
+ await storage.setup()
+
+ case _:
+ raise Exception(f"unsupported storage backend: {args.command}")
pypaste = App(config, storage)
diff --git a/pypaste/sqlite/__init__.py b/pypaste/sqlite/__init__.py
new file mode 100644
index 0000000..5700356
--- /dev/null
+++ b/pypaste/sqlite/__init__.py
@@ -0,0 +1,103 @@
+import asyncio
+import zstandard
+import aiosqlite
+from pypaste import Storage, Paste
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class Sqlite(Storage):
+ connection: aiosqlite.Connection
+
+ async def setup(self) -> None:
+ await self.connection.execute(
+ "create table if not exists sqlite(key text, data blob)"
+ )
+
+ await self.connection.commit()
+
+ async def insert(self, paste: Paste) -> None:
+ def compress():
+ return zstandard.compress(paste.text.encode())
+
+ data = await asyncio.to_thread(compress)
+
+ await self.connection.execute(
+ "insert into pastes values(?, ?, ?, ?)",
+ (paste.key, paste.dt.isoformat(), len(data), paste.syntax),
+ )
+
+ await self.connection.execute(
+ "insert into sqlite values(?, ?)",
+ (
+ paste.key,
+ data,
+ ),
+ )
+
+ await self.connection.commit()
+
+ async def retrieve(self, key: str) -> Optional[Paste]:
+ if not await self.exists(key):
+ return None
+
+ async with self.connection.execute(
+ "select sqlite.data from sqlite where key=? limit 1", (key,)
+ ) as cursor:
+ match await cursor.fetchone():
+ case [bytes(data)]:
+ pass
+ case _:
+ raise Exception("unreachable")
+
+ row = await self.read_row(key)
+
+ assert row is not None
+
+ (dt, size, syntax) = row
+
+ def decompress():
+ return zstandard.decompress(data).decode()
+
+ text = await asyncio.to_thread(decompress)
+
+ return Paste(key, dt, syntax, text)
+
+ async def delete(self, key: str) -> None:
+ await self.connection.execute("delete from pastes where key=?", (key,))
+
+ await self.connection.execute("delete from sqlite where key=?", (key,))
+
+ async def vacuum(self, max: int) -> None:
+ while True:
+ async with self.connection.execute(
+ (
+ "select sum(pastes.size) from pastes "
+ "inner join sqlite on sqlite.key "
+ "where pastes.key=sqlite.key"
+ )
+ ) as cursor:
+ if (row := await cursor.fetchone()) is None:
+ return
+ else:
+ use = row[0]
+
+ async with self.connection.execute(
+ (
+ "select pastes.key from pastes "
+ "inner join sqlite on sqlite.key "
+ "where pastes.key=sqlite.key "
+ "order by pastes.datetime "
+ "limit 1"
+ )
+ ) as cursor:
+ if (row := await cursor.fetchone()) is None:
+ return
+ else:
+ oldest = row[0]
+
+ if use > max:
+ await self.delete(oldest)
+ else:
+ return