修复代码格式
This commit is contained in:
@@ -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)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,5 +37,4 @@ class AlbumRepository {
|
||||
final albums = await getAllAlbums();
|
||||
return albums.where((a) => a.id == id).firstOrNull;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -185,11 +185,7 @@ Future<Response> _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');
|
||||
|
||||
@@ -9,23 +9,19 @@ class Vips {
|
||||
Vips({this.vipsExecuteFile, required this.cacheDir});
|
||||
|
||||
/// 生成图片预览(WebP 格式)
|
||||
///
|
||||
///
|
||||
/// [imgPath] 原图路径
|
||||
/// [w] 预览图宽度(可选)
|
||||
/// [h] 预览图高度(可选)
|
||||
///
|
||||
///
|
||||
/// 如果同时指定 [w] 和 [h],图片将按比例缩放以适应指定尺寸
|
||||
Future<File> generatePreview(
|
||||
String imgPath, {
|
||||
int? w,
|
||||
int? h,
|
||||
}) async {
|
||||
Future<File> 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"
|
||||
|
||||
@@ -43,7 +43,9 @@ void main() {
|
||||
});
|
||||
|
||||
test('GET /api/v1/echo/<message> 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/<id> 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/<id>/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<String, dynamic>;
|
||||
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/<id>/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/<id>/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/<id>/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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
final photoId = photo['id'];
|
||||
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse('$host/api/v1/photo/$photoId/file'),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user