Last active
May 30, 2024 18:58
-
-
Save domodomodomo/1589c9fa418538f08f0dd8b9fdbf282b to your computer and use it in GitHub Desktop.
When a directory on the server is updated, notify the client's browser via WebSocket using watchdog.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
pip install fastapi uvicorn starlette watchdog | |
uvicorn detect_file_updates:app --reload | |
""" | |
import asyncio | |
import os | |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect | |
from fastapi.responses import HTMLResponse | |
from starlette.websockets import WebSocketState | |
from watchdog.events import FileSystemEventHandler | |
from watchdog.observers import Observer | |
TARGET_PATH = "/path/to/your/target/directory" | |
if not os.path.isdir(TARGET_PATH): | |
raise Exception("Target directory does not exist") | |
app = FastAPI() | |
html = """ | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>WebSocket Test</title> | |
</head> | |
<body> | |
<h1>WebSocket File Update Notification</h1> | |
<ul id="messages"> | |
</ul> | |
<script> | |
const ws = new WebSocket("ws://localhost:8000/ws"); | |
ws.onmessage = function(event) { | |
const messages = document.getElementById('messages') | |
const message = document.createElement('li') | |
const content = document.createTextNode(event.data) | |
message.appendChild(content) | |
messages.appendChild(message) | |
}; | |
window.onbeforeunload = function() { | |
ws.close(); | |
}; | |
</script> | |
</body> | |
</html> | |
""" | |
class FileChangeEventHandler(FileSystemEventHandler): | |
def __init__(self, websocket): | |
self._websocket = websocket | |
self._event_loop = asyncio.get_event_loop() | |
def on_modified(self, event): | |
if event.is_directory: | |
return | |
# | |
# Point 1 | |
# | |
# OK 1. | |
asyncio.run_coroutine_threadsafe( | |
self._websocket.send_text(event.src_path), | |
self._event_loop | |
) | |
# OK 2. | |
# self._event_loop.call_soon_threadsafe( | |
# self._event_loop.create_task, | |
# self._websocket.send_text(event.src_path) | |
# ) | |
# NG | |
# asyncio.create_task(self._websocket.send_text(event.src_path)) | |
# | |
# Point 2 | |
# | |
print(event) | |
# > [!NOTE] | |
# > More than one event may occur for a single file change, like below. | |
# > Please ask ChatGPT for the reasons and countermeasures. | |
# ``` | |
# INFO: connection open | |
# FileModifiedEvent(...) | |
# FileModifiedEvent(...) | |
# INFO: connection close | |
# ``` | |
@app.get("/") | |
async def index(): | |
return HTMLResponse(html) | |
@app.websocket("/ws") | |
async def ws(websocket: WebSocket): | |
await websocket.accept() | |
file_change_event_handler = FileChangeEventHandler(websocket) | |
observer = Observer() | |
observer.schedule(file_change_event_handler, TARGET_PATH, recursive=True) | |
observer.start() | |
try: | |
await websocket.receive_text() | |
except WebSocketDisconnect: | |
pass | |
finally: | |
observer.stop() | |
observer.join() | |
# | |
# Point 3 | |
# | |
# To avoid closing it twice, I don't understand why it happens. | |
if websocket.client_state != WebSocketState.DISCONNECTED: | |
await websocket.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WebSocket handles events with functions, whereas watchdog handles events with methods defined in classes. This might make understanding the code a bit more difficult.