|
@@ -14,9 +14,43 @@ using System.Drawing;
|
|
|
using HttpMultipartParser;
|
|
|
using System.Net.NetworkInformation;
|
|
|
using System.Net.Sockets;
|
|
|
+using Newtonsoft.Json;
|
|
|
+using System.Text.RegularExpressions;
|
|
|
|
|
|
namespace Bird_tool
|
|
|
{
|
|
|
+ // 统一响应格式
|
|
|
+ public class ApiResponse
|
|
|
+ {
|
|
|
+ public int code { get; set; }
|
|
|
+ public string msg { get; set; }
|
|
|
+ public object data { get; set; }
|
|
|
+
|
|
|
+ public ApiResponse(int code, string msg, object data = null)
|
|
|
+ {
|
|
|
+ this.code = code;
|
|
|
+ this.msg = msg;
|
|
|
+ this.data = data;
|
|
|
+ }
|
|
|
+
|
|
|
+ public string ToJson()
|
|
|
+ {
|
|
|
+ return JsonConvert.SerializeObject(this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 图像处理结果
|
|
|
+ public class ImageProcessingResult
|
|
|
+ {
|
|
|
+ public string original_name { get; set; } // 原始文件名
|
|
|
+ public string file_name { get; set; } // 保存后的文件名
|
|
|
+ public double score { get; set; } // 清晰度分数
|
|
|
+ public string device_id { get; set; } // 设备ID
|
|
|
+ public string timestamp { get; set; } // 时间戳
|
|
|
+ public string save_path { get; set; } // 保存路径
|
|
|
+ public string image_url { get; set; } // 图像访问URL
|
|
|
+ public string msg { get; set; } // 错误消息
|
|
|
+ }
|
|
|
|
|
|
|
|
|
class ImageSharpnessServer
|
|
@@ -31,15 +65,20 @@ namespace Bird_tool
|
|
|
private readonly string _ip;
|
|
|
private bool _isRunning;
|
|
|
|
|
|
+ private readonly string _imageStoragePath = "upload";
|
|
|
+ private readonly object _fileLock = new object();
|
|
|
+
|
|
|
public ImageSharpnessServer(string ip, int port = 8080)
|
|
|
{
|
|
|
_ip = ip;
|
|
|
_port = port;
|
|
|
-
|
|
|
+
|
|
|
_listener = new HttpListener();
|
|
|
- string urlPrefix = $"http://*:{_port}/";
|
|
|
+ string urlPrefix = $"http://+:{_port}/";
|
|
|
OnLog?.Invoke($"Server started on {urlPrefix}", LogLevel.info);
|
|
|
_listener.Prefixes.Add(urlPrefix);
|
|
|
+
|
|
|
+ Directory.CreateDirectory(_imageStoragePath);
|
|
|
}
|
|
|
|
|
|
public bool Start()
|
|
@@ -52,6 +91,12 @@ namespace Bird_tool
|
|
|
ThreadPool.QueueUserWorkItem(Listen);
|
|
|
return true;
|
|
|
}
|
|
|
+ // 端口占用
|
|
|
+ catch (HttpListenerException ex) when (ex.ErrorCode == 32)
|
|
|
+ {
|
|
|
+ OnLog?.Invoke("端口被占用, 请修改配置文件后重新启动", LogLevel.error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
catch (HttpListenerException ex) when (ex.ErrorCode == 5) // 拒绝访问
|
|
|
{
|
|
|
if (_ip == "+" || _ip == "*")
|
|
@@ -72,14 +117,12 @@ namespace Bird_tool
|
|
|
OnLog?.Invoke($"无法启动服务器: {innerEx.Message}", LogLevel.info);
|
|
|
OnLogShow?.Invoke("无法启动服务器, 请尝试使用管理员模式启动");
|
|
|
return false;
|
|
|
- throw;
|
|
|
}
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
OnLogShow?.Invoke("无法注册服务, 修改配置文件后再次启动");
|
|
|
return false;
|
|
|
- throw;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -96,9 +139,15 @@ namespace Bird_tool
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
+ OnLog?.Invoke("listen to", LogLevel.info);
|
|
|
var context = _listener.GetContext();
|
|
|
ThreadPool.QueueUserWorkItem(ProcessRequest, context);
|
|
|
}
|
|
|
+ catch (HttpListenerException _ex) when (!_isRunning)
|
|
|
+ {
|
|
|
+ // 正常停止时忽略
|
|
|
+ OnLog?.Invoke("listen exit", LogLevel.info);
|
|
|
+ }
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
if (_isRunning) OnLog?.Invoke($"监听端口失败 Error: {ex.Message}", LogLevel.info);
|
|
@@ -180,40 +229,180 @@ namespace Bird_tool
|
|
|
return ipAddresses;
|
|
|
}
|
|
|
|
|
|
+ private string SanitizeFilename(string input)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(input))
|
|
|
+ return "unknown";
|
|
|
+
|
|
|
+ // 定义非法字符集合(包括Windows和Unix文件系统非法字符)
|
|
|
+ char[] invalidChars = Path.GetInvalidFileNameChars();
|
|
|
+
|
|
|
+ // 替换非法字符为下划线
|
|
|
+ string sanitized = new string(input.Select(c =>
|
|
|
+ invalidChars.Contains(c) ? '-' : c).ToArray());
|
|
|
+
|
|
|
+ // 移除连续的下划线
|
|
|
+ sanitized = Regex.Replace(sanitized, @"_{2,}", "_");
|
|
|
+
|
|
|
+ // 截断过长的文件名(保留前100个字符)
|
|
|
+ return sanitized.Length > 100 ? sanitized.Substring(0, 100) : sanitized;
|
|
|
+ }
|
|
|
+
|
|
|
private void ProcessRequest(object state)
|
|
|
{
|
|
|
var context = (HttpListenerContext)state;
|
|
|
try
|
|
|
{
|
|
|
- switch (context.Request.HttpMethod)
|
|
|
+ OnLog?.Invoke($"{context.Request.HttpMethod} query {context.Request.Url.AbsolutePath}", LogLevel.info);
|
|
|
+ var path = context.Request.Url.AbsolutePath;
|
|
|
+ var method = context.Request.HttpMethod;
|
|
|
+ ApiResponse response = null;
|
|
|
+
|
|
|
+ Regex.Match(path, @"^/upload/([^/]+)/([^/]+)$");
|
|
|
+ var match = Regex.Match(path, @"^/upload/([^/]+)/([^/]+)$");
|
|
|
+ if (method == "POST" && (path == "/upload" || match.Success))
|
|
|
{
|
|
|
- case "POST":
|
|
|
- HandleImageUpload(context);
|
|
|
- break;
|
|
|
- case "GET":
|
|
|
+ HandleImageUpload(context);
|
|
|
+ }
|
|
|
+ else if (method == "GET")
|
|
|
+ {
|
|
|
+ if (path == "/api/latest")
|
|
|
+ {
|
|
|
+ HandleGetLatestImage(context);
|
|
|
+ }
|
|
|
+ else if (path == "/api/images")
|
|
|
+ {
|
|
|
+ HandleGetImageList(context);
|
|
|
+ }
|
|
|
+ else if (path == "/api/devices")
|
|
|
+ {
|
|
|
+ HandleGetDevices(context);
|
|
|
+ }
|
|
|
+ else if (path == "/api/image")
|
|
|
+ {
|
|
|
+ HandleGetImage(context); // 新增的图像下载接口
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
HandleGetRequest(context);
|
|
|
- break;
|
|
|
- default:
|
|
|
- SendResponse(context, 405, "Method Not Allowed");
|
|
|
- break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 使用统一的JSON错误响应
|
|
|
+ response = new ApiResponse(404, "Not Found");
|
|
|
+ SendResponse(context, 404, response.ToJson(), "application/json");
|
|
|
}
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
OnLog?.Invoke($"Processing error: {ex}", LogLevel.info);
|
|
|
- SendResponse(context, 500, $"Internal Server Error: {ex.Message}");
|
|
|
+ var response = new ApiResponse(500, $"Internal Server Error: {ex.Message}");
|
|
|
+ SendResponse(context, 500, response.ToJson(), "application/json");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void HandleGetImage(HttpListenerContext context)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var query = context.Request.QueryString;
|
|
|
+ string fileName = query["file_name"];
|
|
|
+ ApiResponse response = null;
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(fileName))
|
|
|
+ {
|
|
|
+ response = new ApiResponse(400, $"Missing file_name parameter");
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全处理文件名,防止路径遍历攻击
|
|
|
+ string safeFileName = Path.GetFileName(fileName);
|
|
|
+ if (safeFileName != fileName)
|
|
|
+ {
|
|
|
+ response = new ApiResponse(400, $"Invalid file name");
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ string filePath = Path.Combine(_imageStoragePath, safeFileName);
|
|
|
+
|
|
|
+ if (!File.Exists(filePath))
|
|
|
+ {
|
|
|
+ response = new ApiResponse(404, $"Image not found");
|
|
|
+ SendResponse(context, 404, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取图像数据
|
|
|
+ byte[] imageData = File.ReadAllBytes(filePath);
|
|
|
+
|
|
|
+ // 设置正确的Content-Type
|
|
|
+ context.Response.ContentType = "image/jpeg";
|
|
|
+ context.Response.ContentLength64 = imageData.Length;
|
|
|
+ context.Response.OutputStream.Write(imageData, 0, imageData.Length);
|
|
|
+ context.Response.Close();
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ OnLog?.Invoke($"获取图片出错: {ex}", LogLevel.error);
|
|
|
+ var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
|
|
|
+ SendResponse(context, 500, response.ToJson(), "application/json");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void HandleImageUpload(HttpListenerContext context)
|
|
|
{
|
|
|
+ var path = context.Request.Url.AbsolutePath;
|
|
|
+ var query = context.Request.QueryString;
|
|
|
+ var match = Regex.Match(path, @"^/upload/([^/]+)/([^/]+)$");
|
|
|
+ string rawTimestamp = null,
|
|
|
+ rawDeviceId = null;
|
|
|
+ if (match.Success)
|
|
|
+ {
|
|
|
+ // 尝试从query中获取
|
|
|
+ rawDeviceId = match.Groups[1].Value;
|
|
|
+ rawTimestamp = match.Groups[2].Value;
|
|
|
+ OnLog?.Invoke($"get path parameter ", LogLevel.info);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ rawTimestamp = query["timestamp"];
|
|
|
+ rawDeviceId = query["device_id"];
|
|
|
+ OnLog?.Invoke($"get query param ", LogLevel.info);
|
|
|
+ }
|
|
|
+ var results = new List<ImageProcessingResult>();
|
|
|
+ double maxScore = -1; // 最高分初始值
|
|
|
+
|
|
|
try
|
|
|
{
|
|
|
+ // 获取查询参数
|
|
|
+
|
|
|
+ ApiResponse response = null;
|
|
|
+
|
|
|
+ // 处理空值情况
|
|
|
+ if (string.IsNullOrEmpty(rawTimestamp) || string.IsNullOrEmpty(rawDeviceId))
|
|
|
+ {
|
|
|
+ response = new ApiResponse(400, "Missing required query parameters: timestamp and device_id");
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全处理文件名
|
|
|
+ string timestamp = SanitizeFilename(rawTimestamp);
|
|
|
+ string deviceId = SanitizeFilename(rawDeviceId);
|
|
|
+
|
|
|
+ // 为设备创建专用目录
|
|
|
+ string deviceDirectory = Path.Combine(_imageStoragePath, deviceId);
|
|
|
+ Directory.CreateDirectory(deviceDirectory);
|
|
|
+
|
|
|
// 获取内容类型
|
|
|
string contentType = context.Request.ContentType;
|
|
|
if (string.IsNullOrEmpty(contentType) || !contentType.Contains("multipart/form-data"))
|
|
|
{
|
|
|
- SendResponse(context, 400, "Content-Type must be multipart/form-data");
|
|
|
+ response = new ApiResponse(400, "Content-Type must be multipart/form-data");
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
return;
|
|
|
}
|
|
|
|
|
@@ -221,55 +410,420 @@ namespace Bird_tool
|
|
|
string boundary = GetBoundaryFromContentType(contentType);
|
|
|
if (string.IsNullOrEmpty(boundary))
|
|
|
{
|
|
|
- SendResponse(context, 400, "Invalid boundary in Content-Type");
|
|
|
+ response = new ApiResponse(400, "Invalid boundary in Content-Type");
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 读取整个请求体
|
|
|
- byte[] requestBody;
|
|
|
- using (var ms = new MemoryStream())
|
|
|
+ // 使用MultipartFormDataParser解析多个文件
|
|
|
+ var parser = MultipartFormDataParser.Parse(context.Request.InputStream, boundary, Encoding.UTF8);
|
|
|
+
|
|
|
+ // 处理所有上传的文件
|
|
|
+ foreach (var file in parser.Files)
|
|
|
{
|
|
|
- context.Request.InputStream.CopyTo(ms);
|
|
|
- requestBody = ms.ToArray();
|
|
|
+ ImageProcessingResult result = new ImageProcessingResult
|
|
|
+ {
|
|
|
+ original_name = file.FileName,
|
|
|
+ device_id = deviceId,
|
|
|
+ timestamp = timestamp,
|
|
|
+ msg = ""
|
|
|
+ };
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 读取文件数据
|
|
|
+ byte[] imageData;
|
|
|
+ using (var memoryStream = new MemoryStream())
|
|
|
+ {
|
|
|
+ file.Data.CopyTo(memoryStream);
|
|
|
+ imageData = memoryStream.ToArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证图片数据
|
|
|
+ if (imageData.Length == 0)
|
|
|
+ {
|
|
|
+ result.msg = "Empty image data";
|
|
|
+ results.Add(result);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算清晰度
|
|
|
+ var (score, isValid) = CalculateSharpness(imageData);
|
|
|
+ if (!isValid)
|
|
|
+ {
|
|
|
+ result.msg = "Unsupported image format or corrupted data";
|
|
|
+ results.Add(result);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ OnLog?.Invoke($"Image score: {score} for device {deviceId} at {timestamp}", LogLevel.info);
|
|
|
+
|
|
|
+ // 更新最高分
|
|
|
+ if (score > maxScore)
|
|
|
+ {
|
|
|
+ maxScore = score;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存图片(文件名格式:设备ID_时间戳_唯一标识_分值.jpg)
|
|
|
+ string uniqueId = Guid.NewGuid().ToString("N").Substring(0, 8);
|
|
|
+ string fileName = $"{deviceId}_{timestamp}_{uniqueId}_{score:F2}.jpg";
|
|
|
+ string filePath = Path.Combine(deviceDirectory, fileName);
|
|
|
+
|
|
|
+ lock (_fileLock)
|
|
|
+ {
|
|
|
+ File.WriteAllBytes(filePath, imageData);
|
|
|
+ }
|
|
|
+
|
|
|
+ result.file_name = fileName;
|
|
|
+ result.score = score;
|
|
|
+ result.save_path = filePath;
|
|
|
+ result.image_url = $"/api/image?file_name={Uri.EscapeDataString(fileName)}";
|
|
|
+ results.Add(result);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ result.msg = $"Error processing image: {ex.Message}";
|
|
|
+ results.Add(result);
|
|
|
+ OnLog?.Invoke($"Error processing image {file.FileName}: {ex}", LogLevel.error);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // 解析multipart数据
|
|
|
- byte[] imageData = ExtractImageFromMultipart(requestBody, boundary);
|
|
|
- if (imageData == null)
|
|
|
+ // 构建响应数据
|
|
|
+ var responseData = new
|
|
|
{
|
|
|
- SendResponse(context, 400, "Failed to extract image from multipart data");
|
|
|
+ score = maxScore, // 最高分
|
|
|
+ device_id = deviceId,
|
|
|
+ timestamp = timestamp,
|
|
|
+ count = results.Count, // 图片数量
|
|
|
+ images = results.Select(r => new
|
|
|
+ {
|
|
|
+ image_url = r.image_url,
|
|
|
+ score = r.score,
|
|
|
+ msg = r.msg
|
|
|
+ }).ToList(),
|
|
|
+ msg = "评估结果测试" // 自定义评估结果消息
|
|
|
+ };
|
|
|
+
|
|
|
+ // 检查是否有成功处理的图像
|
|
|
+ if (results.All(r => r.score < 0))
|
|
|
+ {
|
|
|
+ response = new ApiResponse(400, "All images failed to process", responseData);
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 验证图片数据
|
|
|
- if (imageData.Length == 0)
|
|
|
+ // 返回结果 - 使用统一的JSON格式
|
|
|
+ response = new ApiResponse(200, "Images processed successfully", responseData);
|
|
|
+ SendResponse(context, 200, response.ToJson(), "application/json");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ // 错误情况下的响应数据
|
|
|
+ var responseData = new
|
|
|
{
|
|
|
- SendResponse(context, 400, "Empty image data");
|
|
|
+ score = -1,
|
|
|
+ device_id = rawDeviceId,
|
|
|
+ timestamp = rawTimestamp,
|
|
|
+ count = results.Count,
|
|
|
+ images = results.Select(r => new
|
|
|
+ {
|
|
|
+ image_url = r.image_url,
|
|
|
+ score = r.score,
|
|
|
+ msg = r.msg
|
|
|
+ }).ToList(),
|
|
|
+ msg = $"处理过程中发生错误: {ex.Message}"
|
|
|
+ };
|
|
|
+
|
|
|
+ OnLog?.Invoke($"Upload processing error: {ex}", LogLevel.error);
|
|
|
+ var response = new ApiResponse(500, $"Internal server error: {ex.Message}", responseData);
|
|
|
+ SendResponse(context, 500, response.ToJson(), "application/json");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void HandleGetLatestImage(HttpListenerContext context)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var query = context.Request.QueryString;
|
|
|
+ string rawDeviceId = query["device_id"];
|
|
|
+ string rawTimestamp = query["timestamp"];
|
|
|
+ ApiResponse response = null;
|
|
|
+
|
|
|
+ string deviceId = SanitizeFilename(rawDeviceId);
|
|
|
+ if (string.IsNullOrEmpty(deviceId))
|
|
|
+ {
|
|
|
+ response = new ApiResponse(400, "Missing required parameter: device_id");
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- Directory.CreateDirectory("upload");
|
|
|
- // 保存原始数据用于调试
|
|
|
- File.WriteAllBytes($"upload/upload_{DateTime.Now:yyyyMMddHHmmss}.jpg", imageData);
|
|
|
+ // 获取设备目录
|
|
|
+ string deviceDirectory = Path.Combine(_imageStoragePath, deviceId);
|
|
|
|
|
|
- // 计算清晰度
|
|
|
- var (score, isValid) = CalculateSharpness(imageData);
|
|
|
- if (!isValid)
|
|
|
+ if (!Directory.Exists(deviceDirectory))
|
|
|
{
|
|
|
- SendResponse(context, 400, "Unsupported image format or corrupted data");
|
|
|
+ response = new ApiResponse(404, $"No images found for device: {deviceId}");
|
|
|
+ SendResponse(context, 404, response.ToJson(), "application/json");
|
|
|
return;
|
|
|
}
|
|
|
- OnLog?.Invoke($"img score:{score}", LogLevel.info);
|
|
|
- // 返回结果
|
|
|
- string response = $"{{\"sharpness_score\": {score}}}";
|
|
|
- SendResponse(context, 200, response, "application/json");
|
|
|
+
|
|
|
+ // 获取设备的所有图片
|
|
|
+ var allImages = Directory.GetFiles(deviceDirectory, "*.jpg")
|
|
|
+ .Select(f => new
|
|
|
+ {
|
|
|
+ Path = f,
|
|
|
+ FileName = Path.GetFileName(f),
|
|
|
+ Created = File.GetCreationTime(f)
|
|
|
+ })
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ // 如果没有找到图片
|
|
|
+ if (!allImages.Any())
|
|
|
+ {
|
|
|
+ response = new ApiResponse(404, $"No images found for device: {deviceId}");
|
|
|
+ SendResponse(context, 404, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ string timestamp = !string.IsNullOrEmpty(rawTimestamp) ?
|
|
|
+ SanitizeFilename(rawTimestamp) : null;
|
|
|
+
|
|
|
+ // 根据查询条件过滤
|
|
|
+ if (!string.IsNullOrEmpty(timestamp))
|
|
|
+ {
|
|
|
+ allImages = allImages
|
|
|
+ .Where(img => img.FileName.Contains($"_{timestamp}_"))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ if (!allImages.Any())
|
|
|
+ {
|
|
|
+ response = new ApiResponse(404, $"No image found for device {deviceId} with timestamp {timestamp}");
|
|
|
+ SendResponse(context, 404, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取最新图片
|
|
|
+ var latestImage = allImages
|
|
|
+ .OrderByDescending(img => img.Created)
|
|
|
+ .First();
|
|
|
+
|
|
|
+ // 从文件名解析信息
|
|
|
+ var info = ParseImageInfo(latestImage.FileName);
|
|
|
+
|
|
|
+ // 构建响应 - 使用统一的JSON格式
|
|
|
+ var data = new
|
|
|
+ {
|
|
|
+ device_id = deviceId,
|
|
|
+ timestamp = info.Timestamp,
|
|
|
+ sharpness_score = info.Score,
|
|
|
+ image_url = $"/{_imageStoragePath}/{deviceId}/{latestImage.FileName}",
|
|
|
+ created_time = latestImage.Created.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
|
+ file_name = latestImage.FileName
|
|
|
+ };
|
|
|
+
|
|
|
+ response = new ApiResponse(200, "Latest image retrieved successfully", data);
|
|
|
+ SendResponse(context, 200, response.ToJson(), "application/json");
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
- OnLog?.Invoke($"Upload processing error: {ex}", LogLevel.error);
|
|
|
- SendResponse(context, 500, $"Internal server error: {ex.Message}");
|
|
|
+ OnLog?.Invoke($"Get latest image error: {ex}", LogLevel.error);
|
|
|
+ var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
|
|
|
+ SendResponse(context, 500, response.ToJson(), "application/json");
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private void HandleGetImageList(HttpListenerContext context)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var query = context.Request.QueryString;
|
|
|
+ string rawDeviceId = query["device_id"];
|
|
|
+ int page = int.TryParse(query["page"], out int p) ? p : 1;
|
|
|
+ int perPage = int.TryParse(query["per_page"], out int pp) ? pp : 10;
|
|
|
+ ApiResponse response = null;
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(rawDeviceId))
|
|
|
+ {
|
|
|
+ response = new ApiResponse(400, "Missing required parameter: device_id");
|
|
|
+ SendResponse(context, 400, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ string deviceId = SanitizeFilename(rawDeviceId);
|
|
|
+ string deviceDirectory = Path.Combine(_imageStoragePath, deviceId);
|
|
|
+
|
|
|
+ if (!Directory.Exists(deviceDirectory))
|
|
|
+ {
|
|
|
+ response = new ApiResponse(404, $"No images found for device: {deviceId}");
|
|
|
+ SendResponse(context, 404, response.ToJson(), "application/json");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取设备的所有图片
|
|
|
+ var allImages = Directory.GetFiles(deviceDirectory, "*.jpg")
|
|
|
+ .Select(f => new
|
|
|
+ {
|
|
|
+ Path = f,
|
|
|
+ FileName = Path.GetFileName(f),
|
|
|
+ Created = File.GetCreationTime(f)
|
|
|
+ })
|
|
|
+ .OrderByDescending(img => img.Created)
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ // 分页处理
|
|
|
+ int totalCount = allImages.Count;
|
|
|
+ int totalPages = (int)Math.Ceiling((double)totalCount / perPage);
|
|
|
+ page = Math.Max(1, Math.Min(page, totalPages));
|
|
|
+
|
|
|
+ var pagedImages = allImages
|
|
|
+ .Skip((page - 1) * perPage)
|
|
|
+ .Take(perPage)
|
|
|
+ .Select(img =>
|
|
|
+ {
|
|
|
+ var info = ParseImageInfo(img.FileName);
|
|
|
+ return new
|
|
|
+ {
|
|
|
+ device_id = deviceId,
|
|
|
+ timestamp = info.Timestamp,
|
|
|
+ score = info.Score,
|
|
|
+ image_url = $"/{_imageStoragePath}/{deviceId}/{img.FileName}",
|
|
|
+ created_time = img.Created.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
|
+ file_name = img.FileName
|
|
|
+ };
|
|
|
+ })
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ // 构建响应 - 使用统一的JSON格式
|
|
|
+ var data = new
|
|
|
+ {
|
|
|
+ total_count = totalCount,
|
|
|
+ total_pages = totalPages,
|
|
|
+ current_page = page,
|
|
|
+ per_page = perPage,
|
|
|
+ images = pagedImages
|
|
|
+ };
|
|
|
+
|
|
|
+ response = new ApiResponse(200, "Image list retrieved successfully", data);
|
|
|
+ SendResponse(context, 200, response.ToJson(), "application/json");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ OnLog?.Invoke($"Get image list error: {ex}", LogLevel.error);
|
|
|
+ var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
|
|
|
+ SendResponse(context, 500, response.ToJson(), "application/json");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void HandleGetDevices(HttpListenerContext context)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var query = context.Request.QueryString;
|
|
|
+ int page = int.TryParse(query["page"], out int p) ? p : 1;
|
|
|
+ int perPage = int.TryParse(query["per_page"], out int pp) ? pp : 10;
|
|
|
+ ApiResponse response = null;
|
|
|
+
|
|
|
+ // 获取所有设备目录
|
|
|
+ var deviceDirectories = Directory.GetDirectories(_imageStoragePath)
|
|
|
+ .Select(Path.GetFileName)
|
|
|
+ .Where(name => !string.IsNullOrEmpty(name))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ // 分页处理
|
|
|
+ int totalCount = deviceDirectories.Count;
|
|
|
+ int totalPages = (int)Math.Ceiling((double)totalCount / perPage);
|
|
|
+ page = Math.Max(1, Math.Min(page, totalPages));
|
|
|
+
|
|
|
+ var pagedDevices = deviceDirectories
|
|
|
+ .Skip((page - 1) * perPage)
|
|
|
+ .Take(perPage)
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ // 获取每个设备的图像数量
|
|
|
+ var deviceStats = pagedDevices.Select(deviceId =>
|
|
|
+ {
|
|
|
+ string deviceDir = Path.Combine(_imageStoragePath, deviceId);
|
|
|
+ int imageCount = Directory.Exists(deviceDir) ?
|
|
|
+ Directory.GetFiles(deviceDir, "*.jpg").Length : 0;
|
|
|
+
|
|
|
+ // 获取最新图像信息(如果有)
|
|
|
+ var latestImage = Directory.Exists(deviceDir) ?
|
|
|
+ Directory.GetFiles(deviceDir, "*.jpg")
|
|
|
+ .Select(f => new FileInfo(f))
|
|
|
+ .OrderByDescending(f => f.CreationTime)
|
|
|
+ .FirstOrDefault() : null;
|
|
|
+
|
|
|
+ return new
|
|
|
+ {
|
|
|
+ device_id = deviceId,
|
|
|
+ image_count = imageCount,
|
|
|
+ last_upload = latestImage?.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") ?? "N/A",
|
|
|
+ last_image = latestImage?.Name ?? "N/A"
|
|
|
+ };
|
|
|
+ }).ToList();
|
|
|
+
|
|
|
+ // 构建响应 - 使用统一的JSON格式
|
|
|
+ var data = new
|
|
|
+ {
|
|
|
+ total_count = totalCount,
|
|
|
+ total_pages = totalPages,
|
|
|
+ current_page = page,
|
|
|
+ per_page = perPage,
|
|
|
+ devices = deviceStats
|
|
|
+ };
|
|
|
+
|
|
|
+ response = new ApiResponse(200, "Device list retrieved successfully", data);
|
|
|
+ SendResponse(context, 200, response.ToJson(), "application/json");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ OnLog?.Invoke($"Get devices error: {ex}", LogLevel.error);
|
|
|
+ var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
|
|
|
+ SendResponse(context, 500, response.ToJson(), "application/json");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private (string DeviceId, string Timestamp, double Score) ParseImageInfo(string fileName)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 移除文件扩展名
|
|
|
+ string nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
|
|
+
|
|
|
+ // 查找最后一个下划线的位置(分值前)
|
|
|
+ int lastUnderscore = nameWithoutExt.LastIndexOf('_');
|
|
|
+ if (lastUnderscore == -1)
|
|
|
+ throw new FormatException($"Invalid file name format: {fileName}");
|
|
|
+
|
|
|
+ // 提取分值部分
|
|
|
+ string scorePart = nameWithoutExt.Substring(lastUnderscore + 1);
|
|
|
+ if (!double.TryParse(scorePart, out double score))
|
|
|
+ {
|
|
|
+ score = -1; // 无法解析时使用-1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 剩余部分是设备ID和时间戳的组合
|
|
|
+ string prefix = nameWithoutExt.Substring(0, lastUnderscore);
|
|
|
+
|
|
|
+ // 查找第一个下划线的位置(设备ID和时间戳之间)
|
|
|
+ int firstUnderscore = prefix.IndexOf('_');
|
|
|
+ if (firstUnderscore == -1)
|
|
|
+ throw new FormatException($"Invalid file name format: {fileName}");
|
|
|
+
|
|
|
+ string deviceId = prefix.Substring(0, firstUnderscore);
|
|
|
+ string timestamp = prefix.Substring(firstUnderscore + 1);
|
|
|
+
|
|
|
+ return (deviceId, timestamp, score);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ OnLog?.Invoke($"解析文件名失败: {fileName}, 错误: {ex.Message}", LogLevel.error);
|
|
|
+ return ("unknown", "unknown", -1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private string GetBoundaryFromContentType(string contentType)
|
|
|
{
|
|
|
// 查找boundary参数
|
|
@@ -477,4 +1031,4 @@ namespace Bird_tool
|
|
|
context.Response.Close();
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+}
|