Add Search API endpoint
This commit is contained in:
parent
f40d9fc470
commit
04abeec7b6
84
main.py
84
main.py
@ -1,6 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, Response, status
|
from fastapi import FastAPI, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@ -20,10 +21,31 @@ class Log(BaseModel):
|
|||||||
end: bool
|
end: bool
|
||||||
|
|
||||||
|
|
||||||
|
class SearchResult(BaseModel):
|
||||||
|
matches: list[int]
|
||||||
|
|
||||||
|
|
||||||
class Error(BaseModel):
|
class Error(BaseModel):
|
||||||
error: str
|
error: str
|
||||||
|
|
||||||
|
|
||||||
|
def handle_path(name: str) -> tuple[Path, Optional[JSONResponse]]:
|
||||||
|
path = data_path.joinpath(name)
|
||||||
|
|
||||||
|
# Prevent path traversal
|
||||||
|
if not path.is_relative_to(data_path):
|
||||||
|
return path, JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, content={"error": "Bad Request"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not path.is_file():
|
||||||
|
return path, JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, content={"error": "Log not found"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return path, None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", response_model=Files)
|
@app.get("/", response_model=Files)
|
||||||
def list_logs():
|
def list_logs():
|
||||||
return {"files": [path.name for path in data_path.glob("*.txt") if path.is_file()]}
|
return {"files": [path.name for path in data_path.glob("*.txt") if path.is_file()]}
|
||||||
@ -38,18 +60,9 @@ def list_logs():
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
def get_log(name: str, start: int = 0, size: int = 100):
|
def get_log(name: str, start: int = 0, size: int = 100):
|
||||||
path = data_path.joinpath(name)
|
path, resp = handle_path(name)
|
||||||
|
if resp:
|
||||||
# Prevent path traversal
|
return resp
|
||||||
if not path.is_relative_to(data_path):
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, content={"error": "Bad Request"}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not path.is_file():
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, content={"error": "Log not found"}
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
f.seek(start)
|
f.seek(start)
|
||||||
@ -64,3 +77,48 @@ def get_log(name: str, start: int = 0, size: int = 100):
|
|||||||
"content": content,
|
"content": content,
|
||||||
"end": size + start >= total,
|
"end": size + start >= total,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/log/{name}/search/",
|
||||||
|
response_model=SearchResult,
|
||||||
|
responses={
|
||||||
|
400: {"model": Error},
|
||||||
|
404: {"model": Error},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def search_log(name: str, query: str, start: int = 0):
|
||||||
|
path, resp = handle_path(name)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
buffer_size = max(len(query), 1024)
|
||||||
|
break_outer = False
|
||||||
|
with open(path) as f:
|
||||||
|
f.seek(start)
|
||||||
|
head = start
|
||||||
|
tail = start + 2 * buffer_size
|
||||||
|
buffer = f.read(tail)
|
||||||
|
while True:
|
||||||
|
offset = 0
|
||||||
|
while True:
|
||||||
|
index = buffer.find(query, offset)
|
||||||
|
if index != -1:
|
||||||
|
matches.append(head + index)
|
||||||
|
offset = index + 1
|
||||||
|
if len(matches) >= 100:
|
||||||
|
break_outer = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if break_outer:
|
||||||
|
break
|
||||||
|
|
||||||
|
data = f.read(buffer_size)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
buffer = buffer[-len(query):] + data
|
||||||
|
head = tail - len(query)
|
||||||
|
tail += len(data)
|
||||||
|
return {"matches": matches}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user