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[] getBytes(InputStream inputStream) throws Exception {
305
- ByteArrayOutputStream buffer = new ByteArrayOutputStream();
306
- int nRead;
307
- byte[] data = new byte[16384];
 
 
 
 
 
 
 
 
 
 
308
 
309
- while ((nRead = inputStream.read(data)) != -1) {
310
- buffer.write(data, 0, nRead);
 
 
 
 
 
 
 
 
 
 
 
 
311
  }
312
 
313
- return buffer.toByteArray();
 
 
 
 
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")