Skip to content

Commit 285357a

Browse files
committed
feat: implement compression in PasteService
- Update _save_content to compress content above threshold - Update _read_content to decompress compressed content - Update create_paste to store compression metadata - Update get_paste_by_id to read compressed pastes - Update edit_paste to handle compression on edits - Graceful fallback if compression fails - Full backward compatibility with uncompressed pastes
1 parent 14cfac3 commit 285357a

File tree

1 file changed

+112
-14
lines changed

1 file changed

+112
-14
lines changed

backend/app/services/paste_service.py

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,106 @@ def __init__(
4949
self._lock_file: Path = Path(".cleanup.lock")
5050
self._cleanup_service: CleanupService = cleanup_service
5151

52-
async def _read_content(self, paste_path: str) -> str | None:
52+
async def _read_content(
53+
self, paste_path: str, is_compressed: bool = False
54+
) -> str | None:
55+
"""
56+
Read paste content, handling decompression if needed.
57+
58+
Args:
59+
paste_path: Path to the paste file
60+
is_compressed: Whether the content is compressed
61+
62+
Returns:
63+
Decompressed content string or None on error
64+
"""
5365
try:
54-
async with aiofiles.open(paste_path) as f:
55-
return await f.read()
66+
if is_compressed:
67+
from app.utils.compression import CompressionError, decompress_content
68+
69+
async with aiofiles.open(paste_path, "rb") as f:
70+
compressed_data = await f.read()
71+
try:
72+
return decompress_content(compressed_data)
73+
except CompressionError as exc:
74+
self.logger.error(
75+
"Failed to decompress paste at %s: %s", paste_path, exc
76+
)
77+
return None
78+
else:
79+
async with aiofiles.open(paste_path) as f:
80+
return await f.read()
5681
except Exception as exc:
5782
self.logger.error("Failed to read paste content: %s", exc)
5883
return None
5984

60-
async def _save_content(self, paste_id: str, content: str) -> str | None:
85+
async def _save_content(
86+
self, paste_id: str, content: str
87+
) -> tuple[str, int, bool, int | None] | None:
88+
"""
89+
Save paste content, optionally compressed.
90+
91+
Returns:
92+
Tuple of (content_path, content_size, is_compressed, original_size) or None
93+
"""
6194
try:
95+
from app.utils.compression import (
96+
CompressionError,
97+
compress_content,
98+
should_compress,
99+
)
100+
101+
# Determine if we should compress
102+
use_compression = False
103+
compressed_data = None
104+
original_size = len(content.encode('utf-8'))
105+
106+
if config.COMPRESSION_ENABLED and should_compress(
107+
content, config.COMPRESSION_THRESHOLD_BYTES
108+
):
109+
try:
110+
compressed_data, original_size = compress_content(
111+
content, config.COMPRESSION_LEVEL
112+
)
113+
# Only use compression if it actually saves space
114+
if len(compressed_data) < original_size:
115+
use_compression = True
116+
self.logger.info(
117+
"Compressed paste %s: %d -> %d bytes (%.1f%% reduction)",
118+
paste_id,
119+
original_size,
120+
len(compressed_data),
121+
100 * (1 - len(compressed_data) / original_size),
122+
)
123+
else:
124+
self.logger.debug(
125+
"Compression not beneficial for paste %s, storing uncompressed",
126+
paste_id,
127+
)
128+
except CompressionError as exc:
129+
self.logger.warning(
130+
"Compression failed for paste %s, storing uncompressed: %s",
131+
paste_id,
132+
exc,
133+
)
134+
135+
# Prepare file path
62136
base_file_path = path.join("pastes", f"{paste_id}.txt")
63137
file_path = path.join(self.paste_base_folder_path, base_file_path)
64138
await os.makedirs(path.dirname(file_path), exist_ok=True)
65-
async with aiofiles.open(file_path, "w") as f:
66-
await f.write(content)
67139

68-
return base_file_path
140+
# Write content (compressed or uncompressed)
141+
if use_compression and compressed_data:
142+
async with aiofiles.open(file_path, "wb") as f:
143+
await f.write(compressed_data)
144+
content_size = len(compressed_data)
145+
return base_file_path, content_size, True, original_size
146+
else:
147+
async with aiofiles.open(file_path, "w") as f:
148+
await f.write(content)
149+
content_size = original_size
150+
return base_file_path, content_size, False, None
151+
69152
except Exception as exc:
70153
self.logger.error("Failed to save paste content: %s", exc)
71154
return None
@@ -136,6 +219,7 @@ async def get_paste_by_id(self, paste_id: UUID4) -> PasteResponse | None:
136219
return None
137220
content = await self._read_content(
138221
path.join(self.paste_base_folder_path, result.content_path),
222+
is_compressed=result.is_compressed,
139223
)
140224
return PasteResponse(
141225
id=result.id,
@@ -199,13 +283,21 @@ async def edit_paste(
199283

200284
# Handle content update separately
201285
if edit_paste.content is not None:
202-
new_content_path = await self._save_content(
286+
save_result = await self._save_content(
203287
str(paste_id), edit_paste.content
204288
)
205-
if not new_content_path:
289+
if not save_result:
206290
return None
291+
(
292+
new_content_path,
293+
content_size,
294+
is_compressed,
295+
original_size,
296+
) = save_result
207297
result.content_path = new_content_path
208-
result.content_size = len(edit_paste.content)
298+
result.content_size = content_size
299+
result.is_compressed = is_compressed
300+
result.original_size = original_size
209301

210302
result.last_updated_at = datetime.now(tz=timezone.utc)
211303

@@ -217,7 +309,8 @@ async def edit_paste(
217309
edit_paste.content
218310
if edit_paste.content is not None
219311
else await self._read_content(
220-
path.join(self.paste_base_folder_path, result.content_path)
312+
path.join(self.paste_base_folder_path, result.content_path),
313+
is_compressed=result.is_compressed,
221314
)
222315
)
223316

@@ -285,16 +378,19 @@ async def create_paste(
285378
)
286379

287380
paste_id = uuid.uuid4()
288-
paste_path = await self._save_content(
381+
save_result = await self._save_content(
289382
str(paste_id),
290383
paste.content,
291384
)
292-
if not paste_path:
385+
if not save_result:
293386
raise HTTPException(
294387
status_code=500,
295388
detail="Failed to save paste content",
296389
headers={"Retry-After": "60"},
297390
)
391+
392+
paste_path, content_size, is_compressed, original_size = save_result
393+
298394
try:
299395
# Generate plaintext tokens to return to user
300396
edit_token_plaintext = uuid.uuid4().hex
@@ -313,7 +409,9 @@ async def create_paste(
313409
expires_at=paste.expires_at,
314410
creator_ip=str(user_data.ip),
315411
creator_user_agent=user_data.user_agent,
316-
content_size=len(paste.content),
412+
content_size=content_size,
413+
is_compressed=is_compressed,
414+
original_size=original_size,
317415
edit_token=edit_token_hashed,
318416
delete_token=delete_token_hashed,
319417
)

0 commit comments

Comments
 (0)