Spaces:
Sleeping
Sleeping
Fadhili Sumaye commited on
Commit ·
5d2afe2
1
Parent(s): 573aa2a
Optimize project: add client-side image compression, URL validation, CORS, rate limiting, and DB pruning
Browse files
app/src/main/java/com/example/pestdetection/MainActivity.java
CHANGED
|
@@ -139,6 +139,10 @@ public class MainActivity extends AppCompatActivity {
|
|
| 139 |
private void saveServerUrlFromInput() {
|
| 140 |
String newUrl = etServerUrl.getText().toString().trim();
|
| 141 |
if (!newUrl.isEmpty()) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
ApiConfig.savePredictUrl(this, newUrl);
|
| 143 |
etServerUrl.setText(ApiConfig.getPredictUrl(this));
|
| 144 |
}
|
|
@@ -272,12 +276,12 @@ public class MainActivity extends AppCompatActivity {
|
|
| 272 |
saveServerUrlFromInput();
|
| 273 |
final String serverUrl = ApiConfig.getPredictUrl(this);
|
| 274 |
|
| 275 |
-
InputStream inputStream = getContentResolver().openInputStream(imageUri);
|
| 276 |
-
byte[] imageBytes = getBytes(inputStream);
|
| 277 |
-
|
| 278 |
resultText.setText("Analyzing...");
|
| 279 |
treatmentText.setText("Please wait...");
|
| 280 |
|
|
|
|
|
|
|
|
|
|
| 281 |
PestApiClient.getInstance().predict(serverUrl, imageBytes, new PestApiClient.PredictCallback() {
|
| 282 |
@Override
|
| 283 |
public void onSuccess(String pest, double confidence, String treatment) {
|
|
@@ -301,15 +305,41 @@ public class MainActivity extends AppCompatActivity {
|
|
| 301 |
}
|
| 302 |
}
|
| 303 |
|
| 304 |
-
private byte[]
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
}
|
| 315 |
}
|
|
|
|
| 139 |
private void saveServerUrlFromInput() {
|
| 140 |
String newUrl = etServerUrl.getText().toString().trim();
|
| 141 |
if (!newUrl.isEmpty()) {
|
| 142 |
+
if (!newUrl.startsWith("http://") && !newUrl.startsWith("https://")) {
|
| 143 |
+
newUrl = "http://" + newUrl;
|
| 144 |
+
}
|
| 145 |
+
newUrl = newUrl.replaceAll("\\s+", "");
|
| 146 |
ApiConfig.savePredictUrl(this, newUrl);
|
| 147 |
etServerUrl.setText(ApiConfig.getPredictUrl(this));
|
| 148 |
}
|
|
|
|
| 276 |
saveServerUrlFromInput();
|
| 277 |
final String serverUrl = ApiConfig.getPredictUrl(this);
|
| 278 |
|
|
|
|
|
|
|
|
|
|
| 279 |
resultText.setText("Analyzing...");
|
| 280 |
treatmentText.setText("Please wait...");
|
| 281 |
|
| 282 |
+
// Scaled and compressed image bytes on-device
|
| 283 |
+
byte[] imageBytes = getScaledAndCompressedImage(imageUri);
|
| 284 |
+
|
| 285 |
PestApiClient.getInstance().predict(serverUrl, imageBytes, new PestApiClient.PredictCallback() {
|
| 286 |
@Override
|
| 287 |
public void onSuccess(String pest, double confidence, String treatment) {
|
|
|
|
| 305 |
}
|
| 306 |
}
|
| 307 |
|
| 308 |
+
private byte[] getScaledAndCompressedImage(Uri uri) throws Exception {
|
| 309 |
+
InputStream input = getContentResolver().openInputStream(uri);
|
| 310 |
+
android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(input);
|
| 311 |
+
if (input != null) {
|
| 312 |
+
input.close();
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
if (bitmap == null) {
|
| 316 |
+
throw new Exception("Failed to decode image");
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
int maxSize = 1024;
|
| 320 |
+
int width = bitmap.getWidth();
|
| 321 |
+
int height = bitmap.getHeight();
|
| 322 |
|
| 323 |
+
if (width > maxSize || height > maxSize) {
|
| 324 |
+
float ratio = (float) width / (float) height;
|
| 325 |
+
if (ratio > 1) {
|
| 326 |
+
width = maxSize;
|
| 327 |
+
height = (int) (maxSize / ratio);
|
| 328 |
+
} else {
|
| 329 |
+
height = maxSize;
|
| 330 |
+
width = (int) (maxSize * ratio);
|
| 331 |
+
}
|
| 332 |
+
android.graphics.Bitmap resizedBitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, width, height, true);
|
| 333 |
+
if (resizedBitmap != bitmap) {
|
| 334 |
+
bitmap.recycle();
|
| 335 |
+
}
|
| 336 |
+
bitmap = resizedBitmap;
|
| 337 |
}
|
| 338 |
|
| 339 |
+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
| 340 |
+
bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 80, outputStream);
|
| 341 |
+
byte[] bytes = outputStream.toByteArray();
|
| 342 |
+
bitmap.recycle();
|
| 343 |
+
return bytes;
|
| 344 |
}
|
| 345 |
}
|
backend/app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Request
|
| 2 |
from fastapi.responses import PlainTextResponse
|
|
|
|
| 3 |
import os
|
| 4 |
from pathlib import Path
|
| 5 |
import threading
|
|
@@ -10,6 +11,7 @@ import json
|
|
| 10 |
import io
|
| 11 |
from PIL import Image
|
| 12 |
from typing import Optional
|
|
|
|
| 13 |
|
| 14 |
BASE_DIR = Path(__file__).resolve().parent
|
| 15 |
DB_FILE = BASE_DIR / "pest_detection.db"
|
|
@@ -32,6 +34,17 @@ def init_db():
|
|
| 32 |
)
|
| 33 |
""")
|
| 34 |
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
conn.close()
|
| 36 |
|
| 37 |
def log_audit(username: Optional[str], endpoint: str, status: str, details: str, ip_address: str):
|
|
@@ -51,6 +64,20 @@ init_db()
|
|
| 51 |
|
| 52 |
app = FastAPI(title="Pest Detection API")
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
model = None
|
| 55 |
HAS_YOLO = False
|
| 56 |
is_downloading = False
|
|
@@ -222,6 +249,15 @@ async def predict(
|
|
| 222 |
):
|
| 223 |
ip = request.client.host if request.client else "unknown"
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
if not image.filename:
|
| 226 |
log_audit("anonymous", "/predict", "failed", "No selected file", ip)
|
| 227 |
raise HTTPException(status_code=400, detail="No selected file")
|
|
|
|
| 1 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Request
|
| 2 |
from fastapi.responses import PlainTextResponse
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
| 6 |
import threading
|
|
|
|
| 11 |
import io
|
| 12 |
from PIL import Image
|
| 13 |
from typing import Optional
|
| 14 |
+
from collections import defaultdict
|
| 15 |
|
| 16 |
BASE_DIR = Path(__file__).resolve().parent
|
| 17 |
DB_FILE = BASE_DIR / "pest_detection.db"
|
|
|
|
| 34 |
)
|
| 35 |
""")
|
| 36 |
conn.commit()
|
| 37 |
+
|
| 38 |
+
# Prune audit logs older than 30 days
|
| 39 |
+
try:
|
| 40 |
+
cursor.execute("DELETE FROM audit_logs WHERE timestamp < datetime('now', '-30 days')")
|
| 41 |
+
pruned_count = cursor.rowcount
|
| 42 |
+
conn.commit()
|
| 43 |
+
if pruned_count > 0:
|
| 44 |
+
print(f"[DB] Successfully pruned {pruned_count} audit log entries older than 30 days.")
|
| 45 |
+
except Exception as e:
|
| 46 |
+
print(f"[DB] Warning: Failed to prune audit logs: {e}")
|
| 47 |
+
|
| 48 |
conn.close()
|
| 49 |
|
| 50 |
def log_audit(username: Optional[str], endpoint: str, status: str, details: str, ip_address: str):
|
|
|
|
| 64 |
|
| 65 |
app = FastAPI(title="Pest Detection API")
|
| 66 |
|
| 67 |
+
# Configure CORS Middleware
|
| 68 |
+
app.add_middleware(
|
| 69 |
+
CORSMiddleware,
|
| 70 |
+
allow_origins=["*"],
|
| 71 |
+
allow_credentials=True,
|
| 72 |
+
allow_methods=["*"],
|
| 73 |
+
allow_headers=["*"],
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# In-memory IP-based rate limiting
|
| 77 |
+
RATE_LIMIT_WINDOW = 60 # 1 minute
|
| 78 |
+
RATE_LIMIT_MAX_REQUESTS = 10
|
| 79 |
+
request_history = defaultdict(list)
|
| 80 |
+
|
| 81 |
model = None
|
| 82 |
HAS_YOLO = False
|
| 83 |
is_downloading = False
|
|
|
|
| 249 |
):
|
| 250 |
ip = request.client.host if request.client else "unknown"
|
| 251 |
|
| 252 |
+
# Rate limiting check (excludes development environments)
|
| 253 |
+
if ip not in ("127.0.0.1", "10.0.2.2", "localhost", "unknown"):
|
| 254 |
+
now = time.time()
|
| 255 |
+
request_history[ip] = [t for t in request_history[ip] if now - t < RATE_LIMIT_WINDOW]
|
| 256 |
+
if len(request_history[ip]) >= RATE_LIMIT_MAX_REQUESTS:
|
| 257 |
+
log_audit("anonymous", "/predict", "rate-limited", f"Rate limit exceeded (IP: {ip})", ip)
|
| 258 |
+
raise HTTPException(status_code=429, detail="Too many requests. Please try again later.")
|
| 259 |
+
request_history[ip].append(now)
|
| 260 |
+
|
| 261 |
if not image.filename:
|
| 262 |
log_audit("anonymous", "/predict", "failed", "No selected file", ip)
|
| 263 |
raise HTTPException(status_code=400, detail="No selected file")
|