Skip to content

Commit 4fe917b

Browse files
committed
feat: clear print work tempfiles on expiration
Relates #47
1 parent 36cea14 commit 4fe917b

File tree

3 files changed

+67
-33
lines changed

3 files changed

+67
-33
lines changed

src/bot/shared_messages.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,22 @@ async def go_to_default_state(callback_or_message: CallbackQuery | Message, stat
6262

6363
async def usual_error_answer(event: ErrorEvent):
6464
answer = None
65-
if "message to delete" in str(event.exception):
66-
answer = (
67-
"A necessary message was lost 🤔\n\n"
68-
+ "Ignore if everything looks good\n"
69-
+ html.bold("Otherwise, try to send the file or /scan again\n")
70-
)
71-
elif "Server disconnected" in str(event.exception):
72-
answer = "Telegram has issues with handling our service now 📉\n\n" + html.bold(
65+
try:
66+
answer = f"{event.exception.response.json()['detail']} 😕\n\n" + html.bold(
7367
"Try to send the file or /scan again"
7468
)
75-
elif "Request timeout" in str(event.exception):
76-
answer = "Telegram or our API is busy now 📉\n\n" + html.bold("Try to send the file or /scan again")
69+
except (AttributeError, ValueError):
70+
if "message to delete" in str(event.exception):
71+
answer = (
72+
"A necessary message was lost 🤔\n\n"
73+
+ "Ignore if everything looks good\n"
74+
+ html.bold("Otherwise, try to send the file or /scan again\n")
75+
)
76+
elif "Server disconnected" in str(event.exception):
77+
answer = "Telegram has issues with handling our service now 📉\n\n" + html.bold(
78+
"Try to send the file or /scan again"
79+
)
80+
elif "Request timeout" in str(event.exception):
81+
answer = "Telegram or our API is busy now 📉\n\n" + html.bold("Try to send the file or /scan again")
7782
logger.error(f"{event.exception}")
7883
return answer

src/modules/printing/repository.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
__all__ = ["printing_repository"]
22

3+
import asyncio
4+
import os
5+
import pathlib
36
import re
47
import time
8+
from asyncio import Task
9+
from tempfile import _TemporaryFileWrapper
510

611
import bs4
712
import cups
813
import httpx
914
from cachetools import TTLCache
1015

16+
from src.api.dependencies import USER_AUTH
1117
from src.api.logging_ import logger
1218
from src.config import settings
1319
from src.config_schema import Printer
@@ -40,6 +46,30 @@ def callback(prompt):
4046
# Cache printer toner status for 5 minutes
4147
self._printer_toner_status_cache = TTLCache(maxsize=100, ttl=5 * 60)
4248

49+
self.tempfiles: dict[tuple[str, str], tuple[_TemporaryFileWrapper[bytes], Task[None]]] = {}
50+
self.tempfile_expiration_time = 6 * 60 * 60
51+
52+
def store_tempfile(self, innohassle_user_id, f):
53+
self.tempfiles[(innohassle_user_id, pathlib.Path(f.name).name)] = (
54+
f,
55+
asyncio.create_task(self.wait_for_tempfile_expiration(innohassle_user_id, f)),
56+
)
57+
58+
async def wait_for_tempfile_expiration(self, innohassle_user_id, f):
59+
await asyncio.sleep(self.tempfile_expiration_time)
60+
await self.remove_tempfile(innohassle_user_id, pathlib.Path(f.name).name, True)
61+
62+
async def remove_tempfile(self, innohassle_user_id, filename, expired=False):
63+
if (innohassle_user_id, filename) in self.tempfiles:
64+
os.unlink(self.get_tempfile_path(innohassle_user_id, filename))
65+
if not expired:
66+
self.tempfiles[(innohassle_user_id, filename)][1].cancel()
67+
del self.tempfiles[(innohassle_user_id, filename)]
68+
return True
69+
70+
def get_tempfile_path(self, innohassle_user_id, filename):
71+
return self.tempfiles[(innohassle_user_id, filename)][0].name
72+
4373
def get_printer(self, cups_name: str) -> Printer | None:
4474
for elem in settings.api.printers_list:
4575
if elem.cups_name == cups_name:
@@ -172,9 +202,15 @@ def _parse_paper_percentage(self, html: str) -> int | None:
172202
return int((level / maxcapacity) * 100)
173203
return None
174204

175-
def print_file(self, printer: Printer, file_name: str, title: str, options: PrintingOptions) -> int:
205+
def print_file(
206+
self, innohassle_user_id: USER_AUTH, filename: str, printer: Printer, options: PrintingOptions
207+
) -> int:
176208
options_dict = options.model_dump(by_alias=True, exclude_none=True)
177-
return self.server.printFile(printer.cups_name, file_name, title, options=options_dict)
209+
job_id = self.server.printFile(
210+
printer.cups_name, self.get_tempfile_path(innohassle_user_id, filename), "job", options=options_dict
211+
)
212+
self.remove_tempfile(innohassle_user_id, filename)
213+
return job_id
178214

179215
def get_job_status(self, job_id: int) -> JobAttributes:
180216
attributes = self.server.getJobAttributes(

src/modules/printing/routes.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
import os
32
import tempfile
43
from pathlib import Path
54
from typing import Any
@@ -18,7 +17,6 @@
1817
from src.modules.printing.repository import printing_repository
1918

2019
router = APIRouter(prefix="/print", tags=["Print"])
21-
tempfiles: dict[tuple[str, str], Any] = {}
2220

2321

2422
@router.get("/job_status")
@@ -33,11 +31,13 @@ async def job_status(job_id: int, _innohassle_user_id: USER_AUTH) -> JobAttribut
3331

3432
@router.get("/get_file", responses={404: {"description": "No such file"}})
3533
def get_file(filename: str, innohassle_user_id: USER_AUTH) -> FileResponse:
36-
if (innohassle_user_id, filename) in tempfiles:
37-
full_path = tempfiles[(innohassle_user_id, filename)].name
38-
return FileResponse(full_path, headers={"Content-Disposition": f"attachment; filename={filename}"})
34+
if (innohassle_user_id, filename) in printing_repository.tempfiles:
35+
return FileResponse(
36+
printing_repository.get_tempfile_path(innohassle_user_id, filename),
37+
headers={"Content-Disposition": f"attachment; filename={filename}"},
38+
)
3939
else:
40-
raise HTTPException(404, "No such file")
40+
raise HTTPException(404, "No such file. It was removed from our servers due to expiration")
4141

4242

4343
@router.get("/get_printers")
@@ -69,7 +69,7 @@ async def get_printer_status(printer_cups_name: str, _innohassle_user_id: USER_A
6969
@router.post("/prepare", responses={400: {"description": "Unsupported format"}})
7070
async def prepare_printing(file: UploadFile, innohassle_user_id: USER_AUTH) -> PreparePrintingResponse:
7171
"""
72-
Convert a file to pdf and return the path to the converted file
72+
Convert a file to PDF and return the path to the converted file
7373
"""
7474

7575
if not file.size:
@@ -80,7 +80,7 @@ async def prepare_printing(file: UploadFile, innohassle_user_id: USER_AUTH) -> P
8080
if ext == ".pdf":
8181
f = tempfile.NamedTemporaryFile(dir=settings.api.temp_dir, suffix=".pdf")
8282
f.write(await file.read())
83-
tempfiles[(innohassle_user_id, Path(f.name).name)] = f
83+
printing_repository.store_tempfile(innohassle_user_id, f)
8484
return PreparePrintingResponse(filename=Path(f.name).name, pages=len(PyPDF2.PdfReader(f).pages))
8585
elif ext in [".doc", ".docx", ".png", ".txt", ".jpg", ".md", ".bmp", ".xlsx", ".xls", ".odt", ".ods"]:
8686
with (
@@ -92,7 +92,7 @@ async def prepare_printing(file: UploadFile, innohassle_user_id: USER_AUTH) -> P
9292
# Run conversion in a background thread
9393
await asyncio.to_thread(converting_repository.any2pdf, in_f.name, out_f.name)
9494
in_f.close()
95-
tempfiles[(innohassle_user_id, Path(out_f.name).name)] = out_f
95+
printing_repository.store_tempfile(innohassle_user_id, out_f)
9696
return PreparePrintingResponse(filename=Path(out_f.name).name, pages=len(PyPDF2.PdfReader(out_f).pages))
9797
else:
9898
raise HTTPException(400, f"no support of the {ext} format")
@@ -110,18 +110,15 @@ async def actual_print(
110110
"""
111111
logger.info(f"Printing options: {printing_options}")
112112

113-
if (innohassle_user_id, filename) in tempfiles:
113+
if (innohassle_user_id, filename) in printing_repository.tempfiles:
114114
printer = printing_repository.get_printer(printer_cups_name)
115115
if not printer:
116116
raise HTTPException(400, "No such printer")
117-
full_path = tempfiles[(innohassle_user_id, filename)].name
118-
job_id = printing_repository.print_file(printer, full_path, "job", printing_options)
117+
job_id = printing_repository.print_file(innohassle_user_id, filename, printer, printing_options)
119118
logger.info(f"Job {job_id} has started")
120-
os.unlink(full_path)
121-
del tempfiles[(innohassle_user_id, filename)]
122119
return job_id
123120
else:
124-
raise HTTPException(404, "No such file")
121+
raise HTTPException(404, "No such file. It was removed from our servers due to expiration")
125122

126123

127124
@router.post("/cancel", responses={404: {"description": "No such file"}, 400: {"description": "No such printer"}})
@@ -132,12 +129,8 @@ async def cancel_printing(job_id: int, _innohassle_user_id: USER_AUTH) -> None:
132129

133130
@router.post("/cancel_preparation", responses={404: {"description": "No such file"}})
134131
async def cancel_preparation(filename: str, innohassle_user_id: USER_AUTH) -> None:
135-
if (innohassle_user_id, filename) in tempfiles:
136-
full_path = tempfiles[(innohassle_user_id, filename)].name
137-
os.unlink(full_path)
138-
del tempfiles[(innohassle_user_id, filename)]
139-
else:
140-
raise HTTPException(404, "No such file")
132+
if not printing_repository.remove_tempfile(innohassle_user_id, filename):
133+
raise HTTPException(404, "No such file. It was removed from our servers due to expiration")
141134

142135

143136
@router.post("/debug/getPrinterAttributes")

0 commit comments

Comments
 (0)