diff --git a/bin/config/app_config.dart b/bin/config/app_config.dart index 30ec5db..cf4d4ab 100644 --- a/bin/config/app_config.dart +++ b/bin/config/app_config.dart @@ -1,14 +1,14 @@ import 'dart:io'; /// 应用配置管理 -/// +/// /// 支持通过环境变量配置数据目录和缓存目录: /// - `DATA_DIR`: 数据目录路径,默认为 `data` /// - `CACHE_DIR`: 缓存目录路径,默认为 `cache` class AppConfig { /// 数据目录路径 final String dataDir; - + /// 缓存目录路径 final String cacheDir; @@ -20,8 +20,8 @@ class AppConfig { /// 从环境变量创建配置 AppConfig._fromEnvironment() - : dataDir = Platform.environment['DATA_DIR'] ?? 'data', - cacheDir = Platform.environment['CACHE_DIR'] ?? 'cache'; + : dataDir = Platform.environment['DATA_DIR'] ?? 'data', + cacheDir = Platform.environment['CACHE_DIR'] ?? 'cache'; /// 获取相册预览缓存目录 String get previewCacheDir => '$cacheDir/preview'; @@ -34,4 +34,4 @@ class AppConfig { @override String toString() => 'AppConfig(dataDir: $dataDir, cacheDir: $cacheDir)'; -} \ No newline at end of file +} diff --git a/bin/domain/repositories/album_repository.dart b/bin/domain/repositories/album_repository.dart index 128495e..c648583 100644 --- a/bin/domain/repositories/album_repository.dart +++ b/bin/domain/repositories/album_repository.dart @@ -37,5 +37,4 @@ class AlbumRepository { final albums = await getAllAlbums(); return albums.where((a) => a.id == id).firstOrNull; } - } diff --git a/bin/domain/repositories/photo_repository.dart b/bin/domain/repositories/photo_repository.dart index 00ae672..e0f29c0 100644 --- a/bin/domain/repositories/photo_repository.dart +++ b/bin/domain/repositories/photo_repository.dart @@ -18,12 +18,13 @@ class PhotoRepository { Directory? albumDir; try { - albumDir = await dir - .list() - .where((f) => f is Directory) - .where((d) => p.basename(d.path).hashCode == id) - .first - as Directory; + albumDir = + await dir + .list() + .where((f) => f is Directory) + .where((d) => p.basename(d.path).hashCode == id) + .first + as Directory; } on StateError { return []; } @@ -159,9 +160,9 @@ class PhotoRepository { } final marker = data[offset + 1]; - + // SOF 标记 (Start Of Frame) - if (marker >= 0xC0 && marker <= 0xC3 || + if (marker >= 0xC0 && marker <= 0xC3 || marker >= 0xC5 && marker <= 0xC7 || marker >= 0xC9 && marker <= 0xCB || marker >= 0xCD && marker <= 0xCF) { @@ -191,25 +192,33 @@ class PhotoRepository { /// 解析 PNG 图片尺寸 (int, int)? _parsePngDimensions(Uint8List data) { - if (data.length < 24 || - data[0] != 0x89 || data[1] != 0x50 || - data[2] != 0x4E || data[3] != 0x47 || - data[4] != 0x0D || data[5] != 0x0A || - data[6] != 0x1A || data[7] != 0x0A) { + if (data.length < 24 || + data[0] != 0x89 || + data[1] != 0x50 || + data[2] != 0x4E || + data[3] != 0x47 || + data[4] != 0x0D || + data[5] != 0x0A || + data[6] != 0x1A || + data[7] != 0x0A) { return null; // 不是有效的 PNG } // IHDR chunk 在第 16-23 字节 - final width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19]; - final height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23]; + final width = + (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19]; + final height = + (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23]; return (width, height); } /// 解析 GIF 图片尺寸 (int, int)? _parseGifDimensions(Uint8List data) { - if (data.length < 10 || - data[0] != 0x47 || data[1] != 0x49 || - data[2] != 0x46 || data[3] != 0x38) { + if (data.length < 10 || + data[0] != 0x47 || + data[1] != 0x49 || + data[2] != 0x46 || + data[3] != 0x38) { return null; // 不是有效的 GIF } @@ -224,23 +233,29 @@ class PhotoRepository { return null; // 不是有效的 BMP } - final width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24); - final height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24); + final width = + data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24); + final height = + data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24); return (width, height.abs()); // BMP 高度可能是负数 } /// 解析 WebP 图片尺寸 (int, int)? _parseWebpDimensions(Uint8List data) { - if (data.length < 30 || - data[0] != 0x52 || data[1] != 0x49 || - data[2] != 0x46 || data[3] != 0x46 || - data[8] != 0x57 || data[9] != 0x45 || - data[10] != 0x42 || data[11] != 0x50) { + if (data.length < 30 || + data[0] != 0x52 || + data[1] != 0x49 || + data[2] != 0x46 || + data[3] != 0x46 || + data[8] != 0x57 || + data[9] != 0x45 || + data[10] != 0x42 || + data[11] != 0x50) { return null; // 不是有效的 WebP } final type = String.fromCharCodes(data.sublist(12, 16)); - + if (type == 'VP8 ') { // Lossy WebP if (data.length < 30) return null; @@ -254,7 +269,8 @@ class PhotoRepository { if (data.length < 29) return null; if (data[21] != 0x2F) return null; final width = 1 + ((data[22] | (data[23] << 8)) & 0x3FFF); - final height = 1 + (((data[23] >> 6) | (data[24] << 2) | (data[25] << 10)) & 0x3FFF); + final height = + 1 + (((data[23] >> 6) | (data[24] << 2) | (data[25] << 10)) & 0x3FFF); return (width, height); } else if (type == 'VP8X') { // Extended WebP @@ -263,7 +279,7 @@ class PhotoRepository { final height = 1 + (data[27] | (data[28] << 8) | (data[29] << 16)); return (width, height); } - + return null; } diff --git a/bin/router.dart b/bin/router.dart index 9b441fa..bc1cbb0 100644 --- a/bin/router.dart +++ b/bin/router.dart @@ -185,11 +185,7 @@ Future _getPhotoPreviewHandler(Request req) async { 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, - ); + final file = await vips.generatePreview(photo.filePath, w: w, h: h); if (!await file.exists()) { return Response.notFound('File not found'); diff --git a/bin/util/vips.dart b/bin/util/vips.dart index 43aa8f9..23335f7 100644 --- a/bin/util/vips.dart +++ b/bin/util/vips.dart @@ -9,23 +9,19 @@ class Vips { Vips({this.vipsExecuteFile, required this.cacheDir}); /// 生成图片预览(WebP 格式) - /// + /// /// [imgPath] 原图路径 /// [w] 预览图宽度(可选) /// [h] 预览图高度(可选) - /// + /// /// 如果同时指定 [w] 和 [h],图片将按比例缩放以适应指定尺寸 - Future generatePreview( - String imgPath, { - int? w, - int? h, - }) async { + Future generatePreview(String imgPath, {int? w, int? h}) async { // 生成缓存文件名:包含原图 hash 和尺寸信息 final baseName = basename(imgPath).hashCode.toString(); final sizeSuffix = _buildSizeSuffix(w, h); final imgOut = "$cacheDir/preview/$baseName$sizeSuffix.webp"; final outFile = File(imgOut); - + // 缓存命中:直接返回已存在的预览图 if (outFile.existsSync()) { return outFile; @@ -74,7 +70,7 @@ class Vips { } /// 构建尺寸后缀 - /// + /// /// 例如: /// - w=200, h=150 => "_200x150" /// - w=200, h=null => "_200x0" diff --git a/test/server_test.dart b/test/server_test.dart index bbc4210..7c8d232 100644 --- a/test/server_test.dart +++ b/test/server_test.dart @@ -43,7 +43,9 @@ void main() { }); test('GET /api/v1/echo/ with special characters', () async { - final response = await http.get(Uri.parse('$host/api/v1/echo/test%20message')); + final response = await http.get( + Uri.parse('$host/api/v1/echo/test%20message'), + ); expect(response.statusCode, 200); expect(response.body, contains('test')); }); @@ -63,7 +65,9 @@ void main() { }); test('GET /api/v1/album/ with invalid id returns 400', () async { - final response = await http.get(Uri.parse('$host/api/v1/album/invalid_id')); + final response = await http.get( + Uri.parse('$host/api/v1/album/invalid_id'), + ); expect(response.statusCode, 400); expect(response.body, contains('Invalid album id')); }); @@ -75,7 +79,9 @@ void main() { }); test('GET /api/v1/album//photo with invalid id returns 400', () async { - final response = await http.get(Uri.parse('$host/api/v1/album/invalid_id/photo')); + final response = await http.get( + Uri.parse('$host/api/v1/album/invalid_id/photo'), + ); expect(response.statusCode, 400); expect(response.body, contains('Invalid album id')); }); @@ -85,16 +91,18 @@ void main() { final listResponse = await http.get(Uri.parse('$host/api/v1/album')); expect(listResponse.statusCode, 200); final albums = jsonDecode(listResponse.body) as List; - + // Skip test if no albums exist if (albums.isEmpty) { return; } - + final album = albums.first as Map; final albumId = album['id']; - - final response = await http.get(Uri.parse('$host/api/v1/album/$albumId/photo')); + + final response = await http.get( + Uri.parse('$host/api/v1/album/$albumId/photo'), + ); expect(response.statusCode, 200); expect(response.headers['content-type'], contains('application/json')); final photos = jsonDecode(response.body) as List; @@ -122,41 +130,46 @@ void main() { expect(response.body, contains('Invalid photo id')); }); - test('GET /api/v1/photo//file with non-existent id returns 404', () async { - // Use a hash that won't match any file - final response = await http.get(Uri.parse('$host/api/v1/photo/12345/file')); - expect(response.statusCode, 404); - expect(response.body, contains('Photo not found')); - }); + test( + 'GET /api/v1/photo//file with non-existent id returns 404', + () async { + // Use a hash that won't match any file + final response = await http.get( + Uri.parse('$host/api/v1/photo/12345/file'), + ); + expect(response.statusCode, 404); + expect(response.body, contains('Photo not found')); + }, + ); test('GET /api/v1/photo//file with valid id returns file', () async { // First get a valid photo id from an album final albumResponse = await http.get(Uri.parse('$host/api/v1/album')); expect(albumResponse.statusCode, 200); final albums = jsonDecode(albumResponse.body) as List; - + // Skip test if no albums exist if (albums.isEmpty) { return; } - + final album = albums.first as Map; final albumId = album['id']; - + final photosResponse = await http.get( Uri.parse('$host/api/v1/album/$albumId/photo'), ); expect(photosResponse.statusCode, 200); final photos = jsonDecode(photosResponse.body) as List; - + // Skip test if no photos exist if (photos.isEmpty) { return; } - + final photo = photos.first as Map; final photoId = photo['id']; - + final response = await http.get( Uri.parse('$host/api/v1/photo/$photoId/file'), );