import 'dart:convert'; import 'dart:io'; import 'dart:async'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; import 'config/app_config.dart'; import 'util/vips.dart'; import 'domain/repositories/photo_repository.dart'; import 'domain/repositories/album_repository.dart'; final _config = AppConfig(); final vips = Vips(cacheDir: _config.cacheDir); final albumRepository = AlbumRepository(basePath: _config.dataDir); final photoRepository = PhotoRepository(_config.dataDir); Response jsonResponse(dynamic data) { return Response.ok( jsonEncode(data), headers: {'content-type': 'application/json'}, ); } /// 创建 API v1 版本的路由 Router createApiV1Router() { final router = Router() ..get('/', _rootHandler) ..get("/album", _listAlbumHandler) ..get("/album/", _getAlbumHandler) ..get("/album//photo", _getPhotosOfAlbumHandler) ..get('/photo/', _getPhotoHandler) ..get('/photo//file', _getPhotoFileHandler) ..get('/photo//preview', _getPhotoPreviewHandler) ..get('/echo/', _echoHandler); return router; } /// 创建主路由器,将 API v1 挂载到 /api/v1 路径下 Router createRouter() { final router = Router(); // 将 API v1 路由挂载到 /api/v1 路径下 router.mount('/api/v1', createApiV1Router().call); return router; } Response _rootHandler(Request req) { return Response.ok('Hello, World!\n'); } Response _echoHandler(Request request) { final message = request.params['message']; return Response.ok('$message\n'); } Future _listAlbumHandler(Request req) async { final albums = await albumRepository.getAllAlbums(); final albumList = albums .map( (a) => { 'id': a.id, 'name': a.name, 'createdAt': a.createdAt.toIso8601String(), 'updatedAt': a.updatedAt.toIso8601String(), }, ) .toList(); return jsonResponse(albumList); } Future _getAlbumHandler(Request req) async { final idParam = req.params['id']; if (idParam == null) { return Response.badRequest(body: 'Missing album id'); } final id = int.tryParse(idParam); if (id == null) { return Response.badRequest(body: 'Invalid album id: $idParam'); } final album = await albumRepository.getAlbumById(id); if (album == null) { return Response.notFound('Album not found'); } return jsonResponse({'id': album.id, 'name': album.name}); } Future _getPhotoHandler(Request req) async { final idParam = req.params['id']; if (idParam == null) { return Response.badRequest(body: 'Missing photo id'); } final id = int.tryParse(idParam); if (id == null) { return Response.badRequest(body: 'Invalid photo id: $idParam'); } final photo = await photoRepository.getPhotoById(id); if (photo == null) { return Response.notFound('Photo not found'); } return jsonResponse(photo); } Future _getPhotoFileHandler(Request req) async { final idParam = req.params['id']; if (idParam == null) { return Response.badRequest(body: 'Missing photo id'); } final id = int.tryParse(idParam); if (id == null) { return Response.badRequest(body: 'Invalid photo id: $idParam'); } final photo = await photoRepository.getPhotoById(id); if (photo == null) { return Response.notFound('Photo not found'); } final filePath = photo.filePath; final file = File(filePath); if (!await file.exists()) { return Response.notFound('File not found'); } // ✅ 修复:使用 StreamController 包装,确保在客户端断开时关闭文件流 final streamController = StreamController>(); final fileStream = file.openRead(); // 订阅文件流,并将数据转发到响应流 final subscription = fileStream.listen( streamController.add, onError: streamController.addError, onDone: streamController.close, cancelOnError: true, ); // 当客户端取消订阅时(断开连接),取消文件流订阅并关闭文件 streamController.onCancel = () { subscription.cancel(); return Future.value(); }; return Response.ok( streamController.stream, headers: { 'Content-Type': photo.mimeType, 'Content-Length': photo.fileSize.toString(), }, ); } Future _getPhotosOfAlbumHandler(Request req) async { final idParam = req.params['id']; if (idParam == null) { return Response.badRequest(body: 'Missing album id'); } final albumId = int.tryParse(idParam); if (albumId == null) { return Response.badRequest(body: 'Invalid album id: $idParam'); } final photos = await photoRepository.getPhotosByAlbumId(albumId); return jsonResponse(photos); } Future _getPhotoPreviewHandler(Request req) async { final idParam = req.params['id']; if (idParam == null) { return Response.badRequest(body: 'Missing photo id'); } final id = int.tryParse(idParam); if (id == null) { return Response.badRequest(body: 'Invalid photo id: $idParam'); } final photo = await photoRepository.getPhotoById(id); if (photo == null) { return Response.notFound('Photo not found'); } // 从查询参数获取宽度和高度 final w = int.tryParse(req.url.queryParameters['w'] ?? ''); final h = int.tryParse(req.url.queryParameters['h'] ?? ''); final file = await vips.generatePreview( photo.filePath, w: w, h: h, ); if (!await file.exists()) { return Response.notFound('File not found'); } // ✅ 修复:使用 StreamController 包装,确保在客户端断开时关闭文件流 final streamController = StreamController>(); final fileStream = file.openRead(); // 订阅文件流,并将数据转发到响应流 final subscription = fileStream.listen( streamController.add, onError: streamController.addError, onDone: streamController.close, cancelOnError: true, ); // 当客户端取消订阅时(断开连接),取消文件流订阅并关闭文件 streamController.onCancel = () { subscription.cancel(); return Future.value(); }; return Response.ok( streamController.stream, headers: { 'Content-Type': 'image/webp', 'Content-Length': file.statSync().size.toString(), }, ); }