-
-
Notifications
You must be signed in to change notification settings - Fork 142
Fix: Prevent accidental deletion of user album files in camera & screen capabilities #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import 'dart:convert'; | ||
| import 'dart:io'; | ||
| import 'dart:math'; | ||
| import 'dart:ui' as ui; | ||
| import 'package:camera/camera.dart'; | ||
| import 'package:permission_handler/permission_handler.dart'; | ||
|
|
@@ -8,6 +9,12 @@ import 'capability_handler.dart'; | |
|
|
||
| class CameraCapability extends CapabilityHandler { | ||
| List<CameraDescription>? _cameras; | ||
|
|
||
| // Record the camera file generated during this session (unique ID+path) | ||
| // Ensure that only files created by oneself are deleted during deletion to avoid accidentally deleting user albums | ||
| /**Fixed since March 22, 2026 | ||
| submitter:wuchenxiuwu */ | ||
| final List<Map<String, String>> _cameraFiles = []; | ||
|
|
||
| @override | ||
| String get name => 'camera'; | ||
|
|
@@ -83,75 +90,121 @@ class CameraCapability extends CapabilityHandler { | |
| } | ||
| } | ||
|
|
||
| // Generate a unique ID (timestamp+random number) | ||
| String _generateId() { | ||
| final now = DateTime.now().millisecondsSinceEpoch; | ||
| final random = Random().nextInt(1000000); | ||
| return '${now}_$random'; | ||
| } | ||
|
|
||
| Future<NodeFrame> _snap(Map<String, dynamic> params) async { | ||
| CameraController? controller; | ||
| String? fileId; | ||
| try { | ||
| final facing = params['facing'] as String?; | ||
| controller = await _createController(facing: facing); | ||
|
|
||
| // Brief settle time for auto-exposure/focus | ||
| await Future.delayed(const Duration(milliseconds: 500)); | ||
|
|
||
| final file = await controller.takePicture(); | ||
| final bytes = await File(file.path).readAsBytes(); | ||
| final path = file.path; | ||
|
|
||
| // Record the files generated this time in the inventory | ||
| fileId = _generateId(); | ||
| _cameraFiles.add({'id': fileId, 'path': path}); | ||
|
|
||
| final bytes = await File(path).readAsBytes(); | ||
| final b64 = base64Encode(bytes); | ||
|
|
||
| // Get image dimensions | ||
| final codec = await ui.instantiateImageCodec(bytes); | ||
| final frame = await codec.getNextFrame(); | ||
| final width = frame.image.width; | ||
| final height = frame.image.height; | ||
| frame.image.dispose(); | ||
|
|
||
| // Clean up temp file | ||
| await File(file.path).delete().catchError((_) => File(file.path)); | ||
| // Only delete files when they are on the list to prevent accidental deletion of the album | ||
| final index = _cameraFiles.indexWhere((item) => item['id'] == fileId && item['path'] == path); | ||
| if (index != -1) { | ||
| _cameraFiles.removeAt(index); | ||
| await File(path).delete().catchError((_) => File(path)); | ||
wuchenxiuwu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
Comment on lines
+125
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't untrack temp files before cleanup succeeds. Both methods remove the Also applies to: 139-142, 171-176, 185-188 🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your careful examination. However, here I would like to explain why the current design is more realistic and that the changes suggested by AI may introduce unnecessary complexity.
To sum up, the current design has struck a balance between safety and simplicity, and the risk of edge situations is acceptable. Therefore, I chose to leave the existing implementation intact.😋 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The concern I raised is a real theoretical edge case, but given your testing and the constraints of the environment, the current design is a justifiable balance. I'll respect your decision to keep the existing implementation as-is. 👍
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your approval. Just to be clear, by " testing" I mean validating the design in practice, but decisions are not based on a specific environment. The fact that the file path is in an application's private directory, that deletion operations are almost always successful, and that user data is not affected even if it remains - these are objective facts at the system level, regardless of the test environment. The current design is a reasonable trade-off between security and simplicity, which I believe applies to most real-world scenarios. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your understanding. I'm glad we can agree. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| return NodeFrame.response('', payload: { | ||
| 'base64': b64, | ||
| 'format': 'jpg', | ||
| 'width': width, | ||
| 'height': height, | ||
| }); | ||
| } catch (e) { | ||
| //Clean up records in the inventory when errors occur to avoid residue | ||
| if (fileId != null) { | ||
| _cameraFiles.removeWhere((item) => item['id'] == fileId); | ||
| } | ||
| return NodeFrame.response('', error: { | ||
| 'code': 'CAMERA_ERROR', | ||
| 'message': '$e', | ||
| }); | ||
| } finally { | ||
| // Always release the camera | ||
| await controller?.dispose(); | ||
| } | ||
| } | ||
|
|
||
| Future<NodeFrame> _clip(Map<String, dynamic> params) async { | ||
| CameraController? controller; | ||
| String? fileId; | ||
| try { | ||
| final durationMs = params['durationMs'] as int? ?? 5000; | ||
| final facing = params['facing'] as String?; | ||
| controller = await _createController(facing: facing); | ||
| await controller.startVideoRecording(); | ||
| await Future.delayed(Duration(milliseconds: durationMs)); | ||
| final file = await controller.stopVideoRecording(); | ||
| final bytes = await File(file.path).readAsBytes(); | ||
| final path = file.path; | ||
|
|
||
| //Record the files generated this time in the inventory | ||
| fileId = _generateId(); | ||
| _cameraFiles.add({'id': fileId, 'path': path}); | ||
|
|
||
| final bytes = await File(path).readAsBytes(); | ||
| final b64 = base64Encode(bytes); | ||
| await File(file.path).delete().catchError((_) => File(file.path)); | ||
|
|
||
| //Only delete files when they are on the list to prevent accidental deletion of user albums | ||
| final index = _cameraFiles.indexWhere((item) => item['id'] == fileId && item['path'] == path); | ||
| if (index != -1) { | ||
| _cameraFiles.removeAt(index); | ||
| await File(path).delete().catchError((_) => File(path)); | ||
| } | ||
|
|
||
| return NodeFrame.response('', payload: { | ||
| 'base64': b64, | ||
| 'format': 'mp4', | ||
| 'durationMs': durationMs, | ||
| 'hasAudio': false, | ||
| }); | ||
| } catch (e) { | ||
| //Clean up records in the inventory when errors occur to avoid residue | ||
| if (fileId != null) { | ||
| _cameraFiles.removeWhere((item) => item['id'] == fileId); | ||
| } | ||
| return NodeFrame.response('', error: { | ||
| 'code': 'CAMERA_ERROR', | ||
| 'message': '$e', | ||
| }); | ||
| } finally { | ||
| // Always release the camera | ||
| await controller?.dispose(); | ||
| } | ||
| } | ||
|
|
||
| //Clean up all undeleted temporary files when the application exits | ||
| void dispose() { | ||
| // No persistent controller to clean up anymore | ||
| for (final item in _cameraFiles) { | ||
| final path = item['path']; | ||
| if (path != null) { | ||
| try { | ||
| File(path).deleteSync(); | ||
| } catch (_) {} | ||
| } | ||
| } | ||
| _cameraFiles.clear(); | ||
|
Comment on lines
+202
to
+212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coordinate
🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The condition for triggering OK will be very strict. Do not use theoretical knowledge to make random changes, as it may cause unnecessary confusion. It will only appear when the user just triggers the photo/video and immediately exits the application. Even if it happens, it's just a residual annoyance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged, thanks for the review and understanding. I'll keep the edge case in mind for future iterations.😊 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.