@@ -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