|
1 | 1 | """Utility functions for the server.""" |
2 | 2 |
|
3 | | -import asyncio |
4 | | -import shutil |
5 | | -import time |
6 | | -from contextlib import asynccontextmanager, suppress |
7 | | -from pathlib import Path |
8 | | -from typing import AsyncGenerator |
9 | | - |
10 | | -from fastapi import FastAPI, Request |
| 3 | +from fastapi import Request |
11 | 4 | from fastapi.responses import Response |
12 | 5 | from slowapi import Limiter, _rate_limit_exceeded_handler |
13 | 6 | from slowapi.errors import RateLimitExceeded |
14 | 7 | from slowapi.util import get_remote_address |
15 | 8 |
|
16 | | -from gitingest.config import TMP_BASE_PATH |
17 | 9 | from gitingest.utils.logging_config import get_logger |
18 | | -from server.server_config import DELETE_REPO_AFTER |
19 | 10 |
|
20 | 11 | # Initialize logger for this module |
21 | 12 | logger = get_logger(__name__) |
@@ -52,118 +43,6 @@ async def rate_limit_exception_handler(request: Request, exc: Exception) -> Resp |
52 | 43 | raise exc |
53 | 44 |
|
54 | 45 |
|
55 | | -@asynccontextmanager |
56 | | -async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: |
57 | | - """Manage startup & graceful-shutdown tasks for the FastAPI app. |
58 | | -
|
59 | | - Returns |
60 | | - ------- |
61 | | - AsyncGenerator[None, None] |
62 | | - Yields control back to the FastAPI application while the background task runs. |
63 | | -
|
64 | | - """ |
65 | | - task = asyncio.create_task(_remove_old_repositories()) |
66 | | - |
67 | | - yield # app runs while the background task is alive |
68 | | - |
69 | | - task.cancel() # ask the worker to stop |
70 | | - with suppress(asyncio.CancelledError): |
71 | | - await task # swallow the cancellation signal |
72 | | - |
73 | | - |
74 | | -async def _remove_old_repositories( |
75 | | - base_path: Path = TMP_BASE_PATH, |
76 | | - scan_interval: int = 60, |
77 | | - delete_after: int = DELETE_REPO_AFTER, |
78 | | -) -> None: |
79 | | - """Periodically delete old repositories/directories. |
80 | | -
|
81 | | - Every ``scan_interval`` seconds the coroutine scans ``base_path`` and deletes directories older than |
82 | | - ``delete_after`` seconds. The repository URL is extracted from the first ``.txt`` file in each directory |
83 | | - and appended to ``history.txt``, assuming the filename format: "owner-repository.txt". Filesystem errors are |
84 | | - logged and the loop continues. |
85 | | -
|
86 | | - Parameters |
87 | | - ---------- |
88 | | - base_path : Path |
89 | | - The path to the base directory where repositories are stored (default: ``TMP_BASE_PATH``). |
90 | | - scan_interval : int |
91 | | - The number of seconds between scans (default: 60). |
92 | | - delete_after : int |
93 | | - The number of seconds after which a repository is considered old and will be deleted |
94 | | - (default: ``DELETE_REPO_AFTER``). |
95 | | -
|
96 | | - """ |
97 | | - while True: |
98 | | - if not base_path.exists(): |
99 | | - await asyncio.sleep(scan_interval) |
100 | | - continue |
101 | | - |
102 | | - now = time.time() |
103 | | - try: |
104 | | - for folder in base_path.iterdir(): |
105 | | - if now - folder.stat().st_ctime <= delete_after: # Not old enough |
106 | | - continue |
107 | | - |
108 | | - await _process_folder(folder) |
109 | | - |
110 | | - except (OSError, PermissionError): |
111 | | - logger.exception("Error in repository cleanup", extra={"base_path": str(base_path)}) |
112 | | - |
113 | | - await asyncio.sleep(scan_interval) |
114 | | - |
115 | | - |
116 | | -async def _process_folder(folder: Path) -> None: |
117 | | - """Append the repo URL (if discoverable) to ``history.txt`` and delete ``folder``. |
118 | | -
|
119 | | - Parameters |
120 | | - ---------- |
121 | | - folder : Path |
122 | | - The path to the folder to be processed. |
123 | | -
|
124 | | - """ |
125 | | - history_file = Path("history.txt") |
126 | | - loop = asyncio.get_running_loop() |
127 | | - |
128 | | - try: |
129 | | - first_txt_file = next(folder.glob("*.txt")) |
130 | | - except StopIteration: # No .txt file found |
131 | | - return |
132 | | - |
133 | | - # Append owner/repo to history.txt |
134 | | - try: |
135 | | - filename = first_txt_file.stem # "owner-repo" |
136 | | - if "-" in filename: |
137 | | - owner, repo = filename.split("-", 1) |
138 | | - repo_url = f"{owner}/{repo}" |
139 | | - await loop.run_in_executor(None, _append_line, history_file, repo_url) |
140 | | - except (OSError, PermissionError): |
141 | | - logger.exception("Error logging repository URL", extra={"folder": str(folder)}) |
142 | | - |
143 | | - # Delete the cloned repo |
144 | | - try: |
145 | | - await loop.run_in_executor(None, shutil.rmtree, folder) |
146 | | - except PermissionError: |
147 | | - logger.exception("No permission to delete folder", extra={"folder": str(folder)}) |
148 | | - except OSError: |
149 | | - logger.exception("Could not delete folder", extra={"folder": str(folder)}) |
150 | | - |
151 | | - |
152 | | -def _append_line(path: Path, line: str) -> None: |
153 | | - """Append a line to a file. |
154 | | -
|
155 | | - Parameters |
156 | | - ---------- |
157 | | - path : Path |
158 | | - The path to the file to append the line to. |
159 | | - line : str |
160 | | - The line to append to the file. |
161 | | -
|
162 | | - """ |
163 | | - with path.open("a", encoding="utf-8") as fp: |
164 | | - fp.write(f"{line}\n") |
165 | | - |
166 | | - |
167 | 46 | ## Color printing utility |
168 | 47 | class Colors: |
169 | 48 | """ANSI color codes.""" |
|
0 commit comments