添加图片预览api
This commit is contained in:
@@ -5,9 +5,11 @@ import 'dart:async';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
import 'util/vips.dart';
|
||||
import 'domain/repositories/photo_repository.dart';
|
||||
import 'domain/repositories/album_repository.dart';
|
||||
|
||||
final vips = Vips();
|
||||
final albumRepository = AlbumRepository(basePath: 'data');
|
||||
final photoRepository = PhotoRepository('data');
|
||||
|
||||
@@ -24,9 +26,10 @@ Router createApiV1Router() {
|
||||
..get('/', _rootHandler)
|
||||
..get("/album", _listAlbumHandler)
|
||||
..get("/album/<id>", _getAlbumHandler)
|
||||
..get("/album/<id>/photo", _getPhotosOfAlbumHander)
|
||||
..get("/album/<id>/photo", _getPhotosOfAlbumHandler)
|
||||
..get('/photo/<id>', _getPhotoHandler)
|
||||
..get('/photo/<id>/file', _getPhotoFileHandler)
|
||||
..get('/photo/<id>/preview', _getPhotoPreviewHandler)
|
||||
..get('/echo/<message>', _echoHandler);
|
||||
|
||||
return router;
|
||||
@@ -35,10 +38,10 @@ Router createApiV1Router() {
|
||||
/// 创建主路由器,将 API v1 挂载到 /api/v1 路径下
|
||||
Router createRouter() {
|
||||
final router = Router();
|
||||
|
||||
|
||||
// 将 API v1 路由挂载到 /api/v1 路径下
|
||||
router.mount('/api/v1', createApiV1Router().call);
|
||||
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -79,10 +82,7 @@ Future<Response> _getAlbumHandler(Request req) async {
|
||||
if (album == null) {
|
||||
return Response.notFound('Album not found');
|
||||
}
|
||||
return jsonResponse({
|
||||
'id': album.id,
|
||||
'name': album.name,
|
||||
});
|
||||
return jsonResponse({'id': album.id, 'name': album.name});
|
||||
}
|
||||
|
||||
Future<Response> _getPhotoHandler(Request req) async {
|
||||
@@ -127,7 +127,7 @@ Future<Response> _getPhotoFileHandler(Request req) async {
|
||||
// ✅ 修复:使用 StreamController 包装,确保在客户端断开时关闭文件流
|
||||
final streamController = StreamController<List<int>>();
|
||||
final fileStream = file.openRead();
|
||||
|
||||
|
||||
// 订阅文件流,并将数据转发到响应流
|
||||
final subscription = fileStream.listen(
|
||||
streamController.add,
|
||||
@@ -135,7 +135,7 @@ Future<Response> _getPhotoFileHandler(Request req) async {
|
||||
onDone: streamController.close,
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
|
||||
// 当客户端取消订阅时(断开连接),取消文件流订阅并关闭文件
|
||||
streamController.onCancel = () {
|
||||
subscription.cancel();
|
||||
@@ -151,7 +151,7 @@ Future<Response> _getPhotoFileHandler(Request req) async {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> _getPhotosOfAlbumHander(Request req) async {
|
||||
Future<Response> _getPhotosOfAlbumHandler(Request req) async {
|
||||
final idParam = req.params['id'];
|
||||
if (idParam == null) {
|
||||
return Response.badRequest(body: 'Missing album id');
|
||||
@@ -163,3 +163,51 @@ Future<Response> _getPhotosOfAlbumHander(Request req) async {
|
||||
final photos = await photoRepository.getPhotosByAlbumId(albumId);
|
||||
return jsonResponse(photos);
|
||||
}
|
||||
|
||||
Future<Response> _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 file = await vips.generatePreview(photo.filePath);
|
||||
|
||||
if (!await file.exists()) {
|
||||
return Response.notFound('File not found');
|
||||
}
|
||||
|
||||
// ✅ 修复:使用 StreamController 包装,确保在客户端断开时关闭文件流
|
||||
final streamController = StreamController<List<int>>();
|
||||
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': file.statSync().size.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
31
bin/util/vips.dart
Normal file
31
bin/util/vips.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class Vips {
|
||||
String? vipsExecuteFile;
|
||||
|
||||
Vips({this.vipsExecuteFile});
|
||||
|
||||
Future<File> generatePreview(String imgPath) async {
|
||||
final imgOut =
|
||||
"cache/preview/" + basename(imgPath).hashCode.toString() + ".webp";
|
||||
final outFile = File(imgOut);
|
||||
if (outFile.existsSync()) {
|
||||
return outFile;
|
||||
}
|
||||
final result = await Process.run("vips", [
|
||||
"webpsave",
|
||||
imgPath,
|
||||
imgOut,
|
||||
"--Q",
|
||||
"80",
|
||||
"--smart-subsample",
|
||||
"--strip",
|
||||
]);
|
||||
if (result.exitCode == 0) {
|
||||
return outFile;
|
||||
}
|
||||
throw Exception("vips image transcode failed!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user