diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index d26f0d0..154e92b 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -15,6 +15,9 @@ on: jobs: test-and-build: runs-on: ubuntu-latest + env: + SFM_API_SECRET: ${{ secrets.SFM_API_SECRET }} + SFM_SERVER_SECRET: ${{ secrets.SFM_SERVER_SECRET }} steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index f7dd36c..afb5ec8 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,7 @@ profiling/ # swap files *.swp -/golang/storage/* \ No newline at end of file +/golang/storage/* + +# environment +*.env diff --git a/README.md b/README.md index e5eba94..a109eec 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Smart File Manager (SFM) is a digital housekeeper that uses metadata, natural la - [Demo 3 Video](https://drive.google.com/file/d/1k8QrDCzxmoQ0MvWkq4EAaTX5V6fvfJ1O/view?usp=sharing) +- [Demo 4 Video]() + ## 📑 Documentation
@@ -78,6 +80,22 @@ Smart File Manager (SFM) is a digital housekeeper that uses metadata, natural la
+
+📋 Demo 4 Documentation + +🔗 [Functional Requirements Document (SRS)](docs/documentation/demo_4/srs.md)
+🔗 [Architectural Specifications](docs/documentation/demo_4/Architecture.md)
+🔗 [Wireframe Documentation](docs/documentation/demo_4/wireframe.md)
+🔗 [Research Document](docs/documentation/demo_4/researchDoc.md)
+🔗 [User Manual](docs/documentation/demo_4/user_manual.md)
+🔗 [Coding Standards](docs/documentation/demo_4/coding_standards.md)
+🔗 [Technical Installation Manual](docs/documentation/demo_4/technical_installation.md)
+🔗 [User Manual](docs/documentation/demo_4/user_manual.md)
+🔗 [Testing Policy](docs/documentation/demo_4/testing_policy.md)
+🔗 [Non Functional Testing Report](docs/documentation/demo_4/non_functional_tests.md)
+ +
+ ## 📊 Project Board The GitHub Project boards are used to track current tasks allocated to each team member, facilitating effective communication, progress monitoring, and ensuring we meet project deadlines while maintaining accountability and transparency throughout the development process. diff --git a/app/lib/api.dart b/app/lib/api.dart index 731d629..eebe5cc 100644 --- a/app/lib/api.dart +++ b/app/lib/api.dart @@ -1,19 +1,61 @@ import 'package:app/models/file_model.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +import 'dart:io'; import 'models/file_tree_node.dart'; import 'models/startup_response.dart'; import 'models/duplicate_model.dart'; import 'models/stats_model.dart'; -const uri = "http://localhost:51000"; - class Api { + static String get _goApiSecret => + Platform.environment['SFM_API_SECRET'] ?? ''; + + static String get _baseUri { + try { + final executableDir = File(Platform.resolvedExecutable).parent; + final envFile = File('${executableDir.path}/server.env'); + + if (envFile.existsSync()) { + final contents = envFile.readAsStringSync(); + final lines = contents.split('\n'); + for (final line in lines) { + if (line.trim().startsWith('GO_PORT=')) { + final port = line.split('=')[1].trim(); + return "http://localhost:$port"; + } + } + } + + final currentDir = Directory.current; + final parentEnvFile = File('${currentDir.parent.path}/server.env'); + + if (parentEnvFile.existsSync()) { + final contents = parentEnvFile.readAsStringSync(); + final lines = contents.split('\n'); + for (final line in lines) { + if (line.trim().startsWith('GO_PORT=')) { + final port = line.split('=')[1].trim(); + return "http://localhost:$port"; + } + } + } + } catch (e) { + print('Error reading server.env: $e'); + } + return "http://localhost:51000"; + } + + static Map get _headers => { + 'Content-Type': 'application/json', + 'apiSecret': _goApiSecret, + }; //Call to load tree data static Future loadTreeData(String name) async { try { final response = await http.get( - Uri.parse("$uri/loadTreeData?name=$name"), + Uri.parse("$_baseUri/loadTreeData?name=$name"), + headers: _headers, ); if (response.statusCode == 200) { @@ -33,7 +75,10 @@ class Api { //Call to initialize app and get existing smart managers static Future startUp() async { try { - final response = await http.get(Uri.parse("$uri/startUp")); + final response = await http.get( + Uri.parse("$_baseUri/startUp"), + headers: _headers, + ); if (response.statusCode == 200) { return StartupResponse.fromJson( @@ -50,9 +95,17 @@ class Api { } //Call To Sort Tree structure - static Future sortManager(String name) async { + static Future sortManager( + String name, { + String? caseType, + }) async { try { - final response = await http.get(Uri.parse("$uri/sortTree?name=$name")); + String url = "$_baseUri/sortTree?name=$name"; + if (caseType != null && caseType.isNotEmpty) { + url += "&case=$caseType"; + } + + final response = await http.get(Uri.parse(url), headers: _headers); if (response.statusCode == 200) { return FileTreeNode.fromJson( @@ -72,10 +125,14 @@ class Api { static Future addSmartManager(String name, String path) async { try { final response = await http.post( - Uri.parse("$uri/addDirectory?name=$name&path=$path"), + Uri.parse("$_baseUri/addDirectory?name=$name&path=$path"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { - return true; + print(response.body); + final body = response.body.trim(); + print(body); + return body == "true"; } else { throw Exception( 'Failed to add SmartManager: HTTP ${response.statusCode}', @@ -92,11 +149,11 @@ class Api { static Future deleteSmartManager(String name) async { try { final response = await http.post( - Uri.parse("$uri/deleteManager?name=$name"), + Uri.parse("$_baseUri/deleteManager?name=$name"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { - print(response.body); return true; } else { throw Exception( @@ -114,7 +171,8 @@ class Api { static Future addTagToFile(String name, String path, String tag) async { try { final response = await http.post( - Uri.parse("$uri/addTag?name=$name&path=$path&tag=$tag"), + Uri.parse("$_baseUri/addTag?name=$name&path=$path&tag=$tag"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { @@ -137,7 +195,8 @@ class Api { ) async { try { final response = await http.post( - Uri.parse("$uri/removeTag?name=$name&path=$path&tag=$tag"), + Uri.parse("$_baseUri/removeTag?name=$name&path=$path&tag=$tag"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { @@ -156,7 +215,8 @@ class Api { static Future locking(String managerName, String path) async { try { final response = await http.post( - Uri.parse("$uri/lock?name=$managerName&path=$path"), + Uri.parse("$_baseUri/lock?name=$managerName&path=$path"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { return true; @@ -175,7 +235,8 @@ class Api { static Future unlocking(String managerName, String path) async { try { final response = await http.post( - Uri.parse("$uri/unlock?name=$managerName&path=$path"), + Uri.parse("$_baseUri/unlock?name=$managerName&path=$path"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { return true; @@ -195,7 +256,8 @@ class Api { static Future> loadDuplicates(String name) async { try { final response = await http.get( - Uri.parse("$uri/findDuplicateFiles?name=$name"), + Uri.parse("$_baseUri/findDuplicateFiles?name=$name"), + headers: _headers, ); if (response.statusCode == 200) { @@ -221,12 +283,13 @@ class Api { static Future searchGo(String name, String searchString) async { try { final response = await http.get( - Uri.parse("$uri/search?compositeName=$name&searchText=$searchString"), + Uri.parse( + "$_baseUri/search?compositeName=$name&searchText=$searchString", + ), + headers: _headers, ); if (response.statusCode == 200) { - print("Normal Search Used"); - return FileTreeNode.fromJson( jsonDecode(response.body) as Map, ); @@ -248,12 +311,12 @@ class Api { try { final response = await http.get( Uri.parse( - "$uri/keywordSearch?compositeName=$name&searchText=$searchString", + "$_baseUri/keywordSearch?compositeName=$name&searchText=$searchString", ), + headers: _headers, ); if (response.statusCode == 200) { - print("Advanced Search Used"); return FileTreeNode.fromJson( jsonDecode(response.body) as Map, ); @@ -271,7 +334,8 @@ class Api { static Future searchAdvancedReady(String name) async { try { final response = await http.get( - Uri.parse("$uri/isKeywordSearchReady?compositeName=$name"), + Uri.parse("$_baseUri/isKeywordSearchReady?compositeName=$name"), + headers: _headers, ); if (response.statusCode == 200) { final body = response.body.trim().toLowerCase(); @@ -292,7 +356,8 @@ class Api { ) async { try { final response = await http.post( - Uri.parse("$uri/deleteFile?name=$managerName&path=$path"), + Uri.parse("$_baseUri/deleteFile?name=$managerName&path=$path"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { return FileTreeNode.fromJson( @@ -314,8 +379,8 @@ class Api { ) async { try { final response = await http.post( - Uri.parse("$uri/bulkDeleteFiles?name=$managerName"), - headers: {'Content-Type': 'application/json'}, + Uri.parse("$_baseUri/bulkDeleteFiles?name=$managerName"), + headers: _headers, body: jsonPaths, ); if (response.statusCode == 200 || response.statusCode == 201) { @@ -339,8 +404,8 @@ class Api { ) async { try { final response = await http.post( - Uri.parse("$uri/bulkAddTag?name=$managerName"), - headers: {'Content-Type': 'application/json'}, + Uri.parse("$_baseUri/bulkAddTag?name=$managerName"), + headers: _headers, body: jsonPaths, ); if (response.statusCode == 200 || response.statusCode == 201) { @@ -364,8 +429,8 @@ class Api { ) async { try { final response = await http.post( - Uri.parse("$uri/bulkRemoveTag?name=$managerName"), - headers: {'Content-Type': 'application/json'}, + Uri.parse("$_baseUri/bulkRemoveTag?name=$managerName"), + headers: _headers, body: jsonPaths, ); if (response.statusCode == 200 || response.statusCode == 201) { @@ -389,7 +454,10 @@ class Api { ) async { try { final response = await http.get( - Uri.parse("$uri/returnType?name=$name&type=$type&umbrella=$umbrella"), + Uri.parse( + "$_baseUri/returnType?name=$name&type=$type&umbrella=$umbrella", + ), + headers: _headers, ); if (response.statusCode == 200) { @@ -413,7 +481,6 @@ class Api { .map((item) => FileModel.fromJson(item as Map)) .toList(); } catch (e) { - print('Error parsing JSON: $e'); return []; } } else { @@ -431,11 +498,12 @@ class Api { //loadStats static Future loadStatsData() async { try { - final response = await http.get(Uri.parse("$uri/returnStats")); + final response = await http.get( + Uri.parse("$_baseUri/returnStats"), + headers: _headers, + ); if (response.statusCode == 200) { - print(jsonDecode(response.body) as List); - return ManagersStatsResponse.fromJson( jsonDecode(response.body) as List, ); @@ -453,10 +521,10 @@ class Api { static Future moveDirectory(String managerName) async { try { final response = await http.post( - Uri.parse("$uri/moveDirectory?name=$managerName"), + Uri.parse("$_baseUri/moveDirectory?name=$managerName"), + headers: _headers, ); if (response.statusCode == 200 || response.statusCode == 201) { - print(response.body); if (response.body == "true") { return true; } else { diff --git a/app/lib/custom_widgets/file_item_widget.dart b/app/lib/custom_widgets/file_item_widget.dart index b477563..b4f8a9b 100644 --- a/app/lib/custom_widgets/file_item_widget.dart +++ b/app/lib/custom_widgets/file_item_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../models/file_tree_node.dart'; +import '../services/settings_service.dart'; class FileItemWidget extends StatefulWidget { final FileTreeNode item; @@ -19,6 +20,7 @@ class FileItemWidget extends StatefulWidget { class _FileItemWidgetState extends State { bool _isHovered = false; + Color _folderColor = const Color(0xffFFB400); // Default folder color // File categories with extensions static final Map> _categories = { @@ -54,6 +56,32 @@ class _FileItemWidgetState extends State { "Other": Icons.insert_drive_file, }; + @override + void initState() { + super.initState(); + _loadFolderColor(); + // Listen to settings changes + SettingsService.instance.addListener(_onSettingsChanged); + } + + @override + void dispose() { + SettingsService.instance.removeListener(_onSettingsChanged); + super.dispose(); + } + + void _onSettingsChanged() { + _loadFolderColor(); + } + + Future _loadFolderColor() async { + final colorPreset = await SettingsService.instance.getColorPreset(); + final colors = SettingsService.getPresetColors(colorPreset); + setState(() { + _folderColor = colors.isNotEmpty ? colors.first : const Color(0xffFFB400); + }); + } + @override Widget build(BuildContext context) { return MouseRegion( @@ -164,7 +192,7 @@ class _FileItemWidgetState extends State { } Color _getFileColor() { - if (widget.item.isFolder) return const Color(0xffFFB400); + if (widget.item.isFolder) return _folderColor; return const Color(0xff2563EB); } diff --git a/app/lib/custom_widgets/password_entry_dialog.dart b/app/lib/custom_widgets/password_entry_dialog.dart new file mode 100644 index 0000000..cb9e0e8 --- /dev/null +++ b/app/lib/custom_widgets/password_entry_dialog.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import '../constants.dart'; +import '../services/password_service.dart'; + +class PasswordEntryDialog extends StatefulWidget { + const PasswordEntryDialog({super.key}); + + @override + State createState() => _PasswordEntryDialogState(); +} + +class _PasswordEntryDialogState extends State { + final TextEditingController _passwordController = TextEditingController(); + bool _isPasswordVisible = false; + bool _isLoading = false; + String _errorMessage = ''; + int _attemptCount = 0; + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + Future _verifyPassword() async { + final password = _passwordController.text; + + setState(() { + _errorMessage = ''; + _isLoading = true; + }); + + if (password.isEmpty) { + setState(() { + _errorMessage = 'Please enter your password'; + _isLoading = false; + }); + return; + } + + final isValid = await PasswordService.instance.verifyPassword(password); + + setState(() { + _isLoading = false; + _attemptCount++; + }); + + if (isValid) { + if (mounted) { + Navigator.of(context).pop(true); + } + } else { + setState(() { + _errorMessage = 'Incorrect password. Please try again.'; + _passwordController.clear(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xff242424), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: kOutlineBorder), + ), + child: Container( + padding: const EdgeInsets.all(24), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.lock, + color: kprimaryColor, + size: 24, + ), + const SizedBox(width: 12), + const Text( + 'Enter Password', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'Please enter your password to access Smart File Manager.', + style: TextStyle( + color: Color(0xff9CA3AF), + fontSize: 14, + ), + ), + const SizedBox(height: 24), + + // Password field + const Text( + 'Password', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: kScaffoldColor, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: kOutlineBorder), + ), + child: TextField( + controller: _passwordController, + obscureText: !_isPasswordVisible, + style: const TextStyle(color: Colors.white), + autofocus: true, + decoration: InputDecoration( + hintText: 'Enter your password', + hintStyle: const TextStyle(color: Color(0xff9CA3AF)), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + suffixIcon: IconButton( + icon: Icon( + _isPasswordVisible ? Icons.visibility : Icons.visibility_off, + color: const Color(0xff9CA3AF), + size: 20, + ), + onPressed: () { + setState(() { + _isPasswordVisible = !_isPasswordVisible; + }); + }, + ), + ), + onSubmitted: (_) => _verifyPassword(), + ), + ), + + // Error message + if (_errorMessage.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red.shade400, + size: 16, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _errorMessage, + style: TextStyle( + color: Colors.red.shade400, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ], + + // Attempt counter (if multiple attempts) + if (_attemptCount > 1) ...[ + const SizedBox(height: 8), + Text( + 'Attempt $_attemptCount', + style: const TextStyle( + color: Color(0xff9CA3AF), + fontSize: 12, + ), + ), + ], + + const SizedBox(height: 24), + + // Buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: _isLoading ? null : () => Navigator.of(context).pop(false), + style: TextButton.styleFrom( + foregroundColor: const Color(0xff9CA3AF), + ), + child: const Text('Exit App'), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: _isLoading ? null : _verifyPassword, + style: ElevatedButton.styleFrom( + backgroundColor: kprimaryColor, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : const Text('Unlock'), + ), + ], + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/custom_widgets/password_setup_dialog.dart b/app/lib/custom_widgets/password_setup_dialog.dart new file mode 100644 index 0000000..3e73eaa --- /dev/null +++ b/app/lib/custom_widgets/password_setup_dialog.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import '../constants.dart'; +import '../services/password_service.dart'; + +class PasswordSetupDialog extends StatefulWidget { + const PasswordSetupDialog({super.key}); + + @override + State createState() => _PasswordSetupDialogState(); +} + +class _PasswordSetupDialogState extends State { + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmController = TextEditingController(); + bool _isPasswordVisible = false; + bool _isConfirmVisible = false; + bool _isLoading = false; + String _errorMessage = ''; + + @override + void dispose() { + _passwordController.dispose(); + _confirmController.dispose(); + super.dispose(); + } + + Future _setupPassword() async { + final password = _passwordController.text; + final confirm = _confirmController.text; + + setState(() { + _errorMessage = ''; + _isLoading = true; + }); + + // Validation + if (password.isEmpty) { + setState(() { + _errorMessage = 'Password cannot be empty'; + _isLoading = false; + }); + return; + } + + if (password.length < 4) { + setState(() { + _errorMessage = 'Password must be at least 4 characters'; + _isLoading = false; + }); + return; + } + + if (password != confirm) { + setState(() { + _errorMessage = 'Passwords do not match'; + _isLoading = false; + }); + return; + } + + // Set up password + final success = await PasswordService.instance.setupPassword(password); + + setState(() { + _isLoading = false; + }); + + if (success) { + if (mounted) { + Navigator.of(context).pop(true); + } + } else { + setState(() { + _errorMessage = 'Failed to set up password. Please try again.'; + }); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xff242424), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: kOutlineBorder), + ), + child: Container( + padding: const EdgeInsets.all(24), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + 'Set Up Password', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'Create a password to secure your Smart File Manager. You will need this password every time you open the application.', + style: TextStyle(color: Color(0xff9CA3AF), fontSize: 14), + ), + const SizedBox(height: 24), + + // Password field + const Text( + 'Password', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + _buildPasswordField( + controller: _passwordController, + isVisible: _isPasswordVisible, + onVisibilityToggle: () { + setState(() { + _isPasswordVisible = !_isPasswordVisible; + }); + }, + hintText: 'Enter your password', + ), + const SizedBox(height: 16), + + // Confirm password field + const Text( + 'Confirm Password', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + _buildPasswordField( + controller: _confirmController, + isVisible: _isConfirmVisible, + onVisibilityToggle: () { + setState(() { + _isConfirmVisible = !_isConfirmVisible; + }); + }, + hintText: 'Confirm your password', + ), + + // Error message + if (_errorMessage.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red.shade400, + size: 16, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _errorMessage, + style: TextStyle( + color: Colors.red.shade400, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ], + + const SizedBox(height: 24), + + // Buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: + _isLoading + ? null + : () => Navigator.of(context).pop(false), + style: TextButton.styleFrom( + foregroundColor: const Color(0xff9CA3AF), + ), + child: const Text('Cancel'), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: _isLoading ? null : _setupPassword, + style: ElevatedButton.styleFrom( + backgroundColor: kprimaryColor, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: + _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : const Text('Set Password'), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildPasswordField({ + required TextEditingController controller, + required bool isVisible, + required VoidCallback onVisibilityToggle, + required String hintText, + }) { + return Container( + decoration: BoxDecoration( + color: kScaffoldColor, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: kOutlineBorder), + ), + child: TextField( + controller: controller, + obscureText: !isVisible, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: hintText, + hintStyle: const TextStyle(color: Color(0xff9CA3AF)), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + suffixIcon: IconButton( + icon: Icon( + isVisible ? Icons.visibility : Icons.visibility_off, + color: const Color(0xff9CA3AF), + size: 20, + ), + onPressed: onVisibilityToggle, + ), + ), + onSubmitted: (_) => _setupPassword(), + ), + ); + } +} diff --git a/app/lib/main.dart b/app/lib/main.dart index f906e5a..d51c20a 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:app/navigation/shell.dart'; +import 'package:app/pages/auth_wrapper.dart'; import 'package:window_manager/window_manager.dart'; import 'package:tray_manager/tray_manager.dart'; import 'constants.dart'; @@ -89,7 +89,7 @@ class _MyAppState extends State with WindowListener, TrayListener { primaryColor: kprimaryColor, scaffoldBackgroundColor: kScaffoldColor, ), - home: const Shell(), + home: const AuthWrapper(), ); } } diff --git a/app/lib/navigation/main_navigation.dart b/app/lib/navigation/main_navigation.dart index 3c87886..bdc634f 100644 --- a/app/lib/navigation/main_navigation.dart +++ b/app/lib/navigation/main_navigation.dart @@ -490,7 +490,7 @@ class MainNavigationState extends State { ), ); } else { - // Remove manager API call failed + // Remove manager API call failed - directory already in use setState(() { _managers.removeWhere((m) => m.label == result.name); }); @@ -498,10 +498,10 @@ class MainNavigationState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Failed to create Smart Manager "${result.name}"', + 'Cannot create Smart Manager "${result.name}": Directory conflicts with an existing manager', ), backgroundColor: Colors.redAccent, - duration: Duration(seconds: 2), + duration: Duration(seconds: 3), ), ); } diff --git a/app/lib/navigation/shell.dart b/app/lib/navigation/shell.dart index 076a334..2a6ca69 100644 --- a/app/lib/navigation/shell.dart +++ b/app/lib/navigation/shell.dart @@ -9,6 +9,7 @@ import 'package:app/constants.dart'; import 'package:app/api.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:app/models/file_tree_node.dart'; +import 'package:app/services/settings_service.dart'; GlobalKey globalKey = GlobalKey(); GlobalKey mainNavigationKey = GlobalKey(); @@ -117,7 +118,10 @@ class _ShellState extends State { }); try { - final sortedData = await Api.sortManager(managerName); + // Get the user's preferred case setting + final userPreferredCase = await SettingsService.instance.getNamingConvention(); + + final sortedData = await Api.sortManager(managerName, caseType: userPreferredCase); setState(() { _pendingSorts[managerName] = false; @@ -285,15 +289,6 @@ class _ShellState extends State { icon: Icon(Icons.help_outline_rounded, color: kprimaryColor), ), ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 10, 0), - child: FilledButton.icon( - onPressed: () {}, - label: const Text("Login"), - style: FilledButton.styleFrom(backgroundColor: kScaffoldColor), - icon: const Icon(Icons.account_circle), - ), - ), ], shape: Border(bottom: BorderSide(color: Color(0xff3D3D3D), width: 1)), ), diff --git a/app/lib/pages/auth_wrapper.dart b/app/lib/pages/auth_wrapper.dart new file mode 100644 index 0000000..388c6fe --- /dev/null +++ b/app/lib/pages/auth_wrapper.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import '../services/password_service.dart'; +import '../custom_widgets/password_setup_dialog.dart'; +import '../custom_widgets/password_entry_dialog.dart'; +import '../navigation/shell.dart'; +import '../constants.dart'; +import 'dart:io'; + +class AuthWrapper extends StatefulWidget { + const AuthWrapper({super.key}); + + @override + State createState() => _AuthWrapperState(); +} + +class _AuthWrapperState extends State { + bool _isLoading = true; + bool _isAuthenticated = false; + + @override + void initState() { + super.initState(); + _checkAuthenticationStatus(); + } + + Future _checkAuthenticationStatus() async { + try { + final isPasswordSet = await PasswordService.instance.isPasswordSet(); + + if (!isPasswordSet) { + // First time setup - show password setup dialog + await _showPasswordSetupDialog(); + } else { + // Password exists - show password entry dialog + await _showPasswordEntryDialog(); + } + } catch (e) { + // Error occurred - show setup dialog as fallback + await _showPasswordSetupDialog(); + } + } + + Future _showPasswordSetupDialog() async { + while (!_isAuthenticated && mounted) { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const PasswordSetupDialog(), + ); + + if (result == true) { + setState(() { + _isAuthenticated = true; + _isLoading = false; + }); + } else { + // User cancelled setup - exit app + _exitApp(); + return; + } + } + } + + Future _showPasswordEntryDialog() async { + while (!_isAuthenticated && mounted) { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const PasswordEntryDialog(), + ); + + if (result == true) { + setState(() { + _isAuthenticated = true; + _isLoading = false; + }); + } else { + // User cancelled or chose to exit - exit app + _exitApp(); + return; + } + } + } + + void _exitApp() { + exit(0); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + backgroundColor: kScaffoldColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: kprimaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.security, + size: 40, + color: kprimaryColor, + ), + ), + const SizedBox(height: 24), + const Text( + 'Smart File Manager', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Initializing security...', + style: TextStyle( + color: Color(0xff9CA3AF), + fontSize: 14, + ), + ), + const SizedBox(height: 32), + const CircularProgressIndicator( + color: kprimaryColor, + strokeWidth: 2, + ), + ], + ), + ), + ); + } + + if (_isAuthenticated) { + return const Shell(); + } + + // Fallback - should not reach here + return Scaffold( + backgroundColor: kScaffoldColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: Colors.red, + ), + const SizedBox(height: 16), + const Text( + 'Authentication Error', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Unable to authenticate. Please restart the application.', + style: TextStyle( + color: Color(0xff9CA3AF), + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _exitApp, + style: ElevatedButton.styleFrom( + backgroundColor: kprimaryColor, + foregroundColor: Colors.black, + ), + child: const Text('Exit Application'), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/pages/manager_page_sub/graph_view_page.dart b/app/lib/pages/manager_page_sub/graph_view_page.dart index 3e1bf8b..269021c 100644 --- a/app/lib/pages/manager_page_sub/graph_view_page.dart +++ b/app/lib/pages/manager_page_sub/graph_view_page.dart @@ -10,6 +10,7 @@ import 'package:app/api.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'dart:io'; import 'package:app/custom_widgets/tag_dialog.dart'; +import 'package:app/services/settings_service.dart'; class GraphViewPage extends StatefulWidget { final FileTreeNode treeData; @@ -45,19 +46,50 @@ class _GraphViewPageState extends State { static const int maxDepth = 3; FileTreeNode? currentRootNode; - static const List levelColors = [ + List levelColors = [ kprimaryColor, - Color(0xFF3498DB), - Color(0xFF2ECC71), - Color(0xFFE74C3C), - Color(0xFF9B59B6), + const Color(0xFF3498DB), + const Color(0xFF2ECC71), + const Color(0xFFE74C3C), + const Color(0xFF9B59B6), ]; @override void initState() { super.initState(); + _loadColorPreset(); _initializeController(); _buildFileTreeGraph(); + + // Listen to settings changes + SettingsService.instance.addListener(_onSettingsChanged); + } + + @override + void dispose() { + SettingsService.instance.removeListener(_onSettingsChanged); + super.dispose(); + } + + void _onSettingsChanged() { + refreshColors(); + } + + Future _loadColorPreset() async { + final colorPreset = await SettingsService.instance.getColorPreset(); + setState(() { + levelColors = SettingsService.getPresetColors(colorPreset); + }); + } + + // Public method to refresh colors when settings change + Future refreshColors() async { + await _loadColorPreset(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + controller.needUpdate(); + } + }); } @override @@ -65,7 +97,7 @@ class _GraphViewPageState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.currentPath != widget.currentPath || oldWidget.treeData != widget.treeData) { - _buildFileTreeGraph(); + _loadColorPreset().then((_) => _buildFileTreeGraph()); } } diff --git a/app/lib/pages/settings_page.dart b/app/lib/pages/settings_page.dart index 4617f3a..b8f0f76 100644 --- a/app/lib/pages/settings_page.dart +++ b/app/lib/pages/settings_page.dart @@ -1,26 +1,656 @@ import 'package:flutter/material.dart'; +import '../constants.dart'; +import '../services/settings_service.dart'; +import '../services/password_service.dart'; -class SettingsPage extends StatelessWidget { +class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + String _selectedNamingConvention = "CAMEL"; + String _selectedColorPreset = "Default"; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadSettings(); + } + + Future _loadSettings() async { + final settings = SettingsService.instance; + final namingConvention = await settings.getNamingConvention(); + final colorPreset = await settings.getColorPreset(); + + setState(() { + _selectedNamingConvention = namingConvention; + _selectedColorPreset = colorPreset; + _isLoading = false; + }); + } + + Future _updateNamingConvention(String convention) async { + await SettingsService.instance.setNamingConvention(convention); + setState(() { + _selectedNamingConvention = convention; + }); + } + + Future _updateColorPreset(String preset) async { + await SettingsService.instance.setColorPreset(preset); + setState(() { + _selectedColorPreset = preset; + }); + } + @override Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(20), + if (_isLoading) { + return const Center( + child: CircularProgressIndicator(color: kprimaryColor), + ); + } + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Settings', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + Divider(color: Color(0xff3D3D3D)), + const SizedBox(height: 20), + // Folder Naming Convention Section + _buildSectionTitle('Folder Naming Convention'), + const SizedBox(height: 16), + _buildNamingConventionOptions(), + + const SizedBox(height: 40), + + // Color Presets Section + _buildSectionTitle('Graph View Color Presets'), + const SizedBox(height: 16), + _buildColorPresetOptions(), + + const SizedBox(height: 40), + + // Security Section + _buildSectionTitle('Security'), + const SizedBox(height: 16), + _buildSecurityOptions(), + ], + ), + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ); + } + + Widget _buildNamingConventionOptions() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xff242424), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: kOutlineBorder), + ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Settings', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, + children: + SettingsService.availableNamingConventions.map((convention) { + return _buildCustomRadioTile( + title: _formatConventionName(convention), + value: convention, + groupValue: _selectedNamingConvention, + onChanged: (value) => _updateNamingConvention(value!), + ); + }).toList(), + ), + ); + } + + Widget _buildColorPresetOptions() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xff242424), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: kOutlineBorder), + ), + child: Column( + children: + SettingsService.availablePresets.map((preset) { + return _buildColorPresetTile( + title: preset, + value: preset, + groupValue: _selectedColorPreset, + onChanged: (value) => _updateColorPreset(value!), + ); + }).toList(), + ), + ); + } + + Widget _buildCustomRadioTile({ + required String title, + required String value, + required String groupValue, + required ValueChanged onChanged, + }) { + final isSelected = value == groupValue; + + return InkWell( + onTap: () => onChanged(value), + borderRadius: BorderRadius.circular(6), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + child: Row( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? kprimaryColor : const Color(0xff9CA3AF), + width: 2, + ), + ), + child: + isSelected + ? Center( + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: kprimaryColor, + ), + ), + ) + : null, + ), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + color: isSelected ? Colors.white : const Color(0xff9CA3AF), + fontSize: 14, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } + + Widget _buildColorPresetTile({ + required String title, + required String value, + required String groupValue, + required ValueChanged onChanged, + }) { + final isSelected = value == groupValue; + final colors = _getPresetColors(value); + + return InkWell( + onTap: () => onChanged(value), + borderRadius: BorderRadius.circular(6), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + child: Row( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? kprimaryColor : const Color(0xff9CA3AF), + width: 2, + ), + ), + child: + isSelected + ? Center( + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: kprimaryColor, + ), + ), + ) + : null, + ), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + color: isSelected ? Colors.white : const Color(0xff9CA3AF), + fontSize: 14, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + ), + ), + const Spacer(), + // Color preview + Row( + children: + colors.map((color) { + return Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(left: 4), + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all(color: const Color(0xff3D3D3D)), + ), + ); + }).toList(), ), + ], + ), + ), + ); + } + + String _formatConventionName(String convention) { + switch (convention) { + case "CAMEL": + return "Camel Case (myFolderName)"; + case "SNAKE": + return "Snake Case (my_folder_name)"; + case "PASCAL": + return "Pascal Case (MyFolderName)"; + case "KEBAB": + return "Kebab Case (my-folder-name)"; + case "SPACE": + return "Space Case (My Folder Name)"; + default: + return convention; + } + } + + List _getPresetColors(String preset) { + return SettingsService.getPresetColors(preset).take(3).toList(); + } + + Widget _buildSecurityOptions() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xff242424), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: kOutlineBorder), + ), + child: Column( + children: [ + _buildSecurityTile( + title: 'Change Password', + subtitle: 'Update your password', + icon: Icons.lock_reset, + onTap: _showChangePasswordDialog, ), - SizedBox(height: 20), ], ), ); } + + Widget _buildSecurityTile({ + required String title, + required String subtitle, + required IconData icon, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(6), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), + child: Icon(icon, color: kprimaryColor, size: 20), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: const TextStyle( + color: Color(0xff9CA3AF), + fontSize: 12, + ), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: Color(0xff9CA3AF), size: 20), + ], + ), + ), + ); + } + + void _showChangePasswordDialog() { + showDialog(context: context, builder: (context) => _ChangePasswordDialog()); + } +} + +class _ChangePasswordDialog extends StatefulWidget { + @override + State<_ChangePasswordDialog> createState() => _ChangePasswordDialogState(); +} + +class _ChangePasswordDialogState extends State<_ChangePasswordDialog> { + final TextEditingController _currentPasswordController = + TextEditingController(); + final TextEditingController _newPasswordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + bool _isCurrentVisible = false; + bool _isNewVisible = false; + bool _isConfirmVisible = false; + bool _isLoading = false; + String _errorMessage = ''; + + @override + void dispose() { + _currentPasswordController.dispose(); + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _changePassword() async { + final currentPassword = _currentPasswordController.text; + final newPassword = _newPasswordController.text; + final confirmPassword = _confirmPasswordController.text; + + setState(() { + _errorMessage = ''; + _isLoading = true; + }); + + // Validation + if (currentPassword.isEmpty || + newPassword.isEmpty || + confirmPassword.isEmpty) { + setState(() { + _errorMessage = 'Please fill in all fields'; + _isLoading = false; + }); + return; + } + + if (newPassword.length < 4) { + setState(() { + _errorMessage = 'New password must be at least 4 characters'; + _isLoading = false; + }); + return; + } + + if (newPassword != confirmPassword) { + setState(() { + _errorMessage = 'New passwords do not match'; + _isLoading = false; + }); + return; + } + + // Change password + final success = await PasswordService.instance.changePassword( + currentPassword, + newPassword, + ); + + setState(() { + _isLoading = false; + }); + + if (success) { + if (mounted) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password changed successfully'), + backgroundColor: Colors.green, + ), + ); + } + } else { + setState(() { + _errorMessage = 'Current password is incorrect'; + }); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xff242424), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: kOutlineBorder), + ), + child: Container( + padding: const EdgeInsets.all(24), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.lock_reset, color: kprimaryColor, size: 24), + const SizedBox(width: 12), + const Text( + 'Change Password', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 24), + + // Current password + _buildPasswordField( + label: 'Current Password', + controller: _currentPasswordController, + isVisible: _isCurrentVisible, + onVisibilityToggle: + () => setState(() => _isCurrentVisible = !_isCurrentVisible), + hintText: 'Enter current password', + ), + const SizedBox(height: 16), + + // New password + _buildPasswordField( + label: 'New Password', + controller: _newPasswordController, + isVisible: _isNewVisible, + onVisibilityToggle: + () => setState(() => _isNewVisible = !_isNewVisible), + hintText: 'Enter new password', + ), + const SizedBox(height: 16), + + // Confirm new password + _buildPasswordField( + label: 'Confirm New Password', + controller: _confirmPasswordController, + isVisible: _isConfirmVisible, + onVisibilityToggle: + () => setState(() => _isConfirmVisible = !_isConfirmVisible), + hintText: 'Confirm new password', + ), + + // Error message + if (_errorMessage.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red.shade400, + size: 16, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _errorMessage, + style: TextStyle( + color: Colors.red.shade400, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ], + + const SizedBox(height: 24), + + // Buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: + _isLoading ? null : () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + foregroundColor: const Color(0xff9CA3AF), + ), + child: const Text('Cancel'), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: _isLoading ? null : _changePassword, + style: ElevatedButton.styleFrom( + backgroundColor: kprimaryColor, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: + _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : const Text('Change Password'), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildPasswordField({ + required String label, + required TextEditingController controller, + required bool isVisible, + required VoidCallback onVisibilityToggle, + required String hintText, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: kScaffoldColor, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: kOutlineBorder), + ), + child: TextField( + controller: controller, + obscureText: !isVisible, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: hintText, + hintStyle: const TextStyle(color: Color(0xff9CA3AF)), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + suffixIcon: IconButton( + icon: Icon( + isVisible ? Icons.visibility : Icons.visibility_off, + color: const Color(0xff9CA3AF), + size: 20, + ), + onPressed: onVisibilityToggle, + ), + ), + ), + ), + ], + ); + } } diff --git a/app/lib/services/password_service.dart b/app/lib/services/password_service.dart new file mode 100644 index 0000000..c631b1f --- /dev/null +++ b/app/lib/services/password_service.dart @@ -0,0 +1,116 @@ +import 'dart:convert'; +import 'dart:math'; +import 'package:crypto/crypto.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class PasswordService { + static const String _passwordHashKey = 'password_hash'; + static const String _saltKey = 'password_salt'; + static const String _isSetupKey = 'password_setup_complete'; + + static PasswordService? _instance; + static PasswordService get instance => _instance ??= PasswordService._(); + + PasswordService._(); + + late SharedPreferences _prefs; + bool _initialized = false; + + Future init() async { + if (!_initialized) { + _prefs = await SharedPreferences.getInstance(); + _initialized = true; + } + } + + // Check if password has been set up + Future isPasswordSet() async { + await init(); + return _prefs.getBool(_isSetupKey) ?? false; + } + + // Generate a random salt + String _generateSalt() { + final random = Random.secure(); + final saltBytes = List.generate(32, (i) => random.nextInt(256)); + return base64.encode(saltBytes); + } + + // Hash password with salt + String _hashPassword(String password, String salt) { + final bytes = utf8.encode(password + salt); + final digest = sha256.convert(bytes); + return digest.toString(); + } + + // Set up password for the first time + Future setupPassword(String password) async { + await init(); + + if (password.isEmpty || password.length < 4) { + return false; // Password too short + } + + try { + final salt = _generateSalt(); + final hashedPassword = _hashPassword(password, salt); + + await _prefs.setString(_saltKey, salt); + await _prefs.setString(_passwordHashKey, hashedPassword); + await _prefs.setBool(_isSetupKey, true); + + return true; + } catch (e) { + return false; + } + } + + // Verify password + Future verifyPassword(String password) async { + await init(); + + final salt = _prefs.getString(_saltKey); + final storedHash = _prefs.getString(_passwordHashKey); + + if (salt == null || storedHash == null) { + return false; + } + + final hashedPassword = _hashPassword(password, salt); + return hashedPassword == storedHash; + } + + // Change password + Future changePassword(String oldPassword, String newPassword) async { + await init(); + + // Verify old password first + if (!await verifyPassword(oldPassword)) { + return false; + } + + if (newPassword.isEmpty || newPassword.length < 4) { + return false; // New password too short + } + + try { + final salt = _generateSalt(); + final hashedPassword = _hashPassword(newPassword, salt); + + await _prefs.setString(_saltKey, salt); + await _prefs.setString(_passwordHashKey, hashedPassword); + + return true; + } catch (e) { + return false; + } + } + + // Reset password system (for development/testing) + Future resetPasswordSystem() async { + await init(); + await _prefs.remove(_passwordHashKey); + await _prefs.remove(_saltKey); + await _prefs.remove(_isSetupKey); + } +} \ No newline at end of file diff --git a/app/lib/services/settings_service.dart b/app/lib/services/settings_service.dart new file mode 100644 index 0000000..ce0ec89 --- /dev/null +++ b/app/lib/services/settings_service.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../constants.dart'; + +class SettingsService extends ChangeNotifier { + static const String _colorPresetKey = 'graph_color_preset'; + static const String _namingConventionKey = 'folder_naming_convention'; + + static SettingsService? _instance; + static SettingsService get instance => _instance ??= SettingsService._(); + + SettingsService._(); + + late SharedPreferences _prefs; + bool _initialized = false; + + Future init() async { + if (!_initialized) { + _prefs = await SharedPreferences.getInstance(); + _initialized = true; + } + } + + // Color Preset Methods + Future setColorPreset(String preset) async { + await init(); + await _prefs.setString(_colorPresetKey, preset); + notifyListeners(); + } + + Future getColorPreset() async { + await init(); + return _prefs.getString(_colorPresetKey) ?? 'Default'; + } + + // Naming Convention Methods + Future setNamingConvention(String convention) async { + await init(); + await _prefs.setString(_namingConventionKey, convention); + notifyListeners(); + } + + Future getNamingConvention() async { + await init(); + return _prefs.getString(_namingConventionKey) ?? 'CAMEL'; + } + + // Color Preset Definitions + static List getPresetColors(String preset) { + switch (preset) { + case "Default": + return [ + kprimaryColor, + const Color(0xff3B82F6), + const Color(0xff10B981), + const Color(0xffE74C3C), + const Color(0xff9B59B6), + ]; + case "Ocean": + return [ + const Color(0xff0EA5E9), + const Color(0xff06B6D4), + const Color(0xff3B82F6), + const Color(0xff1D4ED8), + const Color(0xff0C4A6E), + ]; + case "Forest": + return [ + const Color(0xff10B981), + const Color(0xff059669), + const Color(0xff065F46), + const Color(0xff047857), + const Color(0xff064E3B), + ]; + case "Sunset": + return [ + const Color(0xffF59E0B), + const Color(0xffEF4444), + const Color(0xffDC2626), + const Color(0xffB91C1C), + const Color(0xff991B1B), + ]; + case "Monochrome": + return [ + const Color(0xff6B7280), + const Color(0xff4B5563), + const Color(0xff374151), + const Color(0xff1F2937), + const Color(0xff111827), + ]; + default: + return [ + kprimaryColor, + const Color(0xff3B82F6), + const Color(0xff10B981), + const Color(0xffE74C3C), + const Color(0xff9B59B6), + ]; + } + } + + static const List availablePresets = [ + "Default", + "Ocean", + "Forest", + "Sunset", + "Monochrome", + ]; + static const List availableNamingConventions = [ + "CAMEL", + "SNAKE", + "PASCAL", + "KEBAB", + "SPACE", + ]; +} diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 47e516e..5a304fb 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import file_picker import open_file_mac import screen_retriever_macos +import shared_preferences_foundation import tray_manager import url_launcher_macos import window_manager @@ -16,6 +17,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/app/pubspec.lock b/app/pubspec.lock index bc7fafe..3c87e6e 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -178,7 +178,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" @@ -544,6 +544,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -552,6 +576,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -632,6 +664,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e + url: "https://pub.dev" + source: hosted + version: "2.4.13" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -885,6 +973,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -903,4 +999,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.2 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.29.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 45eb439..d5af54d 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -44,6 +44,8 @@ dependencies: url_launcher: ^6.3.1 flutter_context_menu: ^0.2.4 tray_manager: ^0.5.0 + shared_preferences: ^2.3.3 + crypto: ^3.0.6 flutter_launcher_icons: windows: diff --git a/docs/documentation/demo_4/Architecture.md b/docs/documentation/demo_4/Architecture.md new file mode 100644 index 0000000..7aafcb6 --- /dev/null +++ b/docs/documentation/demo_4/Architecture.md @@ -0,0 +1,193 @@ +# Architectural Requirements Document + +

+ Company Logo +

+ +**Version:** 3.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Southern Cross Solutions +**Document Type:** Architectural Requirements Document +**Demo:** Demo 3 + +## Table of Contents + +- [Architectural Quality Requirements](#architectural-quality-requirements) +- [Architectural Strategies](#architectural-strategies) +- [Architectural Design and Pattern](#architectural-design-and-patterns) +- [Architectural Constraints](#architectural-constraints) + +--- + + +## Architectural Quality Requirements + +The following quality requirements are prioritized from highest to lowest priority for the Smart File Manager system: + +## Architectural Quality Requirements for Smart File Manager (SFM) + +### 1. **Reliability** + +- **Specification:** The system must maintain file integrity and recover gracefully from failures (e.g., crashes or power loss). +- **Testable Criteria:** No data corruption or loss during operations; system auto-recovers to last stable state after unexpected shutdowns. +- **Rationale:** Users must trust the system to manage personal or critical files safely and without risk of accidental loss. + +### 2. **Performance** + +- **Specification:** File operations, such as sorting, tagging, and smart search, must execute within acceptable time limits on standard desktop hardware. +- **Testable Criteria:** Smart search results must appear in under 2 seconds; bulk operations (e.g., tagging or reclassifying 500 files) must complete within 5 seconds. +- **Rationale:** Responsiveness is key to a smooth user experience in a personal desktop application. Delays can disrupt workflow and reduce trust in automation. + +### 3. **Scalability** + +- **Specification:** The system must handle increasing numbers of files, metadata, and tags without degradation in performance. +- **Testable Criteria:** Maintain acceptable performance (search in <2 seconds, classification in <5 seconds) when managing up to 1 million files or 1TB of data. +- **Rationale:** Personal file collections can grow substantially over time, so the system must remain efficient and responsive even at high volume. + +### 4. **Usability** + +- **Specification:** The user interface must be intuitive, accessible, and require minimal onboarding or training. +- **Testable Criteria:** First-time users can complete core tasks (e.g., find a file, create a smart manager, apply a tag) within 5 minutes, with no external documentation. +- **Rationale:** The system targets general users with varying technical skill levels; high usability promotes adoption and continued use. + +### 5. **Modifiability** + +- **Specification:** Users must be able to define or update rules, filters, and semantic tags without developer intervention. +- **Testable Criteria:** 100% of common modifications can be made through a graphical user interface without restarting the application. +- **Rationale:** Enables users to customize the system to suit their unique workflows and evolving organizational habits. + +--- + + +## Architectural Strategies + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Quality AttributeArchitectural StrategyImplementation Details
ReliabilityTransactional File OperationsAll operations on the user files should either work completely for the entire operation or not modify the user's files at all.
PerformanceConcurrencyLeverage our master slave pattern to break slow requests into smaller operations which may be run concurrently.
PerformanceI/O overheadMinimize the number of times files need to be opened to reduce I/O overhead.
ScalabilityHorizontal ScalingCreate more instances of Python slaves to increase throughput for high demand.
UsabilityUser-centered designConduct usability tests, provide intuitive navigation, and minimize complexity. Provide accessible interfaces appropriate for users of varying skill level
ModifiabilityModular, loosely coupled architectureDesign the system such that all operations accept various parameters while still having default ones. Allow users to customize these parameters from the user-interface.
+ + +--- + +## Architectural Design and Patterns + +### Architectural Diagram + +![Application Architecture](assets/applicationArchitectureV5.png) + +SFM makes use of various architectural patterns selected to support our quality requirements. Our choices and justifications for them follows. + +### Monolithic Architecture + +Monolithic architecture combines the various components of an application into a single inseparable unit. Our project is deployed as an single executable application running exclusively on the user's personal machine, with no external dependancies. This deployment model justifies our choice for using this architecture. + +#### How it supports our quality requirements +* **Reliability:** + - Monolithic systems have no dependancies on external modules which could fail, therby reducing overall system uptime. + - Monolithic systems are not continuously deployed, instead only being deployed once a stable and fully tested version is ready, improving reliability. +* **Performance:** + - Due to reduced communication overhead between modules within the monolithic system performance may be faster as opposed to network based bottlenecks introduced by other architectures. +* **Scalability:** + - While monolithic systems do not scale horizontally, monolithic systems may scale vertically with improved hardware. Please also see how we leverage the master-slave pattern to offset the limited scalability. +* **Usability:** + - By deploying our monolithic system on different operating systems we improve usability since the application runs in an environment familiar to the end user. +* **Modifiability:** + - Not applicable + +### Master-Slave Architectural Pattern + +Master-Slave architecture leverages a central node (master) to delegate tasks to multiple worker nodes (slaves). We mainly leverage this pattern to improve the performance of our computational demanding tasks. + +#### How it supports our quality requirements +* **Reliability:** + - Reliance on a master node generally decreases the reliability of the master-slave architecture pattern. We compensate for this by ensuring that when our master fails it does so gracefully without going into an unrecoverable state. Use of the monolithic architecture further supports this since the domain in which our system runs is more predictable (owing to the deployment). +* **Performance:** + - The primary reason for using Master-slave is performance. Our backend is paritioned into 2 sub-systems reponsible for filesystem and clustering features respectively. By leveraging threadpools in our clustering subsystem (slaves) we increase performance drastically. + - Furthermore our slaves are stateless and do not interact with the same resources, avoiding traditional concurrency related problems like deadlock. +* **Scalability:** + - The Master-slave patterns facilitates horizontal scaling by creating more slave instances. It should be noted that there is a upper limit on our ability to scale horizontally dictated by the system's number of logical processors. +* **Usability:** + - Not applicable +* **Modifiability:** + - Not applicable + +### Client-Server Architecture Pattern + +In order to allow our different codebases to communicate we leverage the standard client-server architecture pattern. To keep our architecture technology agonstic we refer to the filesystem server and clustering server. For practical purposes these refer to our go and python server respectively. The filesystem server interacts with the user's computer directly via the endpoints referred to in our service contracts while the clustering service is responsible for our "smart" sorting functionality. Furthemore both these servers run on localhost due to the application being a monolithic desktop app. + +#### How it supports our quality requirements +* **Reliability & Performance:** + - Using various servers we benefit from the advantages of using different technologies for what they are best at. For example: Go for its fast and easy concurrency via goroutines and python for its rich AI library ecosystem. Please refer to technology choices in [our SRS document](srs.md). Using technologies for what they are designed for improves both performance and reliability. +* **Scalability:** + - N/A +* **Usability:** + - N/A +* **Modifiability:** + - N/A + + + +--- + +## Architectural Constraints + +The following constraints affect the architectural design of the Smart File Manager: + +### Data Constraints + +- File system access - Which components can access which directories or file types on the system +- Data persistance - Where can files be stored and naming conventions +- File format - restrictions on file types and accessing metadata. + +### Technology Constraints + +- Operating system compatibility - Must run on Windows, macOS, Linux platforms, thus need architecture that supports cross-platform. + +### Performance Constraints + +- runtime bottleneck - Slower execution affects real-time file operations. +- overhead in memory - higher memory usage for large file sorting operations. + +### Deployment Constraints + +- The system must be deployable as a desktop app +- Must run on Windows, Linux, and macOS with consistent functionality. + +--- + diff --git a/docs/documentation/demo_4/api.md b/docs/documentation/demo_4/api.md new file mode 100644 index 0000000..f47f06a --- /dev/null +++ b/docs/documentation/demo_4/api.md @@ -0,0 +1,1075 @@ +# Smart File Manager – Flutter API Client Documentation +**Version:** 4.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Southern Cross Solutions & Personal Development team use + +## Content +* [Introduction](#introduction) +* [A note on security](#a-note-on-security) +* [General Notes](#general-notes) +* [loadTreeData](#loadtreedata) +* [sortTree](#sorttree) +* [moveDiretory](#movedirectory) +* [addSmartManager](#addsmartmanager) +* [deleteManager](#deletemanager) +* [addTagToFile](#addtagtofile) +* [deleteTagFromFile](#deletetagfromfile) +* [deleteFile](#deletefile) +* [deleteFolder](#deletefolder) +* [bulkDeleteFiles](#bulkdeletefiles) +* [bulkDeleteFolders](#bulkdeletefolders) +* [bulkAddTag & bulkRemoveTag](#bulk-add-and-bulk-remove-tags) +* [findDuplicates](#findduplicates) +* [returnType](#returntype) +* [returnStats](#returnstats) +* [Locking](#locking) +* [Unlocking](#unlocking) +* [StartUp](#startup) +* [Search](#search) +* [keywordSearch](#keywordsearch) +* [isKeywordSearchReady](#iskeywordsearchready) + + +## Introduction +Our team makes uses of standard Rest API endpoints to connect our filesystem server (go backend) to our frontend. The reasons we did not use gRPC for these endpoints (like our connection from the filesystem server to the clustering server) is as follows: + +* The size of requests are much smaller than the ones sent to the filesystem server. +* It is easier to use traditional JSON based payloads for frontend development. + +> **Base URI**: `http://localhost:51000/` + +## A note on security +Our application is deployed as a standalone desktop application which does not rely on (or use) internet connection. That being said we require all requests to include an apiSecret header. This value is set during the installation process. + +## General Notes + +* Make sure to URI-encode query parameter values when needed. +* The backend must be running at the defined base URI for requests to succeed. +* apiSecret header must be included for each request + +--- + +## loadTreeData + +Fetches the file tree structure for a Smart Manager. + +### Usage +```GET /loadTreeData?name={name}``` + +### Parameters + +* name: The name of the Smart Manager. + +### Returns + +A nested JSON structure representing the full directory tree and tags. +Example response: + +```json +{ + "name": "root", + "isFolder": true, + "children": [ + { + "name": "file1.txt", + "path": "c:://", + "isFolder": false, + "tags": ["work", "important"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "file2.docx", + "path": "c:://", + "isFolder": false, + "tags": ["document"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "Documents", + "isFolder": true, + "children": [ + { + "name": "resume.pdf", + "path": "c:://", + "isFolder": false, + "tags": ["personal", "career"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "Reports", + "isFolder": true, + "children": [ + { + "name": "annual_2023.pdf", + "path": "c:://", + "isFolder": false, + "tags": ["report", "finance"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "q1_summary.docx", + "path": "c:://", + "isFolder": false, + "tags": ["summary", "q1"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + } + ] + } + ] + } + ] +} +``` + +### Throws + +Exception if the request fails. + +## sortTree + +Fetches the sorted version of the file tree structure for a Smart Manager. + +### Usage +```GET /sortTree?name={name}``` + +### Parameters + +* name: The name of the Smart Manager. + +### Returns + +A nested JSON structure representing the full directory tree and tags. +Example response: + +```json +{ + "name": "root", + "isFolder": true, + "children": [ + { + "name": "file1.txt", + "path": "c:://", + "isFolder": false, + "tags": ["work", "important"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "file2.docx", + "path": "c:://", + "isFolder": false, + "tags": ["document"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "Documents", + "isFolder": true, + "children": [ + { + "name": "resume.pdf", + "path": "c:://", + "isFolder": false, + "tags": ["personal", "career"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "Reports", + "isFolder": true, + "children": [ + { + "name": "annual_2023.pdf", + "path": "c:://", + "isFolder": false, + "tags": ["report", "finance"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + }, + { + "name": "q1_summary.docx", + "path": "c:://", + "isFolder": false, + "tags": ["summary", "q1"], + "metadata": { + "size": "12KB", + "dateCreated": "2023-11-08T14:20:00Z", + "mimeType": "Tiaan/Bosman", + "lastModified": "2024-02-20T10:10:00Z" + }, + } + ] + } + ] + } + ] +} +``` + +### Throws + +Exception if the request fails. + +## moveDirectory + +After sorting a Smart Manager, the user can choose to actually move the files that were sorted into a new sorted directory. + +### Usage +````GET /moveDirectory?name={name}``` + +### Parameters + +* name: Smart Manager name + +### Returns + +true on success. + +false on failure. + +### Throws + +Exception if invalid. + +## addSmartManager + +Creates and registers a new Smart Manager by mounting a specified folder path. + +### Usage +```GET /addDirectory?name={name}&path={path}``` + +### Parameters + +* name: Unique identifier for the Smart Manager. + +* path: Absolute path to the folder to mount. + +### Returns + +true on success. + +### Throws + +Exception if creation fails. + +## deleteManager + +Deletes a Smart Manager and any associated data. + +### Usage +```GET /deleteManager?name={name}``` + +### Parameters + +* name: Name of the Smart Manager to delete. + +### Returns + +true on success. + +Manager not found on failure. + +### Throws + +Exception if deletion fails. + +## deleteFile + +Deletes a file contained in specified manager. + +### Usage +```GET /deleteFile?name={name}&path={path}``` + +### Parameters + +* name: Name of the Smart Manager to delete from. +* path : Path to the file to delete + +### Returns + +A JSON structure consisting of all files + +```json +{ + "name": "TagManager", + "isFolder": true, + "rootPath": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers", + "children": [ + { + "name": "Chapter-9-World-War-I-1914-1918.pdf", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/Chapter-9-World-War-I-1914-1918.pdf", + "isFolder": false, + "tags": [ + "history", + "world-war-1", + "pdf", + "academic", + "chapter-9" + ], + "metadata": { + "size": "10297185", + "dateCreated": "2025-06-27 07:57", + "mimeType": ".pdf", + "lastModified": "2025-06-27 07:57" + }, + "locked": false + } + ] +} +``` + +'false' if not found. + +### Throws + +Exception if deletion fails. + +## deleteFolder + +Deletes a folder contained in specified manager. + +### Usage +```GET /deleteFile?name={name}&path={path}``` + +### Parameters + +* name: Name of the Smart Manager to delete from. +* path : Path to the folder to delete + +### Returns + +A JSON structure consisting of all files + +```json +{ + "name": "TagManager", + "isFolder": true, + "rootPath": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers", + "children": [ + { + "name": "Chapter-9-World-War-I-1914-1918.pdf", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/Chapter-9-World-War-I-1914-1918.pdf", + "isFolder": false, + "tags": [ + "history", + "world-war-1", + "pdf", + "academic", + "chapter-9" + ], + "metadata": { + "size": "10297185", + "dateCreated": "2025-06-27 07:57", + "mimeType": ".pdf", + "lastModified": "2025-06-27 07:57" + }, + "locked": false + } + ] +} +``` + +'false' if not found. + +### Throws + +Exception if deletion fails. + +## bulkDeleteFiles + +Deletes a list of files contained in specified manager. + +### Usage +```GET /bulkDeleteFiles?name={name}, json body``` + +### Parameters + +* name: Name of the Smart Manager to delete from. +* JSON body containing list of files to delete + +```json +[ + +{ +"file_path": "/home/user/documents/report.pdf", +}, +{ +"file_path": "/home/user/photos/vacation.jpg", +}, +{ +"file_path": "/home/user/music/song.mp3", +} +] +``` + + +### Returns + +A JSON structure consisting of all files + +```json +{ + "name": "TagManager", + "isFolder": true, + "rootPath": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers", + "children": [ + { + "name": "Chapter-9-World-War-I-1914-1918.pdf", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/Chapter-9-World-War-I-1914-1918.pdf", + "isFolder": false, + "tags": [ + "history", + "world-war-1", + "pdf", + "academic", + "chapter-9" + ], + "metadata": { + "size": "10297185", + "dateCreated": "2025-06-27 07:57", + "mimeType": ".pdf", + "lastModified": "2025-06-27 07:57" + }, + "locked": false + } + ] +} +``` + +'false' if not found. + +### Throws + +Exception if deletion fails. + +## bulkDeleteFolders + +Deletes a list of folders contained in specified manager. + +### Usage +```GET /bulkDeleteFolders?name={name}, json body``` + +### Parameters + +* name: Name of the Smart Manager to delete from. +* JSON body containing list of folders to delete + +```json +[ + +{ +"file_path": "/home/user/documents", +}, +{ +"file_path": "/home/user/photos", +}, +{ +"file_path": "/home/user/music", +} +] +``` + + +### Returns + +A JSON structure consisting of all files + +```json +{ + "name": "TagManager", + "isFolder": true, + "rootPath": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers", + "children": [ + { + "name": "Chapter-9-World-War-I-1914-1918.pdf", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/Chapter-9-World-War-I-1914-1918.pdf", + "isFolder": false, + "tags": [ + "history", + "world-war-1", + "pdf", + "academic", + "chapter-9" + ], + "metadata": { + "size": "10297185", + "dateCreated": "2025-06-27 07:57", + "mimeType": ".pdf", + "lastModified": "2025-06-27 07:57" + }, + "locked": false + } + ] +} +``` + +'false' if not found. + +### Throws + +Exception if deletion fails. + +## addTagToFile + +Adds a tag to a specific file under a Smart Manager. + +### Usage +```GET /addTag?path={path}&tag={tag}``` + +### Parameters + +path: Path to the file. + +tag: Tag to assign. + +### Returns + +true on success. + +### Throws + +Exception if tagging fails. + +## deleteTagFromFile + +Removes a tag from a specific file. + +### Usage +```GET /removeTag?path={path}&tag={tag}``` + +### Parameters + +* path: Path to the file. + +* tag: Tag to remove. + +### Returns + +true on success. + +### Throws + +Exception if removal fails. + +## Locking + +Locks a file or folder. When locking a folder all sub-folders and files are also locked. + +### Usage +```/lock?name={name}&path=../../testRootFolder``` + +### Returns + +true on success. + +### Throws + +Exception if removal fails. + +## Unlocking + +Unlocks a file or folder. When unlocking a folder all sub-folders and files are also unlocked. + +### Usage +```/unlock?name={name}&path=../../testRootFolder``` + +### Returns + +true on success. + +### Throws + +Exception if removal fails. + +## startUp + +Loads smart managers into memory and returns the respective names + +### Usage +GET /startUp + +### Parameters + +None + +### Response +```json +{ + "responseMessage": "Request successful!, composites: 1", + "managerNames": [ + "first2" + ] +} +``` + +## findDuplicate + +Identifies and returns a list of files that are considered duplicates based on file size, hash, and content matching. + +### Usage +```GET /findDuplicates?name={name}``` + +### Parameters + +* name: The name of the Smart Manager to search for duplicates in. + +### Returns + +A JSON array of duplicate file pairs. Each object includes: + +* name: The common file name. + +* original: The full path to the original file. + +* duplicate: The full path to the detected duplicate. + +Example Response +```json +[ + { + "name": "The Man of Steel.docx", + "original": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers2/The Man of Steel (another copy).docx", + "duplicate": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers2/The Man of Steel.docx" + }, + { + "name": "pyramid-technology.pdf", + "original": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers2/pyramid-technology (copy).pdf", + "duplicate": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers2/pyramid-technology.pdf" + } +] +``` + +### Throws + +Exception if the request fails or if the Smart Manager name is invalid. + +## returnType + +Identifies and returns a list of files of the same file type. + +### Usage +```GET /returnType?name={name}&type{filetype}&umbrella{bool}``` + +### Parameters + +* name: The name of the Smart Manager to retrieve the file types from. +* type: File types to retrieve, can be specific(pdf) or umbrella(Documents). +* umbrella: Boolean that determines if umbrella types should be returned. + +### Returns + +A JSON array of files that fit the parameters. Each object includes: + +* file_name: The common file name. + +* file_path: The full path to the file. + + +Example Response +```json +[ + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/egypts-pyramids.pdf", + "file_name": "egypts-pyramids.pdf" + }, + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/Chapter-9-World-War-I-1914-1918.pdf", + "file_name": "Chapter-9-World-War-I-1914-1918.pdf" + }, + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/Origins-of-World-War-I-Essay-ojznse.pdf", + "file_name": "Origins-of-World-War-I-Essay-ojznse.pdf" + }, + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/World_War_1_The_Great_War_and_its_Impact_OA_edition.pdf", + "file_name": "World_War_1_The_Great_War_and_its_Impact_OA_edition.pdf" + }, + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/pyramid-technology.pdf", + "file_name": "pyramid-technology.pdf" + } +] +``` + +### Throws + +Exception if the request fails or if the Smart Manager name is invalid. + +## returnStats + +Returns a set of statistics from all managers for the dashboard page + +### Usage +```GET /returnStats``` + +### Parameters +* N/A + +### Returns + +A JSON array of objects containing stats per manager. Each object includes: + +* manager_name: Name of the manager +* size: Size of the manager(kB) +* folders: Amount of folders in manager. +* files: Amount of files in manager. +* recent: List of 5 most recently accessed files(lim 5). +* largest: List of 5 largest files(lim 5). +* oldest: List of 5 leasr recently accessed files(lim 5). + + +Example Response +```json +[ + { + "manager_name": "manager3", + "size": 16289179, + "folders": 0, + "files": 45, + "recent": [ + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/test_files_3/Main.form", + "file_name": "Main.form" + }... + ], + "largest": [ + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/test_files_3/~WRL1847.tmp", + "file_name": "~WRL1847.tmp" + }... + ], + "oldest": [ + { + "file_path": "/home/henco/Documents/University-Files/Third-year/COS-301/test_files_3/Apr18 meeting.txt", + "file_name": "Apr18 meeting.txt" + }... + ], + "umbrella_counts": [ + 25, + 15, + 0, + 0, + 0, + 1, + 0, + 4 + ] + } +] +``` + +### Throws + +N/A returns empty structure if no managers found + +## Bulk add and Bulk Remove tags + +This functionality allows adding and removing tags in bulk. + +### Usage +``` +POST /bulkAddTag?name{manager name}, json body +POST /bulkRemoveTag?name{manager name}, json body +``` + +### Parameters + +* name: Name of the Smart Manager. + +* JSON body of all files that require tags. + +Example JSON body: +```json +[ + { + "file_path": "/home/user/documents/report.pdf", + "tags": ["work", "important", "pdf"] + }, + { + "file_path": "/home/user/photos/vacation.jpg", + "tags": ["holiday", "family", "2025"] + }, + { + "file_path": "/home/user/music/song.mp3", + "tags": ["music", "mp3", "favorites"] + } +] +``` + +### Returns + +A JSON structure consisting of all files with the included tags. + +```json +{ + "name": "TagManager", + "isFolder": true, + "rootPath": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers", + "children": [ + { + "name": "Chapter-9-World-War-I-1914-1918.pdf", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/Papers/Chapter-9-World-War-I-1914-1918.pdf", + "isFolder": false, + "tags": [ + "history", + "world-war-1", + "pdf", + "academic", + "chapter-9" + ], + "metadata": { + "size": "10297185", + "dateCreated": "2025-06-27 07:57", + "mimeType": ".pdf", + "lastModified": "2025-06-27 07:57" + }, + "locked": false + } + ] +} +``` + +### Throws + +Exception "Manager not found" if Smart Manager could not be found. + +## Search + +Finds files of a given name inside a specific Smart Manager. + +### Usage +```GET /search?compositeName={name}&searchText={fileToSearch}``` + +### Parameters + +* compositeName: Unique identifier for the Smart Manager. + +* searchText: File name to search for. + +### Returns + +A JSON array of all the files that match the specified search criteria. Each file includes: + +* name: string + +* path: string + +* isFolder: boolean + +* tags: string[] (All user-defined tags for the file) + +* metadata: string[] (All metadata extracted for the file) + +```json +{ + "name": "Josh", + "isFolder": true, + "children": [ + { + "name": "tiaan.jpeg", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/GIT/Smart-File-Manager/docs/documentation/demo_2/assets/readmeAssets/tiaan.jpeg", + "isFolder": false, + "metadata": { + "size": "202624", + "dateCreated": "2025-07-08 13:19", + "mimeType": "", + "lastModified": "2025-07-08 13:19" + }, + "locked": false + }, + { + "name": "tiaan.jpeg", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/GIT/Smart-File-Manager/docs/documentation/demo_1/assets/readmeAssets/tiaan.jpeg", + "isFolder": false, + "metadata": { + "size": "202624", + "dateCreated": "2025-07-08 13:19", + "mimeType": "", + "lastModified": "2025-07-08 13:19" + }, + "locked": false + } + ] +} +``` + +### Throws + +Not applicable (returns empty structure if nothing found). + + +## Search + +Finds files of a given name inside a specific Smart Manager. + +### Usage +```GET /search?compositeName={name}&searchText={fileToSearch}``` + +### Parameters + +* compositeName: Unique identifier for the Smart Manager. + +* searchText: File name to search for. + +### Returns + +A JSON array of all the files that match the specified search criteria. Each file includes: + +* name: string + +* path: string + +* isFolder: boolean + +* tags: string[] (All user-defined tags for the file) + +* metadata: string[] (All metadata extracted for the file) + +```json +{ + "name": "Josh", + "isFolder": true, + "children": [ + { + "name": "tiaan.jpeg", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/GIT/Smart-File-Manager/docs/documentation/demo_2/assets/readmeAssets/tiaan.jpeg", + "isFolder": false, + "metadata": { + "size": "202624", + "dateCreated": "2025-07-08 13:19", + "mimeType": "", + "lastModified": "2025-07-08 13:19" + }, + "locked": false + }, + { + "name": "tiaan.jpeg", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/GIT/Smart-File-Manager/docs/documentation/demo_1/assets/readmeAssets/tiaan.jpeg", + "isFolder": false, + "metadata": { + "size": "202624", + "dateCreated": "2025-07-08 13:19", + "mimeType": "", + "lastModified": "2025-07-08 13:19" + }, + "locked": false + } + ] +} +``` + +### Throws + +Not applicable (returns empty structure if nothing found). + +## keywordSearch + +Extracts keywords from files found in the smartfile manager, it then stores the keywords and tags and locks in a json file. on startup it loads the composite in, reads the stored json and reads over the keywords. + +### Usage +```GET /keywordSearch?compositeName={name}&searchText={fileToSearch}``` + +### Parameters +* compositeName: Unique identifier for the Smart Manager. +* searchText: File name to search for. + +### Return +A JSON array of all the files that match the specified search criteria. Each file includes: +* name: string +* path: string +* isFolder: boolean +* tags: string[] (All user-defined tags for the file) +* metadata: string[] (All metadata extracted for the file) + +```json +{ + "name": "Josh", + "isFolder": true, + "children": [ + { + "name": "tiaan.jpeg", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/GIT/Smart-File-Manager/docs/documentation/demo_2/assets/readmeAssets/tiaan.jpeg", + "isFolder": false, + "metadata": { + "size": "202624", + "dateCreated": "2025-07-08 13:19", + "mimeType": "", + "lastModified": "2025-07-08 13:19" + }, + "locked": false + }, + { + "name": "tiaan.jpeg", + "path": "/home/henco/Documents/University-Files/Third-year/COS-301/GIT/Smart-File-Manager/docs/documentation/demo_1/assets/readmeAssets/tiaan.jpeg", + "isFolder": false, + "metadata": { + "size": "202624", + "dateCreated": "2025-07-08 13:19", + "mimeType": "", + "lastModified": "2025-07-08 13:19" + }, + "locked": false + } + ] +} +``` + +### Throws +Not applicable (returns empty structure if nothing found). + + +## isKeywordSearchReady +Extracts keywords from files found in the smartfile manager, it then stores the keywords and tags and locks in a json file. on startup it loads the composite in, reads the stored json and reads over the keywords. + +### Usage +```GET /isKeywordSearchReady?compositeName={name}``` + +### Parameters +* compositeName: Unique identifier for the Smart Manager. + +### Returns +Boolean value based on if the composite has retrieved keywords + +### Throws +Bad request if manager not found. diff --git a/docs/documentation/demo_4/assets/DomainModel.jpg b/docs/documentation/demo_4/assets/DomainModel.jpg new file mode 100644 index 0000000..b03b75f Binary files /dev/null and b/docs/documentation/demo_4/assets/DomainModel.jpg differ diff --git a/docs/documentation/demo_4/assets/UC1.png b/docs/documentation/demo_4/assets/UC1.png new file mode 100644 index 0000000..6ab1c4d Binary files /dev/null and b/docs/documentation/demo_4/assets/UC1.png differ diff --git a/docs/documentation/demo_4/assets/UC10.png b/docs/documentation/demo_4/assets/UC10.png new file mode 100644 index 0000000..44ae16c Binary files /dev/null and b/docs/documentation/demo_4/assets/UC10.png differ diff --git a/docs/documentation/demo_4/assets/UC11.png b/docs/documentation/demo_4/assets/UC11.png new file mode 100644 index 0000000..6991f0f Binary files /dev/null and b/docs/documentation/demo_4/assets/UC11.png differ diff --git a/docs/documentation/demo_4/assets/UC12.png b/docs/documentation/demo_4/assets/UC12.png new file mode 100644 index 0000000..88f8f7c Binary files /dev/null and b/docs/documentation/demo_4/assets/UC12.png differ diff --git a/docs/documentation/demo_4/assets/UC13.png b/docs/documentation/demo_4/assets/UC13.png new file mode 100644 index 0000000..556ca7d Binary files /dev/null and b/docs/documentation/demo_4/assets/UC13.png differ diff --git a/docs/documentation/demo_4/assets/UC14.png b/docs/documentation/demo_4/assets/UC14.png new file mode 100644 index 0000000..7593804 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC14.png differ diff --git a/docs/documentation/demo_4/assets/UC15.png b/docs/documentation/demo_4/assets/UC15.png new file mode 100644 index 0000000..1ad7529 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC15.png differ diff --git a/docs/documentation/demo_4/assets/UC16.png b/docs/documentation/demo_4/assets/UC16.png new file mode 100644 index 0000000..8baa51a Binary files /dev/null and b/docs/documentation/demo_4/assets/UC16.png differ diff --git a/docs/documentation/demo_4/assets/UC17.png b/docs/documentation/demo_4/assets/UC17.png new file mode 100644 index 0000000..025b013 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC17.png differ diff --git a/docs/documentation/demo_4/assets/UC18.png b/docs/documentation/demo_4/assets/UC18.png new file mode 100644 index 0000000..24c0d40 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC18.png differ diff --git a/docs/documentation/demo_4/assets/UC19.png b/docs/documentation/demo_4/assets/UC19.png new file mode 100644 index 0000000..8092bea Binary files /dev/null and b/docs/documentation/demo_4/assets/UC19.png differ diff --git a/docs/documentation/demo_4/assets/UC2.png b/docs/documentation/demo_4/assets/UC2.png new file mode 100644 index 0000000..1806ea4 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC2.png differ diff --git a/docs/documentation/demo_4/assets/UC3.png b/docs/documentation/demo_4/assets/UC3.png new file mode 100644 index 0000000..5b2a83f Binary files /dev/null and b/docs/documentation/demo_4/assets/UC3.png differ diff --git a/docs/documentation/demo_4/assets/UC4.png b/docs/documentation/demo_4/assets/UC4.png new file mode 100644 index 0000000..2561168 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC4.png differ diff --git a/docs/documentation/demo_4/assets/UC5.png b/docs/documentation/demo_4/assets/UC5.png new file mode 100644 index 0000000..fe087c0 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC5.png differ diff --git a/docs/documentation/demo_4/assets/UC6.png b/docs/documentation/demo_4/assets/UC6.png new file mode 100644 index 0000000..5dbe075 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC6.png differ diff --git a/docs/documentation/demo_4/assets/UC7.png b/docs/documentation/demo_4/assets/UC7.png new file mode 100644 index 0000000..b6cc974 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC7.png differ diff --git a/docs/documentation/demo_4/assets/UC8.png b/docs/documentation/demo_4/assets/UC8.png new file mode 100644 index 0000000..f4a7ad5 Binary files /dev/null and b/docs/documentation/demo_4/assets/UC8.png differ diff --git a/docs/documentation/demo_4/assets/UC9.png b/docs/documentation/demo_4/assets/UC9.png new file mode 100644 index 0000000..f79376d Binary files /dev/null and b/docs/documentation/demo_4/assets/UC9.png differ diff --git a/docs/documentation/demo_4/assets/applicationArchitecture.png b/docs/documentation/demo_4/assets/applicationArchitecture.png new file mode 100644 index 0000000..d415155 Binary files /dev/null and b/docs/documentation/demo_4/assets/applicationArchitecture.png differ diff --git a/docs/documentation/demo_4/assets/applicationArchitectureV2.png b/docs/documentation/demo_4/assets/applicationArchitectureV2.png new file mode 100644 index 0000000..a2cb613 Binary files /dev/null and b/docs/documentation/demo_4/assets/applicationArchitectureV2.png differ diff --git a/docs/documentation/demo_4/assets/applicationArchitectureV3.png b/docs/documentation/demo_4/assets/applicationArchitectureV3.png new file mode 100644 index 0000000..3cc160d Binary files /dev/null and b/docs/documentation/demo_4/assets/applicationArchitectureV3.png differ diff --git a/docs/documentation/demo_4/assets/applicationArchitectureV4.png b/docs/documentation/demo_4/assets/applicationArchitectureV4.png new file mode 100644 index 0000000..3b19676 Binary files /dev/null and b/docs/documentation/demo_4/assets/applicationArchitectureV4.png differ diff --git a/docs/documentation/demo_4/assets/applicationArchitectureV5.png b/docs/documentation/demo_4/assets/applicationArchitectureV5.png new file mode 100644 index 0000000..6126822 Binary files /dev/null and b/docs/documentation/demo_4/assets/applicationArchitectureV5.png differ diff --git a/docs/documentation/demo_4/assets/installationAssets/additional_tasks.png b/docs/documentation/demo_4/assets/installationAssets/additional_tasks.png new file mode 100644 index 0000000..69a7a80 Binary files /dev/null and b/docs/documentation/demo_4/assets/installationAssets/additional_tasks.png differ diff --git a/docs/documentation/demo_4/assets/installationAssets/confirm.png b/docs/documentation/demo_4/assets/installationAssets/confirm.png new file mode 100644 index 0000000..67cadfd Binary files /dev/null and b/docs/documentation/demo_4/assets/installationAssets/confirm.png differ diff --git a/docs/documentation/demo_4/assets/installationAssets/finish.png b/docs/documentation/demo_4/assets/installationAssets/finish.png new file mode 100644 index 0000000..9e07b15 Binary files /dev/null and b/docs/documentation/demo_4/assets/installationAssets/finish.png differ diff --git a/docs/documentation/demo_4/assets/installationAssets/install_location.png b/docs/documentation/demo_4/assets/installationAssets/install_location.png new file mode 100644 index 0000000..44b255f Binary files /dev/null and b/docs/documentation/demo_4/assets/installationAssets/install_location.png differ diff --git a/docs/documentation/demo_4/assets/installationAssets/security_warning.png b/docs/documentation/demo_4/assets/installationAssets/security_warning.png new file mode 100644 index 0000000..3d26e2c Binary files /dev/null and b/docs/documentation/demo_4/assets/installationAssets/security_warning.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/additional_features.png b/docs/documentation/demo_4/assets/manualAssets/additional_features.png new file mode 100644 index 0000000..9d763b3 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/additional_features.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/advanced_search.png b/docs/documentation/demo_4/assets/manualAssets/advanced_search.png new file mode 100644 index 0000000..0224e0e Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/advanced_search.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/advanced_search_empty.png b/docs/documentation/demo_4/assets/manualAssets/advanced_search_empty.png new file mode 100644 index 0000000..133ecdf Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/advanced_search_empty.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/bulk.png b/docs/documentation/demo_4/assets/manualAssets/bulk.png new file mode 100644 index 0000000..828f76d Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/bulk.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/bulk_2.png b/docs/documentation/demo_4/assets/manualAssets/bulk_2.png new file mode 100644 index 0000000..ad7bd08 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/bulk_2.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/create_smart_1.png b/docs/documentation/demo_4/assets/manualAssets/create_smart_1.png new file mode 100644 index 0000000..686ed62 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/create_smart_1.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/create_smart_2.png b/docs/documentation/demo_4/assets/manualAssets/create_smart_2.png new file mode 100644 index 0000000..f83e808 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/create_smart_2.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/create_smart_3.png b/docs/documentation/demo_4/assets/manualAssets/create_smart_3.png new file mode 100644 index 0000000..eefac30 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/create_smart_3.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/dashboard.png b/docs/documentation/demo_4/assets/manualAssets/dashboard.png new file mode 100644 index 0000000..29555e3 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/dashboard.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/dashboard_empty.png b/docs/documentation/demo_4/assets/manualAssets/dashboard_empty.png new file mode 100644 index 0000000..9c76bf7 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/dashboard_empty.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/duplicates.png b/docs/documentation/demo_4/assets/manualAssets/duplicates.png new file mode 100644 index 0000000..05eb02e Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/duplicates.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/graph_view.png b/docs/documentation/demo_4/assets/manualAssets/graph_view.png new file mode 100644 index 0000000..51dbfb2 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/graph_view.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/search.png b/docs/documentation/demo_4/assets/manualAssets/search.png new file mode 100644 index 0000000..0ddae95 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/search.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/setting_empty.png b/docs/documentation/demo_4/assets/manualAssets/setting_empty.png new file mode 100644 index 0000000..cc9c750 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/setting_empty.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/smart_manager_empty.png b/docs/documentation/demo_4/assets/manualAssets/smart_manager_empty.png new file mode 100644 index 0000000..b11ec1c Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/smart_manager_empty.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/sort_1.png b/docs/documentation/demo_4/assets/manualAssets/sort_1.png new file mode 100644 index 0000000..db24f0e Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/sort_1.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/sort_2.png b/docs/documentation/demo_4/assets/manualAssets/sort_2.png new file mode 100644 index 0000000..5f937cc Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/sort_2.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/sort_3.png b/docs/documentation/demo_4/assets/manualAssets/sort_3.png new file mode 100644 index 0000000..f64ad9a Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/sort_3.png differ diff --git a/docs/documentation/demo_4/assets/manualAssets/sort_4.png b/docs/documentation/demo_4/assets/manualAssets/sort_4.png new file mode 100644 index 0000000..f898b78 Binary files /dev/null and b/docs/documentation/demo_4/assets/manualAssets/sort_4.png differ diff --git a/docs/documentation/demo_4/assets/readmeAssets/Henco.jpg b/docs/documentation/demo_4/assets/readmeAssets/Henco.jpg new file mode 100644 index 0000000..35ffa73 Binary files /dev/null and b/docs/documentation/demo_4/assets/readmeAssets/Henco.jpg differ diff --git a/docs/documentation/demo_4/assets/readmeAssets/Jack.jpg b/docs/documentation/demo_4/assets/readmeAssets/Jack.jpg new file mode 100644 index 0000000..71617f2 Binary files /dev/null and b/docs/documentation/demo_4/assets/readmeAssets/Jack.jpg differ diff --git a/docs/documentation/demo_4/assets/readmeAssets/banner.png b/docs/documentation/demo_4/assets/readmeAssets/banner.png new file mode 100644 index 0000000..f49ba53 Binary files /dev/null and b/docs/documentation/demo_4/assets/readmeAssets/banner.png differ diff --git a/docs/documentation/demo_4/assets/readmeAssets/dewald.JPG b/docs/documentation/demo_4/assets/readmeAssets/dewald.JPG new file mode 100644 index 0000000..ff461b3 Binary files /dev/null and b/docs/documentation/demo_4/assets/readmeAssets/dewald.JPG differ diff --git a/docs/documentation/demo_4/assets/readmeAssets/philipp.jpg b/docs/documentation/demo_4/assets/readmeAssets/philipp.jpg new file mode 100644 index 0000000..f17812e Binary files /dev/null and b/docs/documentation/demo_4/assets/readmeAssets/philipp.jpg differ diff --git a/docs/documentation/demo_4/assets/readmeAssets/tiaan.jpeg b/docs/documentation/demo_4/assets/readmeAssets/tiaan.jpeg new file mode 100644 index 0000000..e915a79 Binary files /dev/null and b/docs/documentation/demo_4/assets/readmeAssets/tiaan.jpeg differ diff --git a/docs/documentation/demo_4/assets/sfm_deployment.jpg b/docs/documentation/demo_4/assets/sfm_deployment.jpg new file mode 100644 index 0000000..9808b94 Binary files /dev/null and b/docs/documentation/demo_4/assets/sfm_deployment.jpg differ diff --git a/docs/documentation/demo_4/assets/spark_logo_dark_background.png b/docs/documentation/demo_4/assets/spark_logo_dark_background.png new file mode 100644 index 0000000..5fcd122 Binary files /dev/null and b/docs/documentation/demo_4/assets/spark_logo_dark_background.png differ diff --git a/docs/documentation/demo_4/assets/spark_logo_white_background.png b/docs/documentation/demo_4/assets/spark_logo_white_background.png new file mode 100644 index 0000000..46bd3ac Binary files /dev/null and b/docs/documentation/demo_4/assets/spark_logo_white_background.png differ diff --git a/docs/documentation/demo_4/assets/testingAssets/clustering.png b/docs/documentation/demo_4/assets/testingAssets/clustering.png new file mode 100644 index 0000000..d190f49 Binary files /dev/null and b/docs/documentation/demo_4/assets/testingAssets/clustering.png differ diff --git a/docs/documentation/demo_4/assets/testingAssets/go_coverage.png b/docs/documentation/demo_4/assets/testingAssets/go_coverage.png new file mode 100644 index 0000000..36f7f12 Binary files /dev/null and b/docs/documentation/demo_4/assets/testingAssets/go_coverage.png differ diff --git a/docs/documentation/demo_4/assets/testingAssets/go_tests.png b/docs/documentation/demo_4/assets/testingAssets/go_tests.png new file mode 100644 index 0000000..27e045d Binary files /dev/null and b/docs/documentation/demo_4/assets/testingAssets/go_tests.png differ diff --git a/docs/documentation/demo_4/assets/testingAssets/keywords.png b/docs/documentation/demo_4/assets/testingAssets/keywords.png new file mode 100644 index 0000000..e754076 Binary files /dev/null and b/docs/documentation/demo_4/assets/testingAssets/keywords.png differ diff --git a/docs/documentation/demo_4/assets/testingAssets/python_coverage.png b/docs/documentation/demo_4/assets/testingAssets/python_coverage.png new file mode 100644 index 0000000..6ac4b06 Binary files /dev/null and b/docs/documentation/demo_4/assets/testingAssets/python_coverage.png differ diff --git a/docs/documentation/demo_4/assets/testingAssets/python_test.png b/docs/documentation/demo_4/assets/testingAssets/python_test.png new file mode 100644 index 0000000..f1147c6 Binary files /dev/null and b/docs/documentation/demo_4/assets/testingAssets/python_test.png differ diff --git a/docs/documentation/demo_4/assets/testingAssets/reliability.png b/docs/documentation/demo_4/assets/testingAssets/reliability.png new file mode 100644 index 0000000..6302533 Binary files /dev/null and b/docs/documentation/demo_4/assets/testingAssets/reliability.png differ diff --git a/docs/documentation/demo_4/assets/websiteArchitecure.png b/docs/documentation/demo_4/assets/websiteArchitecure.png new file mode 100644 index 0000000..944d058 Binary files /dev/null and b/docs/documentation/demo_4/assets/websiteArchitecure.png differ diff --git a/docs/documentation/demo_4/assets/wireframeAssets/wireframe_advanced_search.png b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_advanced_search.png new file mode 100644 index 0000000..98639f4 Binary files /dev/null and b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_advanced_search.png differ diff --git a/docs/documentation/demo_4/assets/wireframeAssets/wireframe_dashboard.png b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_dashboard.png new file mode 100644 index 0000000..881a603 Binary files /dev/null and b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_dashboard.png differ diff --git a/docs/documentation/demo_4/assets/wireframeAssets/wireframe_main_navigation.png b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_main_navigation.png new file mode 100644 index 0000000..d3a6fd0 Binary files /dev/null and b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_main_navigation.png differ diff --git a/docs/documentation/demo_4/assets/wireframeAssets/wireframe_manager_page.png b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_manager_page.png new file mode 100644 index 0000000..9bdc492 Binary files /dev/null and b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_manager_page.png differ diff --git a/docs/documentation/demo_4/assets/wireframeAssets/wireframe_manager_page2.png b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_manager_page2.png new file mode 100644 index 0000000..ad42d83 Binary files /dev/null and b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_manager_page2.png differ diff --git a/docs/documentation/demo_4/assets/wireframeAssets/wireframe_settings.png b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_settings.png new file mode 100644 index 0000000..395c5b2 Binary files /dev/null and b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_settings.png differ diff --git a/docs/documentation/demo_4/assets/wireframeAssets/wireframe_smart_managers.png b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_smart_managers.png new file mode 100644 index 0000000..6e1d8dc Binary files /dev/null and b/docs/documentation/demo_4/assets/wireframeAssets/wireframe_smart_managers.png differ diff --git a/docs/documentation/demo_4/coding_standards.md b/docs/documentation/demo_4/coding_standards.md new file mode 100644 index 0000000..d3d95b6 --- /dev/null +++ b/docs/documentation/demo_4/coding_standards.md @@ -0,0 +1,176 @@ +

+ Company Logo +

+ +# Coding Standards Document for SparkIndustries + +**Version:** 2.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Personal Development team use + +## Content +* [Introduction](#introduction) +* [Naming Conventions](#naming-conventions) +* [Indentation](#naming-conventions) +* [File Layout](#file-layout) +* [Exception Handling](#exception-handling) +* [Commenting](#commenting) +* [General](#general) +* [Glossary](#glossary) + +## Introduction +The following document serves as the official coding standards document for SparkIndustries. It outlines all best practices and conventions our team will use to ensure the maintenance of a well formed, readable and uniform codebase. Going forward our team will aim to use this document as guidelines for personal development and during our PR review process. + +It is important to take note that our codebase consists of three primary parts each developed in its own language. These are Python, Go and Flutter (Dart). Conventions for these languages may differ. In cases where this observation holds we decide on choosing the conventions accepted by the individual languages rather than trying to force them across multiple programming domains. In any scenarios where this does not hold we provide ample justification for our choices. + + +## Naming Conventions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConceptPythonGoFlutter (Dart)
Variablessnake_casecamelCasecamelCase
Functions / Methodssnake_case (Use type hinting)camelCase (unexported), PascalCase (exported)camelCase
ConstantsALL_CAPSPascalCase (exported), camelCase (unexported)camelCase
Classes / TypesPascalCasePascalCasePascalCase
File Namessnake_case.pycamelCase.gosnake_case.dart
Packages / Modulessnake_caselowercase (no underscores)snake_case
+ +In general we also propose the following conventions: +- Use descriptive variable, class and method names. +- For python: Make explicit use of type hinting. +- Do not use ambigous abbreviations. +- Avoid variable hiding in nested scopes. + +## Indentation +Python, Go and Flutter should all use tabs. This holds true for all use of indentation. + +## File Layout +#### Python + +``` +. +└── Python/ + ├── src/ + │ ├── file1.py + │ ├── file2.py + │ └── file3.py + └── testing / + ├── test_file1.py + ├── test_file2.py + └── test_files/ + ├── testimg.jpeg + └── testdoc.doxc +``` + +### Go +``` +. +└── golang/ + ├── client/ + │ └── all the generated protos + ├── filesystem/ + │ ├── goFile1.go + │ └── goFile2.go + └── gprc/ + ├── client/ + │ └── grpcClient.go + └── server/ + └── grpcServer.go +``` + +### Flutter +``` +. +└── app/ + ├── images + ├── lib/ + │ ├── api.dart + │ ├── constants.dart + │ └── main.dart + ├── linux/ + │ ├── flutter + │ └── runner/ + │ ├── *.cpp + │ └── *.h + ├── macOS/ + │ ├── flutter + │ └── runner/ + │ └── *.swift + ├── test/ + │ └── widget_test.dart + └── windows/ + ├── flutter + └── runner/ + ├── *.cpp + └── *.h +``` + +## Exception Handling +We do not enforce a uniform way to handle exceptions throughout our system due to the varying nature of exception handling in the different project technologies. Developers should follow the best practices associated with their language. +In general we propose the following guidelines: +- Use specific exception types (i.e. IndexOutOfBounds vs RuntimeError) +- Handle Exceptions at as low a level as reasonably possible. Do not continously propogate exceptions up the call-stack. +- Do not "swallow" exceptions. +- Log exceptions with clear and descriptive messages. +- Avoid using exceptions for flow control +- Be mindful of logging sensitive information in error messages. + +## Commenting +We propose the following guidelines for commenting: +- Comment complex logic instead of blindly commenting what can be inferred from method names +- Make use of TODO comments where applicable +- Where multiple approaches could be taken, use comments to explain rationale for choosing one. +- Do not commit large commented out blocks. Make use of version control for its intended use. +- Explain **why** not **what** + +## General +The following general guidelines must be kept in mind: +- Do not implement functionality already present in a used library. + + +## Glossary +In an effort to keep this document concise we do not belabour what we mean by well known naming conventions. We briefly explain them here for easy reference. + +* camelCase: Entails combining multiple words into a single word with the first letter of each word (except the very first word) capitalized. E.g. myName, fooBar. +* snake_case: Entails combining multiple words into a single word separated by underscores. E.g. my_name, foo_bar. +* PascalCase: Entails combining multiple words into a single word with the first letter of each word capitalized. diff --git a/docs/documentation/demo_4/grpc.md b/docs/documentation/demo_4/grpc.md new file mode 100644 index 0000000..21f30b3 --- /dev/null +++ b/docs/documentation/demo_4/grpc.md @@ -0,0 +1,230 @@ +

+ Company Logo +

+ +# gRPC endpoint Documentation + +**Version:** 4.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Southern Cross Solutions & Personal Development team use + +## Content +* [Introduction](#introduction) +* [A note on security](#a-note-on-security) +* [Data_Sent_and_Received](#data-sent-and-received) +* [Protocol_Buffer_Files](#protocol-buffer-files-proto) + +## Introduction +Our team makes use of gRPC for communication between Go and Python. Our team decided on using gRPC rather than traditional HTTP requests due to the following advantages: + +* High Performance: gRPC uses protocol buffers and HTTP/2 which allows us to keep smart managing files as fast and efficient as possible. +* Cross Platform: Since our project combined both Go and Python gRPC is an ideal candidate for this project. +* Contract-First Development: Allows us as developers to agree on the exact format of data sent and received by request and responses promoting a well-structured codebase and concurrent development by the go and python team. +* Reduced network usage: Protocol buffers use binary serialization which reduces network traffic. + +This document serves as the exact documentation for the services and messages sent and received. + +## A note on security +Our application is deployed as a standalone desktop application which does not rely on (or use) internet connection. All servers simply run on localhost and cannot be accessed by any malicious user, except possibly the user of the app themselves. Should the user (for some reason) decide to do so they can do no more damage to their system then what they could do using the standard file explorer. Like any application it is protected from unauthorized use by the standard system login functionality. It is for this reason that our endpoints are unprotected and does not require authentication via a secret key based solution or other similar methods. Please note that we have discussed this decision with Mr. Avinash Singh who is both, involved in both the COS301 lecturing team, and COS330 module coordinator. He has approved this decision from a security point of view, noting that we should perhaps add a login to the application itself, a feature we will be adding for demo4. + +## Data sent and received +Go must extract and send the following information to python processor: +* Current directory tree structure +* Filenames contained in directory tree (includes the extension) +* Tags defined and added for specific file + +After reorganising the folder structure based on output from the clustering algorithm the following information must be sent back to go for creating the new file strucutre. +* Newly organised directory tree structure (not necessarily containing the same folders as before) +* Filenames contained in the directory tree (Includes the old location of the file on disk) +* Tags defined for original files + +The following JSON serves as an example of what information will be sent. Note that JSON is not over gRPC and this merely serves as an example of how the data that we're sending might be structured. + +``` +{ + "name": "root", + "path": "/root", + "files": [ + { + "name": "readme.md", + "path": "/root/readme.md", + "tags": ["intro"] + } + ], + "directories": [ + { + "name": "docs", + "path": "/root/docs", + "files": [ + { + "name": "report.pdf", + "path": "/root/docs/report.pdf", + "tags": ["finance", "q1"] + }, + { + "name": "notes.txt", + "path": "/root/docs/notes.txt", + "tags": ["meeting"] + } + ], + "directories": [] + }, + { + "name": "images", + "path": "/root/images", + "files": [ + { + "name": "logo.png", + "path": "/root/images/logo.png", + "tags": ["brand"] + }, + { + "name": "graph.jpg", + "path": "/root/images/graph.jpg", + "tags": ["analytics"] + } + ], + "directories": [] + } + ] +} +``` + +## Protocol Buffer Files (.proto) +The following .proto files can be created to represent the information that needs to be communicated as explained [here](#data-sent-and-received): + +``` +syntax = "proto3"; + +// Simple tags defined by users +message Tag { + string name = 1; +} + +// Key-val metadata as extracted (don't use maps as this is more portable for go) +message MetadataEntry { + string key = 1; + string value = 2; +} + +// Keyword extracted from file along with the score it was given by yake +message Keyword { + string keyword = 1; + float score = 2; +} + +message File { + string name = 1; + string original_path = 2; + string new_path = 3; + repeated Tag tags = 4; + repeated MetadataEntry metadata = 5; + bool is_locked = 6; + repeated Keyword keywords = 6; +} + +message Directory { + string name = 1; + string path = 2; + repeated File files = 3; + repeated Directory directories = 4; + bool is_locked = 5; +} + +// For request/response messages +// Go to python +message DirectoryRequest { + Directory root = 1; + string requestType = 2; // This can be CLUSTERING or METADATA or KEYWORDS + string prefferedCase = 3; // This can be CAMEL or PASCAL or SNAKE or KEBAB + string serverSecret = 4; +} + +// Python to go +message DirectoryResponse { + Directory root = 1; + int32 response_code = 2; + string response_msg = 3; +} + +service DirectoryService { + rpc SendDirectoryStructure(DirectoryRequest) returns (DirectoryResponse); +} + +``` + +The .proto files may then be used generate the required Go and Python code via +``` +protoc --go_out=. directory.proto // Go +python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. directory.proto // Python + +``` + + +Which may then be used in implementation + +``` +// Go +package main + +import ( + "context" + "log" + + "google.golang.org/grpc" + pb "your-generated-go-package" // Import your generated Go protobuf package +) + +func main() { + // Set up gRPC client connection + conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) + if err != nil { + log.Fatalf("Failed to connect: %v", err) + } + defer conn.Close() + + // Create gRPC client + client := pb.NewDirectoryServiceClient(conn) + + // Prepare directory structure (omitted for brevity) + + // Send directory structure to Python server + response, err := client.SendDirectoryStructure(context.Background(), &pb.DirectoryRequest{ /* directory structure */ }) + if err != nil { + log.Fatalf("Error sending directory structure: %v", err) + } + + // Handle response from Python server (updated directory structure) + // process response +} + +``` + +``` +// Python +import grpc +from concurrent import futures +import directory_pb2 +import directory_pb2_grpc + +class DirectoryService(directory_pb2_grpc.DirectoryServiceServicer): + def SendDirectoryStructure(self, request, context): + # Process directory structure (apply clustering algorithms, etc.) + # Construct updated directory structure + response = directory_pb2.DirectoryResponse( /* updated directory structure */ ) + return response + +def serve(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + directory_pb2_grpc.add_DirectoryServiceServicer_to_server(DirectoryService(), server) + server.add_insecure_port('[::]:50051') + server.start() + server.wait_for_termination() + +if __name__ == '__main__': + serve() + +``` + +**Important Note** +In .proto3 all field are optional by default. If a field is not set then the value is simply set to the field's default type. Hence if a field is not needed, for example the new_path or MetadataEntry in a DirectoryRequest message it should simply be left empty. \ No newline at end of file diff --git a/docs/documentation/demo_4/installer_guide.md b/docs/documentation/demo_4/installer_guide.md new file mode 100644 index 0000000..4e8bc0e --- /dev/null +++ b/docs/documentation/demo_4/installer_guide.md @@ -0,0 +1,523 @@ +# Complete Guide: Flutter App to Windows EXE with Servers + +## Prerequisites + +- Flutter SDK installed and configured +- Go installed and configured +- Python with PyInstaller installed (`pip install pyinstaller`) +- Inno Setup Compiler installed +- Bat to EXE converter (like Bat To Exe Converter or Advanced BAT to EXE Converter) + +## Step 1: Prepare Build Environment + +1. **Create build directory structure** + + ``` + project_root/ + ├── build/ (add this folder) + │ └── storage/ (add this folder) + ├── golang/ (your Go server code) + ├── python/ (your Python server code) + ├── app/ (your Flutter app) + └── installer.iss + ``` + +## Step 2: Update Go Server for Windows Deployment + +Before compiling the Go server, you need to update the path handling code to work correctly on Windows instead of WSL paths. + +1. **Update the imports in directoryToObject.go** + + Add `"runtime"` to your imports section: + + ```go + import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" // Add this import + "strings" + "time" + // ... other imports + ) + ``` + +2. **Update the ConvertToWSLPath function** + + Replace the existing `ConvertToWSLPath` function with this Windows-compatible version: + + ```go + func ConvertToWSLPath(winPath string) string { + winPath = strings.TrimSpace(winPath) + + // Check if running on Windows + if runtime.GOOS == "windows" { + // For Windows deployment: normalize path separators and clean up double slashes + winPath = strings.ReplaceAll(winPath, "\\", "/") + // Remove any double slashes that might have been created + for strings.Contains(winPath, "//") { + winPath = strings.ReplaceAll(winPath, "//", "/") + } + return winPath + } + + // Original WSL conversion logic for non-Windows systems + winPath = strings.ReplaceAll(winPath, "\\", "/") + if len(winPath) > 2 && winPath[1] == ':' { + drive := strings.ToLower(string(winPath[0])) + rest := winPath[2:] + return "/mnt/" + drive + rest + } + + return winPath + } + ``` + + This change ensures that: + + - On Windows: Paths are normalized with forward slashes and double slashes are removed + +## Step 3: Compile Go Server + +Navigate to your Go server directory (cd golang) and run: + +```bash +go build -ldflags="-H=windowsgui" -o ../build/go_server.exe . +``` + +**Note**: The `-H=windowsgui` flag prevents console windows from appearing. + +## Step 4: Compile Python Server + +### 4.1 Download and Setup SentenceTransformers Model + +First, download the required model to your project (cd python/src): + +```python +# download_model.py +from sentence_transformers import SentenceTransformer +import os + +# Create models directory in your project +models_dir = './models' +os.makedirs(models_dir, exist_ok=True) + +print("Downloading all-MiniLM-L6-v2 model...") +# Download directly to your project folder for offline use +model = SentenceTransformer('all-MiniLM-L6-v2', cache_folder=models_dir) + +print("✅ Model downloaded successfully!") + +# Test the model +embeddings = model.encode(["Hello world", "This is a test"]) +print(f"✅ Model working! Embedding shape: {embeddings.shape}") + +# Show what was downloaded +print(f"\nModel saved to: {models_dir}") +for root, dirs, files in os.walk(models_dir): + level = root.replace(models_dir, '').count(os.sep) + if level < 3: # Don't go too deep + indent = ' ' * 2 * level + print(f"{indent}{os.path.basename(root)}/") +``` + +Run this script: + +```bash +python download_model.py +``` + +### 4.2 Add file runtime_hook_transformers.py + +This file validates the YAKE stopwords: + +```python +# runtime_hook_transformers.py +import os +import yake + +yake_path = os.path.dirname(yake.__file__) +stopwords_path = os.path.join(yake_path, 'core', 'StopwordsList') + +print(f"YAKE path: {yake_path}") +print(f"Stopwords path: {stopwords_path}") +print(f"Exists: {os.path.exists(stopwords_path)}") + +# Test the specific file that was missing +missing_file = os.path.join(stopwords_path, 'stopwords_noLang.txt') +print(f"Missing file exists: {os.path.exists(missing_file)}") + +if os.path.exists(missing_file): + print("✅ Ready to build!") +else: + print("❌ File still missing") +``` + +Keep this file in the python/src directory. + +### 4.3 Create python_server.spec in python/src + +Your spec file needs to include both the SentenceTransformers model and YAKE stopwords: + +```python +# -*- mode: python ; coding: utf-8 -*- +import os +import yake + +# Get the yake stopwords path +yake_path = os.path.dirname(yake.__file__) +stopwords_path = os.path.join(yake_path, 'core', 'StopwordsList') + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[ + ('models', 'models'), # Include the downloaded SentenceTransformers model + (stopwords_path, 'yake/core/StopwordsList'), # Include YAKE stopwords files + ], + hiddenimports=[ + 'magic', 'mutagen', 'docx', 'PIL', 'pymediainfo', + 'grpc', 'grpc_tools', 'yake', 'yake.core', 'yake.core.yake', + 'pandas', 'sklearn', 'sentence_transformers', + 'sentence_transformers.models', 'sentence_transformers.SentenceTransformer', + 'nltk', 'pyinstrument', 'fitz', 'fitz._fitz', + 'torch', 'tokenizers', 'transformers', + 'transformers.models', 'transformers.models.auto', 'numpy', + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=['runtime_hook_transformers.py'], + excludes=[], + noarchive=False, + optimize=0, +) + +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='python_server', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, # This prevents console window + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +``` + +### 4.4 Update your Python code for offline model loading + +Update your RequestHandler class to properly load the model in both development and PyInstaller environments: + +```python +# request_handler.py +from concurrent import futures +from sentence_transformers import SentenceTransformer +import grpc +import message_structure_pb2_grpc +import os +import sys + +import master + +class RequestHandler(message_structure_pb2_grpc.DirectoryServiceServicer): + + def __init__(self): + # Configure for offline mode + os.environ["TRANSFORMERS_OFFLINE"] = "1" + + # Determine the correct path for the model + if hasattr(sys, '_MEIPASS'): + # When running as PyInstaller bundle + cache_folder = os.path.join(sys._MEIPASS, 'models') + else: + # When running in development + cache_folder = './models' + + print(f"Loading model with cache_folder: {cache_folder}") + + # Load SentenceTransformers model with local files only + transformer = SentenceTransformer('all-MiniLM-L6-v2', + cache_folder=cache_folder, + local_files_only=True) + self.master = master.Master(10, transformer) + + def SendDirectoryStructure(self, request, context): + response = self.master.submit_task(request).result() + return response + + def serve(self): + port = "50051" + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + message_structure_pb2_grpc.add_DirectoryServiceServicer_to_server(self, server) + server.add_insecure_port("[::]:" + port) + server.start() + print("Server started, listening on " + port) + server.wait_for_termination() +``` + +### 4.5 Compile Python server + +```bash +python -m PyInstaller --clean python_server.spec +``` + +Move the generated `python_server.exe` in the dist folder to your `build/` folder. + +## Step 5: Build Flutter Application + +1. **Build Flutter for Windows** + + ```bash + cd flutter_app + flutter build windows --release + ``` + +2. **Copy and rename** + ```bash + cp -r build/windows/runner/Release ../build/FlutterApp + ``` + +## Step 6: Create Enhanced Launcher with Progress Bar + +### 6.1 Create launcher.bat inside the build folder + +```batch +@echo off +setlocal EnableDelayedExpansion +title Smart File Manager Launcher +cd /d "%~dp0" + +REM Create a simple progress display +echo. +echo ==================================================== +echo ^| Smart File Manager ^| +echo ^| Starting Up... ^| +echo ==================================================== +echo. + +echo [----------] 0%% - Initializing... +timeout /t 1 >nul + +echo [##--------] 20%% - Starting Go server... +start /B "" "%~dp0go_server.exe" + +echo [####------] 40%% - Starting Python server (this may take a while)... +start /B "" "%~dp0python_server.exe" + +echo [######----] 60%% - Waiting for servers to be ready... + +REM Wait for Go server (port 51000) - no timeout, wait indefinitely +:waitgo +timeout /t 2 >nul +netstat -an 2>nul | find "LISTEN" | find ":51000" >nul +if errorlevel 1 ( + echo Waiting for Go server... + goto waitgo +) + +echo [########--] 80%% - Go server ready! + +REM Wait for Python server (port 50051) - no timeout, wait indefinitely +:waitpython +timeout /t 2 >nul +netstat -an 2>nul | find "LISTEN" | find ":50051" >nul +if errorlevel 1 ( + echo Waiting for Python server... + goto waitpython +) + +echo [##########] 100%% - All servers ready! +echo. +echo Launching Smart File Manager... +timeout /t 1 >nul + +REM Launch Flutter app +if exist "%~dp0FlutterApp\app.exe" ( + start "" "%~dp0FlutterApp\app.exe" + echo Flutter app launched successfully +) else ( + echo ERROR: app.exe not found in FlutterApp folder + echo Expected location: %~dp0FlutterApp\app.exe + pause + goto cleanup +) + +REM Wait for Flutter app to close +:checkapp +timeout /t 2 >nul +tasklist /fi "imagename eq app.exe" 2>nul | find /i "app.exe" >nul +if %errorlevel%==0 goto checkapp + +:cleanup +echo. +echo App closed, cleaning up servers... +taskkill /F /IM "go_server.exe" 2>nul +taskkill /F /IM "python_server.exe" 2>nul +echo Cleanup complete. +timeout /t 2 >nul +exit /b 0 +``` + +### 6.2 Convert BAT to EXE + +Use a BAT to EXE converter with these **critical** settings: + +- **Visibility**: Set to "Visible" (not hidden) so you can see the progress bar +- **Include icon**: Add your app icon (inside FlutterApp folder under assets) + +Name the output `launcher.exe` and place it in the `build/` folder. + +## Step 7: Create Installer with Inno Setup in root directory + +### 7.1 installer.iss + +```ini +[Setup] +AppName=Smart File Manager +AppVersion=1.0 +AppPublisher=Southern Cross Solutions +DefaultDirName={autopf}\SmartFileManager +DefaultGroupName=Smart File Manager +OutputBaseFilename=SmartFileManagerSetup +Compression=lzma2/ultra +SolidCompression=yes +SetupIconFile=icon.ico +WizardStyle=modern +DisableProgramGroupPage=yes +PrivilegesRequired=lowest +; Ignore file access errors during installation +IgnoreTasksCheck=yes + +[Files] +Source: "build\go_server.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "build\python_server.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "build\launcher.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "build\storage\*"; DestDir: "{app}\storage"; Flags: recursesubdirs createallsubdirs ignoreversion +Source: "build\FlutterApp\*"; DestDir: "{app}\FlutterApp"; Flags: recursesubdirs createallsubdirs ignoreversion + +[Icons] +Name: "{group}\Smart File Manager"; Filename: "{app}\launcher.exe"; IconFilename: "{app}\FlutterApp\data\flutter_assets\assets\logo.ico" +Name: "{commondesktop}\Smart File Manager"; Filename: "{app}\launcher.exe"; IconFilename: "{app}\FlutterApp\data\flutter_assets\assets\logo.ico"; Tasks: desktopicon + +[Tasks] +Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional icons:" + +[Run] +Filename: "{app}\launcher.exe"; Description: "Launch Smart File Manager"; Flags: nowait postinstall skipifsilent + +[UninstallRun] +Filename: "taskkill"; Parameters: "/F /IM go_server.exe"; Flags: runhidden skipifdoesntexist +Filename: "taskkill"; Parameters: "/F /IM python_server.exe"; Flags: runhidden skipifdoesntexist +Filename: "taskkill"; Parameters: "/F /IM app.exe"; Flags: runhidden skipifdoesntexist + +[Code] +function InitializeSetup(): Boolean; +var + ResultCode: Integer; +begin + // Kill any running instances before installation + Exec('taskkill', '/F /IM go_server.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('taskkill', '/F /IM python_server.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('taskkill', '/F /IM app.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Result := True; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + ResultCode: Integer; +begin + if CurStep = ssPostInstall then + begin + // Add Windows Firewall exceptions for the servers (ignore errors) + try + Exec('netsh', 'advfirewall firewall add rule name="Smart File Manager - Go Server" dir=in action=allow program="' + ExpandConstant('{app}') + '\go_server.exe" enable=yes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('netsh', 'advfirewall firewall add rule name="Smart File Manager - Python Server" dir=in action=allow program="' + ExpandConstant('{app}') + '\python_server.exe" enable=yes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + + // Also add outbound rules + Exec('netsh', 'advfirewall firewall add rule name="Smart File Manager - Go Server Out" dir=out action=allow program="' + ExpandConstant('{app}') + '\go_server.exe" enable=yes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('netsh', 'advfirewall firewall add rule name="Smart File Manager - Python Server Out" dir=out action=allow program="' + ExpandConstant('{app}') + '\python_server.exe" enable=yes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + except + // Silently continue if firewall rules fail + end; + end; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + ResultCode: Integer; +begin + if CurUninstallStep = usPostUninstall then + begin + // Remove firewall rules during uninstall (ignore errors) + try + Exec('netsh', 'advfirewall firewall delete rule name="Smart File Manager - Go Server"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('netsh', 'advfirewall firewall delete rule name="Smart File Manager - Python Server"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('netsh', 'advfirewall firewall delete rule name="Smart File Manager - Go Server Out"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('netsh', 'advfirewall firewall delete rule name="Smart File Manager - Python Server Out"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + except + // Silently continue if firewall rule deletion fails + end; + end; +end; +``` + +### 7.2 Compile installer + +1. Open Inno Setup Compiler +2. Load your `installer.iss` file +3. Click **Build** → **Compile** +4. The installer will be created as `SmartFileManagerSetup.exe` in the Output folder + +## Step 8: Final Directory Structure + +Your final `build/` directory should look like: + +``` +build/ +├── storage/ +├── FlutterApp/ +│ ├── app.exe +│ ├── data/ +│ │ ├── app.so +│ │ ├── flutter_assets/ +│ │ │ ├── AssetManifest.bin +│ │ │ ├── AssetManifest.json +│ │ │ ├── assets/ +│ │ │ │ └── logo.ico +│ │ │ ├── FontManifest.json +│ │ │ ├── fonts/ +│ │ │ │ └── MaterialIcons-Regular.otf +│ │ │ ├── images/ +│ │ │ │ └── logo.png +│ │ │ ├── NativeAssetsManifest.json +│ │ │ ├── NOTICES.Z +│ │ │ ├── packages/ +│ │ │ │ └── cupertino_icons/ +│ │ │ │ └── assets/ +│ │ │ │ └── CupertinoIcons.ttf +│ │ │ └── shaders/ +│ │ │ └── ink_sparkle.frag +│ │ └── icudtl.dat +│ ├── flutter_windows.dll +│ ├── screen_retriever_windows_plugin.dll +│ ├── tray_manager_plugin.dll +│ ├── url_launcher_windows_plugin.dll +│ └── window_manager_plugin.dll +├── go_server.exe +├── python_server.exe +└── launcher.exe +``` \ No newline at end of file diff --git a/docs/documentation/demo_4/non_functional_tests.md b/docs/documentation/demo_4/non_functional_tests.md new file mode 100644 index 0000000..f1720e6 --- /dev/null +++ b/docs/documentation/demo_4/non_functional_tests.md @@ -0,0 +1,78 @@ +

+ Company Logo +

+ +# Non Function Testing Document + +**Version:** 1.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Demo4 Purposes + +## Content +* [Introduction](#introduction) +* [Performance Test](#performance-testing) +* [Reliability Test](#reliability-test) + +## Introduction +The following document outlines some of the non functional testing we performed to ensure the quality requirements as outlined in our documentation. +It is broken up into specific non functional tests that we performed. It details why these tests were needed, what quality requirement it aims to test, how the tests were performed and the test results. + +## Performance Testing + +To ensure the adequate performance of our application we test 2 features specifically namely our clustering (semantic sorting) and keyword (used for smart search) endpoints. Both of the mentioned endpoints were tested due to them being the most performance intensive. + +### Expected Results +We aim to ensure that both of these endpoints scales at least linearly in execution time for an increasing number of files. We cannot place a numerical value on exactly how long these tests should take, as it is heavily dependant on the hardware of the user using the service. We note the hardware of the machine used to run these tests in our experimental procedure. + +### Experimental Procedure + +To properly test these endpoints we conduct an experiment running both the clustering and keyword service for an increasing number of files. We make use of the same file types containing the same content (in this test a textfile). Note that different file types could slightly influence the results. To ensure accurate results we also run each test 3 times and take the average of the results to report on. + +To run these tests make use of the following command: +``` +make python_non_functional +``` + +These tests were executed on a machine with the following hardware: +* CPU: AMD Ryzen 5 5600H 3.30GHz +* RAM: 16GB 3200 MT/s +* DISK: Samsung SSD 980 500GB +Note that Disk speed in particular is important as the results are endpoints are bottlenecked by the I/O subsystem, owing to the files needing to be opened to extract metadata and keywords. + +### Results +#### Clustering + +Plotting the results obtained from our test yields the following results + +![Clustering Results](assets/testingAssets/clustering.png) + +As can be seen the clustering endpoint scales linearly in time with the number of files. + +### Keywords + +Plotting the results obtained from our test yields the following results + +![Keywords Results](assets/testingAssets/keywords.png) + +As can be seen the keyword endpoint scales linearly in time with the number of files. + +## Reliability Test + +### Expected Results +To ensure the reliability of our application we test that file integrity is preserved by our file operations. When performing a sorting operation (which will both traverse the directory and extract keywords) we must ensure that no file changes, or is somehow modified during the operation. + +### Experimental Procedure + +We conduct an experiment where we calculate the hashes for a subset of files. We then perform a sorting operation (which also extracts keywords) and afterwards we recalculate the file hashes. We compare these results to ensure that no file is modified by the operation. + +### Results + + +To run this test make use of the following command: +``` +make python_non_functional +``` + +Having run this test we can see from the output that no file's integrity has been compromised by any of the operations as the file's SHA256 hash remained unchanged. + +![Reliability Test](assets/testingAssets/reliability.png) \ No newline at end of file diff --git a/docs/documentation/demo_4/researchDoc.md b/docs/documentation/demo_4/researchDoc.md new file mode 100644 index 0000000..a38e3ca --- /dev/null +++ b/docs/documentation/demo_4/researchDoc.md @@ -0,0 +1,251 @@ +# Capstone SFM Research Document + +# Document Overview + +This document aims to serve as a research document that discusses details regarding the implementation of the SFM project. It mainly focuses on the approach that will be taken to implement the “auto place” functionality used to determine where in a folder structure a file should be placed based on metadata. + +# File Organization + +## How are file usually organized and its characteristics + +The standard way of organizing files is based on the context in which the file is used. For example for University, files may be organized in subdirectories by Study Year ⇒ Semester ⇒ Module ⇒ Assignment Type ⇒ etc… + +A structured approach as outlined allows for files to be found in a directory not by memorizing the location but by answering simple questions about the use of said file, serving as directions to its location. + +Importantly it should then be noted that the files are not organized by some single metric but rather based on the “semantic value” that the file has. This forms the centre of our definition of “smart” in the context of a file manager. A smart file manager should be able to autonomously detect the correct context of a given file while adhering to best organizational practices. + +We define a series of such organizational practices as follows: + +1. The relation between entries (files or subdirectories) in a directory should be visible at first glance +2. Directories should impose a maximum limit of the amount of entries (should be user defined) +3. Given a directory tree, files should preferably be located at a similar “level” i.e. a directory should not contain only subdirectories with a single file, as this would seem out of place. + +# Collecting Information On Files + +In order to make informed decisions on where a file must be located we first need adequate data to base such a decision on. + +**Metadata** + +Modern files contain metadata which contains useful information to characterize a file. Metadata may be extracted from a file using Python modules such as Pillow or Piexif. The type of metadata that may be extracted from a file varies depending on the file type (metadata in itself) but for a .pdf includes: + +- checksum +- file_name +- file_size +- file_type +- mime_type +- pdf_version +- linearized +- page_count +- page_mode +- title +- creator +- create_date +- etc… + +**Keywords** + +Text based files such as .pdf, .txt, .rtf, .html etc… may further be scanned for common keywords. A high frequency of certain keywords may then also be used as keywords common among certain files may imply a logical connection between them. Note that care must be taken to sanitize keywords to remove redundant information (commonly used words such as indefinites etc.). + +**Tags** + +Users may also wish to apply their own tags to a file to help the system better label the file. + +# Extracting Information from File Data + +Having looked at a variety of sources that may be used to gain information on the file it follows that some process must then be followed to organize the files based on this data. This involves parsing the data to determine a similarity between different files. Files may then be grouped via this similarity into a directory tree structure. + +These types of classification based problems lend themselves to AI based solutions. We specifically consider clustering approaches for the problem. + +## K-means clustering + +K means is an unsupervised machine learning model that can be used to group unlabelled data (files) into different clusters (directories). This is the same approach used by online retailers to provide functionality such as “users also bought”. The primary challenge with this approach involves converting our data (vectors of metadata and keywords) into a form for which a “distance” metric may be assigned. + +### Encoding Metadata + +Note: To achieve all functionality in this explanation we make use of scikit-learn open source library for machine learning in python + +Suppose we are given 3 files for which we have extracted the following metadata + +```json +files = [ + { + "filename": "invoice_q1.pdf", + "keywords": ["invoice", "payment", "q1"], + "size_kb": 120, + "filetype": "pdf" + }, + { + "filename": "meeting_notes.docx", + "keywords": ["meeting", "project", "minutes"], + "size_kb": 85, + "filetype": "docx" + }, + { + "filename": "holiday_photo.jpg", + "keywords": ["holiday", "beach", "family"], + "size_kb": 2048, + "filetype": "jpg" + } +] +``` + +**Vectorizing keywords** + +We first vectorize the keywords into a numerical format TF-IDF (term frequency-inverse document frequency) - a numerical statistic which can be used to evaluate the importance of word across a collection of documents. + +To do this we combine the keywords across all files into a single array and use the *TfidfVectorizer.* Keywords may then be represented as floating points values for example: + +```json +files = [ + { + "filename": "invoice_q1.pdf", + "keywords_vectorized": [0.32, 0.58, 0.392], + + }, + { + "filename": "meeting_notes.docx", + "keywords_vectorized": [0.42, 0.2421, 0.99], + + }, + { + "filename": "holiday_photo.jpg", + "keywords_vectorized": [0.23, 0.69, 0.92], + } +] +``` + + Note: Removed other keys for brevity + +Some pseudocode + +```python +from sklearn.feature_extraction.text import TfidfVectorizer + +# Join keywords into a single string for each file +docs = [" ".join(file["keywords"]) for file in files] + +vectorizer = TfidfVectorizer() +keyword_vectors = vectorizer.fit_transform(docs).toarray() +``` + +**Vectorizing categorical data** + +Categorical data such as filetypes may be encoded using “One-hot” encoding. An encoding scheme to represent categorical data as binary vectors. This takes each category and converts it into a column of a vector with a 1 indicating the category is present and 0 indicating an absence. + +Suppose we support only the file types “.pdf”, “.jpg”, “.docx” then file type could be encoded as + +```json +files = [ + { + "filename" : "invoice_q1.pdf", + "filetype_encoded" : [1, 0, 0] // 1 in index 0 ==> .pdf + } + { + "filename" : "meeting_notes.docx", + "filetype_encoded" : [0, 0, 1] // 1 in index 2 ==> .docx + } +] +``` + +Note: Removed other keys for brevity + +Some pseudocode + +```python +from sklearn.preprocessing import OneHotEncoder +import numpy as np + +filetypes = [[file["filetype"]] for file in files] +encoder = OneHotEncoder(sparse=False) +filetype_vectors = encoder.fit_transform(filetypes) +``` + +**Normalization of other data** + +Other numerical data such as the file size should also be normalized to fit in the range [0..1]. + +Some pseudocode + +```python +from sklearn.preprocessing import MinMaxScaler + +sizes = np.array([[file["size_kb"]] for file in files]) +scaler = MinMaxScaler() +size_vectors = scaler.fit_transform(sizes) + +``` + +**Bringing it all together** + +Finally we concatenate all vectors to form a n-dimensional real number based representation of all our data on the files. This data may then be used to perform K-means clustering on + +Some pseudocode + +```python +full_vectors = np.hstack([keyword_vectors, filetype_vectors, size_vectors]) +``` + +All data should then be represented as follows + +```json +files = [ + { + "full_vector" : [0.56, 0.87, 0.34 1,0,0, 0.02] + }, + { + "full_vector" : [0.26, 0.17, 0.54 0,0,1, 0.24] + + }, + { + "full_vector" : [0.23, 0.45, 0.45 0,1,0, 0.22] + } +] +``` + +In reality these vectors will be considerably longer + +### Running K-means + +The sci-kit learn K-means algorithm can then be applied to the data + +[https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html) + +This function also includes parameters for number of clusters to generate. How to choose initial centroid clusters etc… + +The K-means algorithm may also be used recursively for generating a directory tree up to a certain depth. Consider the following high level overview of such an implementation + +```python +# I wrote this while very tired so if it makes no sense sorry... +def findClusters(files, nrClusters): + + if nrSubTrees > 0: + Run K-means with nrClusters + for each cluster: + Create directory with appropriate name + findClusters(cluster_files, nrClusters) + else: + place files in current cluster + +findClusters(original_files, 3) # Creates folder-strucutre with 3 sub-dirs for each dir + +``` + +## Alternative and additional ways of extracting information + +### HDBSCAN + +Another built in method in scikit-learn, this clusters data using hierarchical density based clustering. + +Subject to the same encoding scheme as describe above + +## Cosine Similarity + +If keywords lists may be converted into term-frequency vectors cosine similarity + +$Consine(A,B) = (A.B) / ||A||||B||$ + +which may capture overlap and similarity between two files’ keywords. + +### Semantic Embedding (BERT) + +Idk what this is exactly yet but seems cool \ No newline at end of file diff --git a/docs/documentation/demo_4/srs.md b/docs/documentation/demo_4/srs.md new file mode 100644 index 0000000..e0a19e8 --- /dev/null +++ b/docs/documentation/demo_4/srs.md @@ -0,0 +1,638 @@ +

+ Company Logo +

+ +# System Requirements Specifications + +**Version:** 3.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Southern Cross Solutions + +## Content + +- [Introduction](#introduction) +- [User Stories / User Characteristics](#user-stories--user-characteristics) +- [Use Cases / Use Case Diagram](#use-cases--use-case-diagram) +- [Functional Requirements](#functional-requirements) +- [Service Contract](#service-contract) +- [Domain Model](#domain-model) +- [Architectural Requirements](#architectural-requirements-please-see-architecturemd-linked-below) + - [Quality Requirements](#quality-requirements) + - [Architectural Patterns](#architectural-patterns) + - [Design Patterns](#design-patterns) + - [Constraints](#constraints) +- [Technology Requirements](#technology-requirements) +- [Deployment Model](#deployment-model) + +## Introduction + +The digital age seems to evolve at nearly the speed of light, yet somehow file managers are still learning to take their first steps. Cluttered desktops, folder mazes and the laborious hassle of organizing your files are not tasks the modern computer user should be plagued by. + +Meet Smart File Manager (SFM) your new digital housekeeper! + +SFM aims to take advantage of the power of metadata, natural language processing and AI clustering algorithms to automate file management. +Whether you are an everyday general computer user, server administrator, tech-whizz or anything in between, SFM will automate your file management needs. + +Lay down some house rules - place filters, constraints and locks on your folders to help ensure SFM is doing its possible best job. +Looking for that one file from 2 weeks ago that happened to be important all along - Ask SFM to do a smart context search using whatever semantic information you might still remember. +Navigate your directories with an intuitive mind map exposing the underlying web connecting your files. + +SFM is not just another tool, it's your new digital housekeeper, your AI file finder and your new best friend. + +## User Stories / User Characteristics + +### 1. User Story: Register on portal + +**As a:** New user. +**I want to:** Register for an account using either the web portal or after downloading the app. +**So that I:** I can log in and sync all my preferences across multiple devices. + +#### Acceptance criteria: + +**Given** I am a new user. +**When** I navigate to either the web portal registration page or choose register on the application. +**Then** My account is created and I should be notified that it took place successfully. +**Covers:** UC19 + +### 2. User Story: Download and Install the Application + +**As a:** New or registered user. +**I want to:** Download and install the SFM app on to my device. +**So that I:** Can use the application to manage my files. + +#### Acceptance criteria: + +**Given** I have access to the SFM portal and my system meets requirements. +**When** I click the correct download button for my platform (Windows /macOS / linux) and run the installer. +**Then** The application is successfully installed and ready for configuration. +**Covers:** UC1 + +### 3. User Story: Log into the application + +**As a:** Registered User. +**I want to:** Log in on any device with the application installed. +**So that I:** May have my preferences like themes and naming conventions apply on this device. + +#### Acceptance criteria: + +**Given** I am a registered user. +**When** I enter my information and click log in. +**Then** My details are authenticated and I am navigated to the application dashboard. +**Covers:** UC2 + +### 4. User Story: Create a smart manager + +**As a:** Registered User. +**I want to:** Create a smart manager from a given folder root. +**So that I:** Have all contained files automatically organized for me. + +#### Acceptance criteria: + +**Given** I have installed the application. +**When** I click on create new smart manager and select a root directory. +**Then** A smart manager is created and the system updates its scan targets to monitor this directory. +**Covers:** UC8 + +### 5. User Story: Configure sorting preferences + +**As a:** User. +**I want to:** Configure how and when smart folders are sorted. +**So that I:** Have predictability and control over how and when my folders are sorted. + +#### Acceptance criteria: + +**Given** I have installed the application. +**When** I open the settings and configure rules for sorting (metadata, keywords, tags) and sorting timers. +**Then** My configuration is saved and applied for future sorting. +**Covers:** UC3 + +### 6. User Story: Tag files manually + +**As a:** User +**I want to:** Associate tags that I defined with certain files. +**So that I:** May influence how the files are sorted and be used for sorting by tags. + +#### Acceptance criteria: + +**Given** The application is installed and a smart manager created. +**When** I select files and assign custom tags to them. +**Then** Tags are stored and associated with the files for use in sorting. +**Covers:** UC4 + +### 7. User Story: Graph File organization visualization + +**As a:** User. +**I want to:** See how my files are organized via an intuitive mind-map / graph view. +**So that I:** May more naturally traverse through my files and interact with them. + +#### Acceptance criteria: + +**Given** I have created a smart manager which contains content. +**When** I click on graph view. +**Then** I see an interactive graph-based UI of my files and folders. +**Covers:** UC7 + +### 8. User Story: Traditional File organization visualization + +**As a:** User. +**I want to:** See how my files are organized via traditional folder viewer. +**So that I:** May traverse and interact with files in a well-known manner. + +#### Acceptance criteria: + +**Given** I have created a smart manager which contains content. +**When** I click on folder view. +**Then** I can traverse through the folder structure by clicking on folders. +**Covers:** UC7 + +### 9. User Story: Trigger Manual Sort + +**As a:** User. +**I want to:** Manually initiate a file sorting process. +**So that I:** May initiate a reorganize immediately. + +#### Acceptance criteria: + +**Given** The application is installed and settings configured. +**When** I click on Sort Now on a smart manager. +**Then** The sorting process begins and files are moved/sorted appropriately. +**Covers:** UC9 + +### 10. User Story: Lock files and folders + +**As a:** User. +**I want to:** Lock specific files and folders. +**So that I:** Prevent them from being moved or sorted during organization. + +#### Acceptance criteria: + +**Given** I have a smart manager with content. +**When** I select a file or folder and click the lock button. +**Then** The item is excluded from future organization operations and locked items are skipped during sorting. +**Covers:** UC10 + +### 11. User Story: View File metadata + +**As a:** User. +**I want to:** See metadata associated with a file. +**So that I:** Can understand the content associated with my files. + +#### Acceptance criteria: + +**Given** I have a smart manager with content and files are indexed by the system. +**When** I select a file by clicking on it. +**Then** A side-menu pops up showing me the detailed metadata associated with that file. +**Covers:** UC6 + +### 12. User Story: Search files, tags or metadata + +**As a:** User. +**I want to:** Search for files using names, tags or keywords. +**So that I:** Can quickly find relevant content. + +#### Acceptance criteria: + +**Given** My files are sorted in a smart manager. +**When** I enter a search query in the search bar. +**Then** Search results matching the criteria are displayed. +**Covers:** UC11 + +### 13. User Story: Apply or Discard sorting changes + +**As a:** User. +**I want to:** Review sorting changes and decide whether to apply them or discard them. +**So that I:** Have control over whether changes are made to my file system. + +#### Acceptance criteria: + +**Given** I have performed a smart organization and sorting has occurred. +**When** I review the proposed changes and click apply or discard. +**Then** Either changes are applied to the file system or files return to their prior state. +**Covers:** UC12 + +### 14. User Story: Add a new file or folder + +**As a:** User. +**I want to:** Create a new file or folder at some location in my smart manager. +**So that I:** May interact with my filesystem via SFM to perform tasks I would with a regular filesystem. + +#### Acceptance criteria: + +**Given** I have a smart manager. +**When** I click to create a new file or folder. +**Then** The new item is added to the smart manager but not yet sorted. +**Covers:** UC8 + +### 15. User Story: Stay logged in + +**As a:** User. +**I want to:** Remain logged in after restarting the app. +**So that I:** Don't have to log in every time I open the app. + +#### Acceptance criteria: + +**Given** I have logged in and did not log out. +**When** I reopen the app. +**Then** I am automatically still logged in. +**Covers:** UC2 + +### 16. User Story: Filter files by type + +**As a:** User. +**I want to:** View files filtered by their type (e.g., images, documents, music). +**So that I:** Can quickly access specific types of content. + +#### Acceptance criteria: + +**Given** Files exist and are indexed by the system. +**When** I select a file type filter or request files of a specific type. +**Then** Files of the requested type are returned and displayed. +**Covers:** UC13 + +### 17. User Story: Bulk tag operations + +**As a:** User. +**I want to:** Add or remove tags from multiple files at once. +**So that I:** Can efficiently manage tags across many files. + +#### Acceptance criteria: + +**Given** I have selected multiple files. +**When** I choose to add or remove tags in bulk. +**Then** The specified tags are added to or removed from all selected files. +**Covers:** UC14, UC15 + +### 18. User Story: Find duplicate files + +**As a:** User. +**I want to:** Identify duplicate files across my monitored directories. +**So that I:** Can manage storage space and remove redundant files. + +#### Acceptance criteria: + +**Given** Files exist in monitored directories. +**When** I request duplicate file detection. +**Then** Duplicate file groups are identified and displayed. +**Covers:** UC16 + +### 19. User Story: Bulk delete files + +**As a:** User. +**I want to:** Delete multiple files in a single operation. +**So that I:** Can efficiently clean up unwanted files. + +#### Acceptance criteria: + +**Given** Files exist and are indexed by the system. +**When** I select multiple files and choose delete. +**Then** All selected files are deleted from the system. +**Covers:** UC17 + +### 20. User Story: View manager statistics + +**As a:** User. +**I want to:** See statistics about my file managers (file counts, sizes, tags, etc.). +**So that I:** Can understand and monitor my file organization. + +#### Acceptance criteria: + +**Given** I have smart managers configured. +**When** I request manager statistics. +**Then** Statistics including file counts, sizes, and tags are displayed across all managers. +**Covers:** UC18 + +## Use cases / Use Case Diagram + +| Use Case | Actor | Abstract | Preconditions | Postconditions | Diagram | +| ------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------- | ------------------------ | +| UC1: Install Application | User | The user installs the SFM application on their device. | System requirements met. | Application is ready for configuration. | ![UC1](assets/UC1.png) | +| UC2: Login | User | The user logs in to save preferences across devices (optional). | User is registered. | Preferences are saved. | ![UC2](assets/UC2.png) | +| UC3: Configure Sorting Preferences | User | The user sets preferences for how sorting should occur (rules: locks, tags). | Application is installed. | Configuration is saved. | ![UC3](assets/UC3.png) | +| UC4: Tag Files Manually | User | The user assigns custom tags to files. | Application is installed. | Tags are stored and used for sorting. | ![UC4](assets/UC4.png) | +| UC5: Sort Files | User | The user manually initiates sorting of files into directories based on metadata, keywords, and tags. | Files exist in monitored directories. | Files are moved/sorted appropriately. | ![UC5](assets/UC5.png) | +| UC6: View File Metadata | User | The user views detailed metadata of any file managed by SFM. | File exists and is indexed by the system. | Metadata displayed. | ![UC6](assets/UC6.png) | +| UC7: Visualize File Structure | User | The user views the current file organization as a graph. | File exists and is indexed by the system. | A graph-based UI is presented. | ![UC7](assets/UC7.png) | +| UC8: Add/Remove Monitored Directories | User | The user selects which directories the SFM should monitor. | | The system updates its scan targets. | ![UC8](assets/UC8.png) | +| UC9: Trigger Manual Sort | User | The user initiates file sorting manually. | Application is installed. | Sorting process begins. | ![UC9](assets/UC9.png) | +| UC10: Lock Files/Folders | User | The user locks specific files or folders to prevent them from being sorted by SFM. | Application is installed. | Locked items are skipped during sorting. | ![UC10](assets/UC10.png) | +| UC11: Search Files/Tags/Keywords | User | The user searches for files based on name, tags, or keywords. | Application is installed. | Search results are displayed. | ![UC11](assets/UC11.png) | +| UC12: Apply/Discard Sorting Changes | User | After sorting, the user decides whether to apply the changes to the file system or discard them. | Sorting has occurred at least once. | Either changes are applied or files return to prior state. | ![UC12](assets/UC12.png) | +| UC13: Return Files by Type | User | The user requests a list of files filtered by type (e.g., images, documents, music). | Files exist and are indexed by the system. | Files of the requested type are returned. | ![UC13](assets/UC13.png) | +| UC14: Add Tags in Bulk | User | The user assigns one or more tags to multiple files at once. | Application is installed. | Tags are stored and associated with selected files. | ![UC14](assets/UC14.png) | +| UC15: Remove Tags in Bulk | User | The user removes one or more tags from multiple files at once. | Files have existing tags. | Tags are removed from selected files. | ![UC15](assets/UC15.png) | +| UC16: Return Duplicate Files | User | The system identifies and returns all duplicate files across monitored directories. | Files exist in monitored directories. | Duplicate file groups are displayed. | ![UC16](assets/UC16.png) | +| UC17: Bulk Delete | User | The user deletes multiple files in a single operation. | Files exist and are indexed by the system. | Selected files are deleted. | ![UC17](assets/UC17.png) | +| UC18: Return Manager Statistics | User | The user requests statistics (e.g., file counts, sizes, tags) across all managers. | Application is installed. | Statistics are displayed. | ![UC18](assets/UC18.png) | +| UC19: Register | User | The user registers in order to login and save preferences. | Application is installed. | User account is created. | ![UC19](assets/UC19.png) | + +## Functional Requirements + +1. Provide User Authentication. + 1.1 Users must be able to register on the web portal if they wish. + 1.2 Users must be able to download the application from the web portal. + 1.3 Registered users must be able to log in to the downloaded application. + 1.4 Users must be able to remain logged in to the application. + 1.5 Logged in user must be able to log out of the application. + 1.6 Registered user's preferences must be saved for to carry over to other platforms when logged in. + +2. File Metadata. + 2.1 Users must be able to inspect file specific metadata by clicking on a file. + 2.2 Users must be able to view a base set of metadata for all files and an extended set for specific file types. + +3. Directory Organization. + 3.1 Users must be able to create autonomously managed directories known as "smart folders". + 3.2 Users must be able to choose a list of folders and files to be managed by smart folders. + 3.3 Users must be able to let smart folders reorganize the contained files by semantic meaning (what logically relates the files). +   3.3.1 Smart folders must provide users with a preview of how the sorted file will look. +   3.3.2 Users must be able to accept or decline previewed reorganized file structure. +   3.3.3 Users should choose between copying accepted reorganized files or moving original files. +   3.3.4 Users should be able to lock files/folders they do not want to move or that should maintain relative position. + 3.4 Users must be able to later add new files or folders to smart folder which should be auto-organized. + 3.5 Users must be able to choose to add newly downloaded files directly to a smart folder for organization. + +4. File Searching. + 4.1 Users must be able to search for files by name. + 4.2 Users must be able to search for files by its semantic values (related files). + 4.3 Users must be able to search for files by keywords. + 4.4 Users must be able to search for files by supported metadata. + +5. Natural File Navigation. + 5.1 Smart folders should be viewable as a graph / mind-map like strucutre. + 5.2 Users must be able to navigate a smart manager via this mind-map structure. + 5.3 Users must be able to see semantic connections between files in graph. + +6. Smart Manager Options and Customization. + 6.1 Users must be able to set maximum number of subdirectories. + 6.2 Users must be able to set maximum number of files in subdirectories. + 6.3 Advanced users must have the option to tune the influence of different parameters on how files are organized. + 6.4 Users must be able to configure how frequently smart folders are reorganized via a settings interface. + +7. File and folder tagging. + 7.1 Users must be able to create new tags for files and folders. + 7.2 Users must be able to apply tags to files and folders. + 7.3 Smart folders must also use tags to make organization decisions. + +8. Rollback. + 8.1 Users must be able to rollback to how folder structure looked before applying smart organization. + +## Service Contract +As explained in our architecture below, our project has two "servers" for which we provide service contracts. + +* Filesystem Server (Go codebase) | REST API | [Documentation](api.md) +* Clustering Server (Python codebase) | gRPC API | [Documentation](grpc.md) + +## Domain Model + +![Domain-Model](assets/DomainModel.jpg) + +## Architectural Requirements (Please see Architecture.md linked below) + +**IMPORTANT:** This sections only provides a brief overview of our architecture. Kindly consult [here](Architecture.md) for the detailed documentation on architecture prepared for marking. + + +### Quality Requirements + +1. Performance. + 1.1 Time for reorganizing a new smart manager should scale linearly with amount of folders and files. + 1.2 Adding singular new file for organizing to a smart manager should not take longer than 30 seconds. + +2. Reliability. + 2.1 System AI should organize folders and files with high accuracy (> 90% as judged by user testing). + 2.2 Smart folders should remain accurate over prolonged usage and not become discoherent. + 2.3 Logical connection between files or folders inside a directory should be clear at first glance. + +3. Usability. + 3.1 Usage should be less cumbersome than traditional file mangers such as file explorer. + 3.2 Inexperienced users should not be overwhelmed by large number of options. + 3.3 Advanced users should be able to tailor smart management to their very particular needs. + +4. Portability. + 4.1 The system should run on any modern windows, linux or macOS machine via. + 4.2 The system must be packaged as a singular downloadble application that may be installed on any of the above mentioned platforms. + +5. Security. + 5.1 The system should never permenantly store any information obtained from user files and all processing should happen locally. + 5.2 The system should ignore any files that requires administrative privilleges to modify. + 5.3 The system should only track and organize files that the user gave explicit permissions to organize. + +### Architectural Patterns +#### Application Architecture + +![Application architecture](assets/applicationArchitectureV3.png) +Hybrid between Monolithic, Master-Slave and Client-Server. + +### Design Patterns + +The current design of our project makes use of 2 design patterns to achieve various functionality. Our decisions as to why these were used and how they are implemented follows: + +**Composite** +Used to represent the tree like structure associated with a directory structure. The classes that map to the participants are as follows: + +- Component : ManagedItem +- Leaf: File +- Folder : Composite + +The use of the composite pattern allows us to easily create, manage and traverse folder structures. A single instance of _ManagedItem_ may then be used by the _SmartFolder_ as an access point to an entire folder structure. By decoupling the folder structure from the _SmartFolder_ we may treat files and compositions of files (folders) uniformly. + +**Memento** +Used to store the state of a _SmartFolder_ so it may be reinstated via a rollback at a later time. The classes that map to the participants are as follows: + +- Originator : ManagedItem +- Memento : Memento +- Caretaker : SmartFolderBackup + +Using the composite pattern an entire directory structure may be stored by creating a memento from the root of such a _ManagedItem_. This memento may then be stored in the _SmartFolderBackup_ for later rollback as initiated by the _SmartFolder_ upon request of the user. + +### Constraints + +The following constraints has been highlighted by our team: + +- SFM will not manage any system-level files that would require administrative privileges to move, change, or otherwise manage. +- SFM will not provide access to any shared user directories as it only manages local machine files. +- SFM will not integrate directly with existing cloud storage services such as Microsoft One-Drive or Dropbox. + +## Technology Requirements + +This sections acts as a brief overview of the various technologies used in our stack and why they were chosen. + +![Flutter](https://img.shields.io/badge/Flutter-%2302569B.svg?style=for-the-badge&logo=Flutter&logoColor=white) +![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54) +![Go](https://img.shields.io/badge/go-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TechnologyOptionsBenefitsDrawbacksFinal Choice Reasoning
Frontend FrameworkFlutter + Cross-platform (Both Desktop and Mobile)
+ Native performance
+ Material UI widgets +
+ Larger binary size
+
Chosen For its ability to create cross platform desktop apps.
Electron + Easy integration with web
+
+ Heavy memory usage
+ Does not directly support mobile (Needs Capacitor or Cordova) +
React Native + Cross-platform mobile/web
+
+ Weak desktop support
+
AI CapabilitiesPython + Well supported for AI purposes (We use SciKit)
+ Fast development +
+ Slower runtime performance
+ Weak multithreading support +
Chosen For its out of the box AI functionality and portability
Java + High performance
+ Strong threading and ecosystem +
+ Slower dev speed for AI +
C++ + High performance
+ Fine-grained control +
+ Unreliable due to no memory safety
+
Filesystem management & BridgeGo + Easy concurrency with goroutines
+ Fast compilation
+ Simple learning curve +
+ Less dynamic than scripting languages
+
Chosen For powerful concurrency features to balance out python bottleneck & Easy web development for endpoints required by frontend
Rust + High performance
+ Memory Safety +
+ Extreme learning curve
+
Java + Portable due to JVM + + Verbose and heavyweight for lightweight APIs +
APIgRPC + Very fast due to binary serialization
+ Code generation support
+ Strong typing +
+ Requires more setup than REST + Chosen For efficiency between Go and Python. See grpc.md
REST + Simple and well-understood
+ Easy testing and debugging +
+ No built-in type safety
+ Not ideal for dynamic recursive structures like Directories +
Chosen For easy to use and test endpoints between Go and Frontend
GraphQL + Flexible queries
+ Efficient data fetching +
+ Overkill for simple RPCs
+ More complex tooling +
+ +## Deployment Model +Our application will be deployed as an executable desktop app for various operating systems. While we currently have support for Microsoft Windows and GNU Linux we hope to support deployment for MacOS in the future. + +### Justification for Deployment model +Our project's nature requires it to run on the user's machine. Its close dependancy on the filesystem makes it unsuitable for a web-based application which would need to be deployed on a server. Furthermore, by doing all processing on the local machine we avoid the obvious security and privacy risk involved in sending user files (which could contain sensitive information) over a network for processing or (even worse) storing these files on a server. +At this point it would be important to make the distinction that our project **never** stores any file information permenantly in any other location than the user's filesystem. + +To ease the complicated build process on behalf of users we aims to provide exceutables for the various commonly used operating systems by means of an installer. + +### How to access our deployments +Our installers can be found on and downloaded via the google drive link provided on our readme. + + +### Deployment Diagram +![Deployment-Diagram](assets/sfm_deployment.jpg) \ No newline at end of file diff --git a/docs/documentation/demo_4/technical_installation.md b/docs/documentation/demo_4/technical_installation.md new file mode 100644 index 0000000..aeedaeb --- /dev/null +++ b/docs/documentation/demo_4/technical_installation.md @@ -0,0 +1,152 @@ +

+ Company Logo +

+ +# Technical Installation Document for SparkIndustries + +**Version:** 1.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Southern Cross Solutions and Users of SFM + +## Content +* [Introduction](#introduction) +* [Installer](#installer) +* [Building from Scratch](#building-the-project-from-scratch) +* [Creating your own installer](#creating-your-own-installer) + +## Introduction +The following document serves as the technical installation document for SparkIndustries. It provides detailed instructions for how to download and install the program using the following approaches: +1. Using the provided downloadable installer (The recommend approach) +2. Building the project from scratch via cloning the repository. +3. Creating your own installer (Not recommended but provided for completeness) + +**A Note of supported platforms:** +Our application is designed with the intention to be useable on Microsoft Windows, Linux and MacOS. For the purpose of this demo we only provide an installer for Windows, however in the future we will be releasing an installer for Linux distibutions as well (Tested on Linux Mint). The requirement of an apple developer's lisence prevents us from deploying to MacOS. However Mac Users may still build the project from scratch using the instructions provided in this document. + +## Installer + +This section details where to download the installer and how to use it to install Smart File Manager to your device. Please note that the Windows operating system might (and likely will) flag the application as high risk and administrator privileges may be required to allow the install. While SparkIndustries is not aware of any risk that the application might pose, we do not take any liability for issues or harm caused to a system by installing the program. User discresion is advised. + +### Step 1: Downloading the installer +The installer may be downloaded from our google drive [here](https://drive.google.com/drive/folders/1KlQ3yYhmHYFbv0vVpLBJv5CjVyD5idO4) (approximately 500MB) + +### Step 2: Running the installer +Execute the installer by double clicking on it. At this point you might be flagged by the security system as follows. Click the __run__ button as indicated to proceed. + +![security_warning](assets/installationAssets/security_warning.png) + + +Next you will be prompted to select the install destination. Choose this path as desired or use the default install location __Users/AppData/Local/Programs/SmartFileManager__ and click on __next__ to proceed. + +![install_location](assets/installationAssets/install_location.png) + +You will now be prompted with a screen prompting you with additional tasks. On this page you may choose to create a desktop shortcut using the provided checkbox (higlighted in blue). After selecting your choice proceed by clicking on __next__. + +![additional_tasks](assets/installationAssets/additional_tasks.png) + +Finally, confirm the installation by clicking on the __install__ button as highlighted. + +![confirm_installation](assets/installationAssets/confirm.png) + +The app will now install. After installation you will be shown the following screen from where you may choose to launch the app. Click on __finish__ to complete the process. +![finish](assets/installationAssets/finish.png) + +You have now successfully installed Smart File Manager! + + +## Building the project from scratch + +This section details how the project can be built and run from scratch by cloning the repo and running all the various services required for the application to function. While we primarily suggest use of an installer this might be useful should you wish to run the application on a non supported operating system. + +### Required Installs + +The following must be installed on the local system before using this installation. + +**Programming Languages:** +* Python 3 ([installation instructions here](https://www.python.org/downloads/)) +* Golang ([installation instructions here](https://go.dev/doc/install)) +* Flutter & Dart ([installation instructions here](https://docs.flutter.dev/install)) + +**Python Packages:** +* pytest +* python-magic +* mutagen +* pypdf +* python-docx +* Pillow +* pymediainfo +* yake +* scikit-learn +* sentence-transformers +* grpcio +* grpcio-tools + +Note: Using pip you may install all these as follows: + +```bash +pip install \ + pytest \ + python-magic \ + mutagen \ + pypdf \ + python-docx \ + Pillow \ + pymediainfo \ + yake \ + scikit-learn \ + sentence-transformers \ + grpcio \ + grpcio-tools +``` + +**gRPC:** + +gRPC must be installed for both go and python. Note the python install is included in the section above. Documentation for installing gRPC may be found [here](https://grpc.io/blog/installation/) + +### Building the project + +**Cloning the Repository** +Clone the repository onto your local device using the following command +```bash +git clone https://github.com/COS301-SE-2025/Smart-File-Manager +``` + +**Generating the gRPC protos:** +Having cloned the repository we must now generate the gRPC proto files required for running the project. Ensure you are in the root directory then run the following command +```bash +make proto_gen +make go_proto_gen +``` + +**Compling and running the go server:** +Next the go server must be compiled and started. From the root directory run the following command +```bash +make go_api +``` + +**Compiling and running the python server:** +Run the python server by running the following command from the root directory +```bash +make python_server +``` +Ensure that you wait for the terminal to output __python server started__ before proceeding to the next step. + +**Running the flutter app:** +Now that both the python and go server are running we can compile and run the actual flutter application using the following command from root: +``` +flutter build +cd build +flutter run +``` + +The application should now be running and working as intended. + +## Creating your own installer + +This section details how to create your own installer, or rather the process we use to create our installer. + +**Note:** We do NOT recommend this approach to installing our program whatsoever! It has merely been added for completeness sake. + +The install instructions is what we used for creating our Windows installer and may or may not work for other operating systems. Should some user wish to create a different installer for this application we hope that this information may provide some reference point for how this process should be approached. + +The document detailing this process may be found [here](installer_guide.md) \ No newline at end of file diff --git a/docs/documentation/demo_4/testing_policy.md b/docs/documentation/demo_4/testing_policy.md new file mode 100644 index 0000000..4e3b22d --- /dev/null +++ b/docs/documentation/demo_4/testing_policy.md @@ -0,0 +1,97 @@ +# Testing Policy Document + +

+ Company Logo +

+ +**Version:** 1.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** Southern Cross Solutions +**Document Type:** Testing Policy Document +**Demo:** Demo 4 + +## Content +* [Introduction](#introduction) +* [Policy Overview](#policy-overview) +* [Finding and Running the Tests](#finding-and-running-tests) +* [Test Execution](#testing-coverage) +* [Testing Coverage](#testing-coverage) + + +## Introduction +The following docment serves to outline our team's testing policy used to ensure the highest possible quality and reliability for our project. + +## Policy Overview +We believe that extensive testing is foundational for producing a high quality application with as little bugs as possible. To achieve this we make use of various levels of testing ensuring optimal functionality at every part of our project. These levels of testing are as follows: + +* Unit Testing: Testing the smallest isolatable parts of software. This also involves mocking any dependancies that such a unit might have. +* Integration Testing: Testing if various components interact correctly and expectedly. +* End To End (E2E) Testing: Tests if the entire workflows for a specific feature works from start to finish. +* Non Functional Testing: Ensure that the quality requirements are satisfied by our implementation - see [here](non_functional_tests.md) + +## Finding and Running Tests +The location of the testing files may be found in the following locations from the project root + +### Python Tests +#### Normal Tests + +**location:** _python/testing_ + +They may be ran using + +``` +make python_test +``` + +#### Non Functional Tests + +**location:** _python/non_functional_tests_ + +They may be ran using +``` +make python_non_functional +``` + +### Go Tests +**location:** _golang/filesystem_ +Note that the tests are all .go files ending in _test + +They may be ran using +``` +make go_test +``` + +### Flutter Test +**location:** _app/test_ + +They may be ran using +``` +cd app +flutter test --reporter=expanded +``` + +## Testing Execution +Proof that all tests executed are provided in the following screenshots for convenience, should you not wish to run them. + +### Python +![Python Tests Passing](assets/testingAssets/python_test.png) + +### Go +![Go Tests Passing](assets/testingAssets/go_tests.png) + + + +## Testing Coverage +The testing coverage for the various codebases are as follows + +### Python + +![Python Coverage](assets/testingAssets/python_coverage.png) + +Total Coverage: 86% + + +### Go +![Go Coverage](assets/testingAssets/go_coverage.png) + +Total Coverage: 70.5% \ No newline at end of file diff --git a/docs/documentation/demo_4/user_manual.md b/docs/documentation/demo_4/user_manual.md new file mode 100644 index 0000000..7228adb --- /dev/null +++ b/docs/documentation/demo_4/user_manual.md @@ -0,0 +1,207 @@ +

+ Company Logo +

+ +# User Manual + +**Version:** 2.0.0.0 +**Prepared By:** Spark Industries +**Prepared For:** All our Users + +## Content +* [Introduction](#introduction) +* [Startup Page](#start-up-page) +* [Creating A Smart Manager](#creating-a-smart-manager) +* [Graph View](#graph-view) +* [Search](#search) +* [Advanced Search](#advanced-search) +* [Sorting a Manager](#sort-manager) +* [Tagging and Locking](#tagging-locking-and-viewing-details) +* [Bulk Operations](#bulk-operations) +* [Statistics Dashboard](#statistics-dashboards) +* [Glossary](#glossary) + +## Introduction +This document aims to server as a detailed manual of how to use smart file manager. It goes into detail on how to use the various features by simulating the flow of how the average user would likely interact with the system. It is broken up into sections corresponding to different features. If any term is unclear please consult the glossary linked [here](#glossary). We recommend reading this document sequentially for the best overview of how to use the system effectively. + +**Note:** Please ensure that SFM has been installed on your system. Detailed instructions for doing so may be found [here](technical_installation.md) + +## Start Up Page +When starting SFM for the first time you will be shown the following page + +

+ Dashboard +

+ +Currently there are no managers created so the dashboard page shows no statistics. From the sidebar we can see the various pages available to the user. These include: + +1. Dashboard (currently selected) +2. Smart Managers +3. Advanced Search +4. Settings + +We'll now show each page as it appears without any smart managers created (note: dashboard already shown) + +### Smart Manager +

+ SmartManager +

+ + +### Advanced Search +

+ AdvancedSearch +

+ + +### Settings +

+ Settings +

+ +## Creating a Smart Manager + +To create a new smart manager to manage a subset of files follow these steps. + +Click on the **+Create Smart Manager** button in the bottom-left corner of any page. The following pop-up will appear. +

+ Create_Manager_popup +

+ +Enter the name of the manager and select the root of the new manager by using the **browse** button. Click **create** to confirm and make a new manager +

+ Create_Manager_popup_2 +

+ +After creating a manager the following screen will appear. +

+ Create_Manager_done +

+ +All of the your created managers will appear on the sidebar. Clicking on the manager in the sidebar will provide you with the page shown above. From here all files inside a manager may be viewed. The other features on this page will be explained in the sections to follow. + +## Graph View +Once a manager has been created the files may be viewed in the traditional file structure. However, SFM offers a state of the art graph based view of your files which allows you to better understand the organization of your files. +
+Having selected a smart manager on the sidebar swap over from Folder view to Graph view using the button in the top-right corner of the screen. You will be presented with a view appearing as follows: +

+ Create_Manager_done +

+ +## Search +For any created manager SFM allows you to search for any file at breakneck speed. To access this feature select the smart manager you wish to search in using the sidebar. Underneath the manager name there is a textbox captioned **search for files inside manager**. Type your desired filename in here to search for it. SFM make use of a [fuzzy search](#glossary), hence even if an exact result cannot be found a close match will be presented. The search results are returned as shown below. + +

+ Create_Manager_done +

+ +As can be seen from the example searching for "books" returned the exact match, followed by close results (words starting in "b"). + +## Advanced Search +SFM also provides an advanced search which considers keywords extracted from text-based files. To access this feature select Advanced Search in the sidebar. In the top right corner wait for the page to display **Advanced Search Active**. Using the left dropdown select the name of the manager that you want to perform the advance search on. Using the right input, enter the search text which will be compared to the file's keywords. Note that advanced search still makes use of a [fuzzy search](#glossary). After performing an advanced search the output will display similar to below. + +

+ Create_Manager_done +

+ +Note the difference in the files returned for the same search text. Here the a file on world-war-1 was returned since the search text was compared to keywords and the document contains terms such as "textbook", "workbook" etc... + +## Sort Manager +One of SFM's most powerful features is the ability to automatically perform a [semantic sort](#glossary). The sorting is influenced by tags added by the user as well as whether a file is locked or not (explained below). SFM will make a best effort to sort files semantically. Note that this operations does work on any file type but is considerably more accurate for text based files, due to the keywords they contain. Suggested use cases include sorting large collections of unrelated files into neatly organised smaller groups e.g. sorting research papers by topic.
+ +To access this feature navigate to the **Smart Managers** tab on the sidebar. On this page all your smart managers will appear with options for the manager and deleting the manager. The page will look comparable to below + +

+ sort_1 +

+ +After clicking the sort button (allow a reasonable time for it to complete) the top button will change allowing the user to **view sorted** as shown below. + +

+ sort2 +

+ +When clicking on the **view sorted** button a preview of the sorted files will appear, allowing you to traverse through the sorted files using the folder view or graph view. + +

+ sort3 +

+ +From here you may choose to either **Aprove and Apply** the changes or **Decline**. Choosing to approve and apply will move the actual files on the filesystem to be consitent with the preview shown. **Important Note:** This action cannot be undone. Choosing to decline the sort will not apply the sorting. After choosing to apply the changes the updated structure may be viewed by going to the relevant manager on the sidebar as can be seen. + +

+ sort3 +

+ +## Tagging, Locking and viewing Details +SFM allows you to perfrom various other operations of files and folder. To access these additional operations right click on any file or folder using either the folder viewer or graph view. A pop-up as shown below provides you with these options. + +

+ sort3 +

+ +A short explanation of these features are: + +* Details: Provides you with a list of metadata extracted for the files. +* Add Tag: Allows you to add a user defined [tag](#glossary) to a file. +* Lock: Allows you to [lock](#glossary) the file or folder. Note that locking a folder recursively locks all files inside it. +* Unlock: Allows you to unlock a file. + + +## Bulk Operations +Some of the additional features might be tedious to apply to single files at a time. We provide functionality to provide these operations in bulk. To access this feature navigate to the relevant smart manager in the sidebar. Click on the button called **bulk operations** to access these features. + +

+ sort3 +

+ +The operations which can be performed in bulk are: +* Bulk Delete +* Bulk Add Tag +* Bulk Remove Tag + +Furthermore these operations may be applied to different file types as can be seen below + +

+ sort3 +

+ +You may choose to apply these operations to all files that match the selected filter of you can select them individually using the checkboxes. + +## Duplicates +SFM allows you to easily find and remove duplicate files from your system. SFM does not simply check filenames for duplicates but hashes the files to ensure that the content are duplicates. To access this feature use the sidebar to navigate to the relevant smart manager. From there use the **Find Duplicates** to be shown the following screen. + +

+ sort3 +

+ +From here you can see all duplicate files with the paths from where they are located. You can choose to delete specific duplicates or all duplicates. + +## Statistics Dashboards +Once atleast a single manager has been created the dashboard page will show you some interesting statistics regarding your managers as can be seen below. + +

+ sort3 +

+ +## Glossary +In this section we describe some terms that we use in the user manual. + +### Smart Manager +A smart manager is the principle object of smart file manager. It can be thinked of as a repo which manages all files it has been set to track. When creating a smart manager you may select the root. All content contained in this root will be tracked by the smart manager and any operations that you wish to perform will act on the files inside this manager. + +### Graph View +A graph or mindmap based view of a smart directory showing files and folders as nodes with the connections between them as edges. + +### Fuzzy Search +A type of search where exact results and close results are returned when searching for something. For example: Searching for the term "Banking Details" may also retunr "Banking", "Bank Details" or even "Membership Details" etc... Note that "closest" results will always be returned first. + +### Semantic Sort +A sorting procedure which sorts files by how files logically relates to each other. This works by looking at file metadata, keywords included in the files and user defined tags and performing K-means clustering on a vector representation of this information. Tl;dr files relating to similar topics will be grouped together. + +## Tags +A short string attached to a file or set of files which allows the user to indicate that the files contain some relation that should be kept in mind during clustering. Also allows you to retrieve a subset of files from a manager which all contain a certain tag. + +## Lock / Unlock +A locked file is prevented from being moved during the sorting process. While a locked folder can be moved the relative structure of all content inside the directory may not be moved. Unlocking a folder will also remove locks from all content inside the folder which has been unlocked. Note that our program automatically checks for hidden folders like _.git_ to automatically lock coding projects to prevent them from being broken by sorting. + diff --git a/docs/documentation/demo_4/wireframe.md b/docs/documentation/demo_4/wireframe.md new file mode 100644 index 0000000..fdf8a45 --- /dev/null +++ b/docs/documentation/demo_4/wireframe.md @@ -0,0 +1,143 @@ +# Smart File Manager - Wireframe Documentation + +## Overview + +This document provides a wireframe description of the Smart File Manager (SFM) application's user interface, including all main screens and their components. + +## Global Navigation + +![Sidebar Navigation](assets/wireframeAssets/wireframe_main_navigation.png) + +- **Main Navigation Menu:** + - Dashboard - Home screen with metrics and quick access + - Smart Managers - Configuration page Smart Managers + - Advanced Search - Enhanced file search for entire system + - Settings - Application settings and options +- **Smart Managers List:** + - Lists all current Smart Managers + - Option to add new Smart Manager + +## 1. Dashboard + +![Dashboard View](assets/wireframeAssets/wireframe_dashboard.png) + +### Purpose + +Main screen displaying system statistics and quick access to frequently used files. + +### Elements + +- **Statistics Cards:** + + - Total Files + - Storage Used + - Organization Level + - Total Smart Managers + +- **Quick Access Section:** + - User can add files/folders that are often used. + +## 2. Smart Managers + +![Smart Managers Configuration](assets/wireframeAssets/wireframe_smart_managers.png) + +### Purpose + +Interface for configuring and managing Smart Managers. + +### Elements + +- **Main Features:** + + - Create Smart Manager + - Sort a Smart Manager + - Delete a Smart Manager + - Rename a Smart Manager + +- **Configure Smart Manager** + - Max files per directory + - Max directory depth + +## 3. Advanced Search + +![Advanced Search](assets/wireframeAssets/wireframe_advanced_search.png) + +### Purpose + +Comprehensive search interface for finding files using multiple criteria across all Smart Managers. + +### Elements + +- **Filter Options:** + + - File Type + - Location + - Date Modified + - Size + - Tags + - Author/Owner + +## 4. Settings + +![Settings](assets/wireframeAssets/wireframe_settings.png) + +### Purpose + +Configuration interface for application preferences and user settings. + +### Elements + +- **Settings Categories** + - General + - Appearance + - Notifications + - Account + +## 5. Documents Manager - Folder View + +![Documents Manager - Folder View](assets/wireframeAssets/wireframe_manager_page.png) + +### Purpose + +Traditional folder-based interface for viewing and managing files within a Smart Sanager. + +### Elements + +- **Attributes Shown** + + - Title + - Organization status + - Meta Data + - Tags + +- **Search and Filter:** + + - Search(within manager) + - Sort By + - Filter + +- **File Actions** + - Open File + - Add Tags + - Lock File + - Delete File + +## 6. Documents Manager - Graph View + +![Documents Manager - Graph View](assets/wireframeAssets/wireframe_manager_page2.png) + +### Purpose + +Visualization interface showing file relationships in a Graph format. + +### Elements + +- **Functionality:** + + - Same as Folder View but with Graph View selected + +- **Visualization:** + - Node graph showing file relationships + - Central node representing primary folder + - Connected nodes representing related files/folders + - File details panel (same as in Folder View) diff --git a/docs/index.html b/docs/index.html index f59e425..1ba3207 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,378 +5,564 @@ Smart File Manager - Your AI-Powered Digital Housekeeper - + + + + - -
- -
-
-

Meet Your New Digital Housekeeper

-

- Say goodbye to cluttered desktops and folder mazes. Smart File - Manager uses Clustering Algorithms to automatically organize your - files by semantic meaning, making file management effortless and - intuitive. -

-
- Download Now + +
+
+

+ Stop Wrestling with + Messy Files +

+

+ Your new digital housekeeper organizes thousands of files using + advanced clustering algorithms. No more folder mazes, no more desktop + chaos. +

+
+ +
+
+
+

BEFORE

+
+
📄 document_final_v2.docx
+
📊 budget.xlsx
+
🖼️ IMG_4521.jpg
+
📄 presentation.pptx
+
📄 notes_meeting.txt
+
🖼️ screenshot.png
+
📊 data_export.csv
+
📄 contract_draft.pdf
+
+
+ +
+ + Clustering Magic
-
-
- -
-
-

Powerful Features

-
-
-
- Code Icon +
+

AFTER

+
+
+
+ 📊 Financial Documents +
+
+
budget.xlsx
+
data_export.csv
+
-

Clustering Organization

-

- Advanced clustering algorithms automatically sort your files by - semantic meaning, not just file types. -

-
-
-
- Graph Icon +
+
+ 📄 Work Documents +
+
+
document_final_v2_FINAL.docx
+
presentation.pptx
+
contract_draft.pdf
+
-

Interactive Graph View

-

- Visualize your file structure with an interactive graph view, - easier than ever before. -

-
-
- Search Icon +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ Smart File Manager
-

Smart Search

-

- Find files using natural language, tags, metadata, or contextual - relationships. -

-
-
-
- Sort Icon +
+
+ +
+
+ + Deep Learning Research.pdf + 98% match +
+
+ + ML_Notes_lecture.docx + 94% match +
+
+ + AI_Presentation.pptx + 91% match +
+
+
-

Auto-Sorting

-

- Set schedules for automatic organization or trigger manual sorts - whenever you need. -

-
-
- Tag Icon +
+ +
+
+
+ +
+ Semantic Understanding +

+ Analyzes file content, not just names. Research papers go + with research papers, receipts with receipts. +

+
-

Custom Tagging

-

- Add your own tags to files and folders to influence how the - Algorithm organizes your content. -

-
-
-
- Lock Icon +
+ +
+ Lightning Fast +

+ Organize 1000+ files in record time. Find any file in under + 2 seconds with smart search. +

+
+
+
+ +
+ 100% Private +

+ Everything happens on your computer. No cloud uploads, no + data collection, no privacy concerns. +

+
+
+
+ +
+ Mistake-Proof +

+ Preview changes before applying. Don't like the result? Undo + with one click. +

+
-

File Locking

-

- Protect important files and folders from being moved during - organization. -

-
+
+
- -
-
-

Download Smart File Manager

-

- Available for Windows, macOS, and Linux. Get started with your - digital housekeeper today! -

- + +
+
+
+

Powerful Features That Save You Hours

+

Everything you need to take control of your digital files

-
-
- -
-
-
-

Help Center

-

Everything you need to know about Smart File Manager

+
+
+
+ +
+

Smart Sorting

+

+ Advanced clustering algorithms automatically organize files by + meaning, not just type. Customize your folder naming with + camel-case, cebab-case and many more! +

+
+ +
+
+ +
+

Mind-Map File View

+

+ See your files as an interactive graph showing how everything + connects. Navigate naturally through your content. +

+
+ +
+
+ +
+

Smart Search

+

+ Find files by describing what you remember. No more searching for + hours. +

+
+ +
+
+ +
+

Smart Tagging

+

+ Add custom tags that the Algorithm uses for sorting. Tag a few + files as "dogs" and it will stick together with sorting. +

+
+ +
+
+ +
+

Duplicate Hunter

+

+ Find and remove duplicate files instantly. Say goodbye to that + mess. +

+
+ +
+
+ +
+

File Protection

+

+ Lock important files and folders. Automatically protects system + files and project folders from accidental moves. +

+
+
+
-
- - - - + +
+
+
+

Need help getting started?

+

+ Check out our demo videos below, become a expert in Smart File + Manager. +

- -
-

Getting Started

-
-
-

1. Download & Install

-

- Download the appropriate version for your operating system and - follow the installation wizard. -

+
+
+
+
-
-

2. Create Account (Optional)

-

- Register for an account on the app to sync preferences across - devices. -

+

Your First Smart Manager

+

+ See how easy it is to create a Smart Manager with some usefull + tips. +

+ + + Watch Demo + +
+ +
+
+
-
-

3. Create Smart Manager

-

- Choose a folder to manage and let the AI analyze and organize - your files automatically. -

+

Navigate your Smart Manager

+

+ Explore your files through our revolutionary graph-view interface + or the stick with old school navigation. +

+ + + Watch Demo + +
+ +
+
+
-
-

4. Configure Preferences

-

- Set sorting schedules, tagging rules, and organizational - preferences to match your workflow. -

+

Tags and Locking

+

+ Want more control over the smart sorting? See the importance of + adding tags and locking folders/files. +

+ + + Watch Demo + +
+ +
+
+
+

Sort your Manager

+

+ The most exciting feature of them all! Get a full overview of how + you can clean up your file mess. +

+ + + Watch Demo +
-
- -
-

Video Tutorials

-
-
-

Creating Your First Smart Manager

-

- Learn how to set up and configure a Smart Manager for automatic - file organization. -

- Watch Tutorial +
+
+
-
-

Using Graph View

-

- Discover how to navigate and interact with the mind-map - visualization of your files. -

- Watch Tutorial +

Duplicate Files

+

+ Do you have files with the same content and everything feels + cluttered? Easily remove those duplicates. +

+ + + Watch Demo + +
+ +
+
+
-
-

Advanced Search Techniques

-

- Master the art of finding files using semantic search and - metadata filters. -

- Watch Tutorial +

Searching in your Manager

+

+ Struggling to find a certain file by name or contents? Advanced + Search has got you covered! +

+ + + Watch Demo + +
+
+
+
+ + +
+
+
+

Ready to Transform Your Digital Life?

+

+ Download Smart File Manager now and start organizing smarter, not + harder +

+
+ +
+
+
+ +

Windows

+
Available Now
-
-

Customizing Sort Behavior

-

- Learn how to fine-tune the AI sorting algorithm to match your - specific needs. -

- Watch Tutorial +
+

Windows 10 & 11 Compatible

+
+ • 4GB RAM minimum (8GB recommended) + • 500MB storage space required + • 64-bit architecture required + • Works completely offline + *Before install, turn on + Windows Developer Mode +
+ + + Download for Windows +
+
+
- -
-

Frequently Asked Questions

+ +
+
+
+

Questions? We've Got Answers

+
+
-
- How does the AI file organization work? - + +
+ Is it really free? +
- Smart File Manager uses advanced clustering algorithms and natural - language processing to analyze file content, metadata, and - relationships. It groups files by semantic meaning rather than - just file types, creating logical organizational structures. + Yes! Smart File Manager is completely free with no hidden costs, + premium versions, or subscription fees. Built by University of + Pretoria students as a project to help everyone organize their + digital lives better.
-
- Is my data safe and private? - + +
+ Is my data safe? +
- Yes! All file processing happens locally on your machine. We never - store or transmit your file contents. The system only accesses - files you explicitly choose to manage. + Absolutely! Everything happens locally on your computer. We never + upload, store, or access your files. The app works completely + offline and doesn't even require an internet connection.
-
- Can I undo organization changes? - + +
+ What if I don't like how it organizes my files? +
- Absolutely! Every Smart Manager includes a rollback feature that - lets you restore your files to their previous locations with a - single click. + No problem! You always see a preview before any changes are made. + You can also lock important files to prevent them from being + moved.
-
- What file types are supported? - + +
+ How accurate is the clustering organization? +
- Smart File Manager works with all common file types including - documents, images, videos, audio files, code files, and more. The - AI adapts its organization strategy based on the file types in - your directories. + Our testing shows over 90% accuracy for most file types. The + clustering is particularly good with text-based files (documents, + spreadsheets) and gets smarter as you add custom tags and locks.
-
- How do I prevent certain files from being moved? - + +
+ Will it work with my existing folder structure? +
- Use the file locking feature to protect specific files or folders - from being reorganized. Locked items will be excluded from all - future sorting operations. + Yes! You choose which folders to manage with Smart File Manager. + It won't touch anything you don't explicitly tell it to organize, + so your existing important folders stay exactly as they are.
-
- -
-
-

Need More Help?

-

- Our support team is here to help you get the most out of Smart - File Manager. -

-
-

Email: sparkindustries301@gmail.com

+
+
+ What file types does it support? +
-
- Send Email +
+ Smart File Manager works with all common file types: documents, + images, videos, audio files, spreadsheets, presentations, PDFs, + and more. It's especially effective with text-based files that it + can analyze for content.
-
+
+ + +
+
+ + +
+
+ - diff --git a/docs/script.js b/docs/script.js index 12e5da9..5f51363 100644 --- a/docs/script.js +++ b/docs/script.js @@ -1,48 +1,244 @@ -function showLanding() { - document.getElementById("landing-page").style.display = "block"; - document.getElementById("help-page").style.display = "none"; +// Modern JavaScript for Smart File Manager Landing Page +document.addEventListener("DOMContentLoaded", function () { + initializeAnimations(); + initializeFAQ(); + initializeNavigation(); + initializeModals(); + initializeScrollEffects(); +}); + +// Smooth scrolling navigation +function initializeNavigation() { + const navLinks = document.querySelectorAll('nav a[href^="#"]'); + + navLinks.forEach((link) => { + link.addEventListener("click", function (e) { + e.preventDefault(); + + const targetId = this.getAttribute("href"); + const targetSection = document.querySelector(targetId); + + if (targetSection) { + targetSection.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + }); + }); + + // Mobile menu toggle + const navToggle = document.querySelector(".nav-toggle"); + const navLinks2 = document.querySelector(".nav-links"); + + if (navToggle && navLinks2) { + navToggle.addEventListener("click", function () { + navLinks2.classList.toggle("show"); + }); + } +} + +// FAQ functionality +function initializeFAQ() { + const faqItems = document.querySelectorAll(".faq-item"); + + faqItems.forEach((item) => { + const question = item.querySelector(".faq-question"); + + if (question) { + question.addEventListener("click", function () { + // Close other FAQ items + faqItems.forEach((otherItem) => { + if (otherItem !== item) { + otherItem.classList.remove("active"); + } + }); + + // Toggle current item + item.classList.toggle("active"); + }); + } + }); } -function showHelp() { - document.getElementById("landing-page").style.display = "none"; - document.getElementById("help-page").style.display = "block"; +// FAQ toggle function (called from HTML) +function toggleFAQ(element) { + const faqItem = element.closest(".faq-item"); + const faqItems = document.querySelectorAll(".faq-item"); + + // Close other FAQ items + faqItems.forEach((item) => { + if (item !== faqItem) { + item.classList.remove("active"); + } + }); + + // Toggle current item + faqItem.classList.toggle("active"); } -function showPopup(platform) { - alert(`${platform} version is not yet available for download.`); +// Scroll effects +function initializeScrollEffects() { + // Navbar background on scroll + const navbar = document.querySelector("nav"); + + window.addEventListener("scroll", function () { + if (window.scrollY > 50) { + navbar.style.background = "rgba(46, 46, 46, 0.98)"; + navbar.style.backdropFilter = "blur(20px)"; + } else { + navbar.style.background = "rgba(46, 46, 46, 0.95)"; + navbar.style.backdropFilter = "blur(20px)"; + } + }); + + // Intersection Observer for animations + if ("IntersectionObserver" in window) { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.style.opacity = "1"; + entry.target.style.transform = "translateY(0)"; + } + }); + }, + { + threshold: 0.1, + rootMargin: "0px 0px -50px 0px", + } + ); + + // Animate elements on scroll + const animateElements = document.querySelectorAll( + ".feature-card, .demo-card, .platform-card, .pain-item, .benefit-item" + ); + animateElements.forEach((el) => { + el.style.opacity = "0"; + el.style.transform = "translateY(30px)"; + el.style.transition = "opacity 0.6s ease, transform 0.6s ease"; + observer.observe(el); + }); + } } -function showTutorial(tutorial) { - alert(`The "${tutorial}" is not yet available.`); +// Initialize animations +function initializeAnimations() { + // Typing animation for search demo + animateSearchDemo(); } -// Help section navigation -function showHelpSection(sectionId) { - const sections = document.querySelectorAll(".help-section"); - sections.forEach((section) => section.classList.remove("active")); +// Typing animation for search demo +function animateSearchDemo() { + const searchInput = document.querySelector(".search-bar input"); + if (!searchInput) return; - const buttons = document.querySelectorAll(".help-nav-btn"); - buttons.forEach((btn) => btn.classList.remove("active")); + const phrases = [ + "Find research about machine learning...", + "budget spreadsheet from last month", + "presentation about quarterly results", + "photos from vacation 2023", + ]; - document.getElementById(sectionId).classList.add("active"); + let phraseIndex = 0; + let charIndex = 0; + let isDeleting = false; - event.target.classList.add("active"); + function typeWriter() { + const currentPhrase = phrases[phraseIndex]; + + if (isDeleting) { + searchInput.placeholder = currentPhrase.substring(0, charIndex - 1); + charIndex--; + } else { + searchInput.placeholder = currentPhrase.substring(0, charIndex + 1); + charIndex++; + } + + let typeSpeed = isDeleting ? 50 : 100; + + if (!isDeleting && charIndex === currentPhrase.length) { + // Pause before deleting + typeSpeed = 2000; + isDeleting = true; + } else if (isDeleting && charIndex === 0) { + isDeleting = false; + phraseIndex = (phraseIndex + 1) % phrases.length; + typeSpeed = 500; + } + + setTimeout(typeWriter, typeSpeed); + } + + // Start the animation after a delay + setTimeout(typeWriter, 2000); } -// FAQ toggle -function toggleFAQ(element) { - const answer = element.nextElementSibling; - const icon = element.querySelector("span"); - - if (answer.classList.contains("active")) { - answer.classList.remove("active"); - icon.textContent = "+"; - } else { - answer.classList.add("active"); - icon.textContent = "-"; +// Utility functions for backwards compatibility +function showSection(sectionId) { + const section = document.getElementById(sectionId); + if (section) { + section.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } +} + +function showPrivacyInfo() { + alert( + "Smart File Manager is committed to your privacy. All processing happens locally on your device. We never collect, store, or transmit your personal files or data. The application works completely offline and does not require an internet connection." + ); +} + +// Mobile menu toggle +function toggleMobileMenu() { + const navLinks = document.querySelector(".nav-links"); + if (navLinks) { + navLinks.classList.toggle("show"); } } +// Performance optimizations +if ("requestIdleCallback" in window) { + requestIdleCallback(() => { + // Load non-critical resources + initializeNonCriticalFeatures(); + }); +} else { + setTimeout(initializeNonCriticalFeatures, 1000); +} + +function initializeNonCriticalFeatures() { + // Preload demo videos + const demoLinks = document.querySelectorAll( + '.demo-link[href*="drive.google.com"]' + ); + demoLinks.forEach((link) => { + link.addEventListener("mouseover", function () { + // Prefetch the video page + const linkEl = document.createElement("link"); + linkEl.rel = "prefetch"; + linkEl.href = this.href; + document.head.appendChild(linkEl); + }); + }); +} + +// Error handling +window.addEventListener("error", function (e) { + console.error("JavaScript error:", e.error); + + // Optional: Send error to analytics + if (typeof gtag !== "undefined") { + gtag("event", "exception", { + description: e.error.message, + fatal: false, + }); + } +}); + +// Add smooth scrolling for all anchor links document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", function (e) { e.preventDefault(); @@ -54,3 +250,13 @@ document.querySelectorAll('a[href^="#"]').forEach((anchor) => { } }); }); + +// Service Worker registration for offline capabilities (optional) +if ("serviceWorker" in navigator && "caches" in window) { + window.addEventListener("load", () => { + // Uncomment if you want to add service worker + // navigator.serviceWorker.register('/sw.js') + // .then(registration => console.log('SW registered')) + // .catch(error => console.log('SW registration failed')); + }); +} diff --git a/docs/styles.css b/docs/styles.css index 195b0cf..db55e2b 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -1,371 +1,1002 @@ +/* Reset and Base Styles */ * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - - body { - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; - line-height: 1.6; - color: #333; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - min-height: 100vh; - } - - /* Navigation */ - nav { - position: fixed; - top: 0; - width: 100%; - background: #1E1E1E; - backdrop-filter: blur(10px); - z-index: 1000; - padding: 1rem 0; - } - - .nav-container { - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 2rem; - } - - .logo { - display: flex; - align-items: start; - color: #ffa500; - font-size: 1.5rem; - font-weight: bold; - } - - .nav-links { - display: flex; - list-style: none; - gap: 2rem; - } - - .nav-name { - display: flex; - list-style: none; - gap: 1rem; - align-items: center; - } - - .nav-links a { - color: white; - text-decoration: none; - transition: color 0.3s ease; - } - - .nav-links a:hover { - color: #ffa500; - } - - /* Landing Page */ - .landing-page { - display: block; - } - - .hero { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - padding: 2rem; - background: #2E2E2E; - } - - .hero-content { - max-width: 800px; - color: white; - } - - .hero h1 { - font-size: 3.5rem; - margin-bottom: 1rem; - opacity: 0; - animation: fadeInUp 1s ease forwards; - } - - .hero p { - font-size: 1.3rem; - margin-bottom: 2rem; - opacity: 0; - animation: fadeInUp 1s ease 0.3s forwards; - } - - .cta-buttons { - display: flex; - gap: 1rem; - justify-content: center; - flex-wrap: wrap; - opacity: 0; - animation: fadeInUp 1s ease 0.6s forwards; - } - - .btn { - padding: 15px 30px; - border: none; - border-radius: 50px; - font-size: 1.1rem; - font-weight: bold; - cursor: pointer; - transition: all 0.3s ease; - text-decoration: none; - display: inline-block; - } - - .btn-primary { - background: #FFB400; - color: white; - } - - - - .btn-primary:hover { - background: white; - color: #FFB400; - transform: translateY(-5px); - } - - .btn-secondary { - background: transparent; - color: white; - border: 2px solid white; - } - - .btn-secondary:hover { - background: white; - color: #333; - } - - /* Features Section */ - .features { - padding: 5rem 2rem; - background: white; - } - - .features-container { - max-width: 1200px; - margin: 0 auto; - } - - .features h2 { - text-align: center; - font-size: 2.5rem; - margin-bottom: 3rem; - color: #333; - } - - .features-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - } - - .feature-card { - background: #f8f9fa; - padding: 2rem; - border-radius: 15px; - text-align: center; - transition: transform 0.3s ease; - } - - .feature-card:hover { - transform: translateY(-5px); - } - - .feature-icon { - font-size: 3rem; - margin-bottom: 1rem; - } - - .feature-card h3 { - font-size: 1.5rem; - margin-bottom: 1rem; - color: #333; - } - - /* Download Section */ - .download { - padding: 5rem 2rem; - background: #2E2E2E; - color: white; - text-align: center; - } - - .download-container { - max-width: 800px; - margin: 0 auto; - } - - .download h2 { - font-size: 2.5rem; - margin-bottom: 2rem; - } - - .platform-buttons { - display: flex; - gap: 1rem; - justify-content: center; - flex-wrap: wrap; - margin-top: 2rem; - } - - .platform-btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 15px 25px; - background: rgba(255, 255, 255, 0.1); - border: 2px solid rgba(255, 255, 255, 0.3); - border-radius: 10px; - color: white; - text-decoration: none; - transition: all 0.3s ease; - } - - .platform-btn:hover { - background: rgba(255, 255, 255, 0.2); - transform: translateY(-2px); - } - - /* Help Page */ - .help-page { - display: none; - background: white; - min-height: 100vh; - padding-top: 80px; - } - - .help-container { - max-width: 1000px; - margin: 0 auto; - padding: 2rem; - } - - .help-header { - text-align: center; - margin-bottom: 3rem; - } - - .help-header h1 { - font-size: 2.5rem; - color: #333; - margin-bottom: 1rem; - } - - .help-nav { - display: flex; - justify-content: center; - gap: 1rem; - margin-bottom: 3rem; - flex-wrap: wrap; - } - - .help-nav-btn { - padding: 10px 20px; - background: #f8f9fa; - border: none; - border-radius: 25px; - cursor: pointer; - transition: all 0.3s ease; - } - - .help-nav-btn.active { - background: #FFB400; - color: white; - } - - .help-section { - display: none; - } - - .help-section.active { - display: block; - } - - .faq-item { - background: #f8f9fa; - margin-bottom: 1rem; - border-radius: 10px; - overflow: hidden; - } - - .faq-question { - padding: 1.5rem; - background: #e9ecef; - cursor: pointer; - font-weight: bold; - display: flex; - justify-content: space-between; - align-items: center; - } - - .faq-answer { - padding: 1.5rem; - display: none; - } - - .faq-answer.active { - display: block; - } - - .tutorial-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - } - - .tutorial-card { - background: #f8f9fa; - padding: 2rem; - border-radius: 15px; - text-align: center; - } - - .tutorial-card h3 { - color: #333; - margin-bottom: 1rem; - } - - .contact-info { - background: #f8f9fa; - padding: 2rem; - border-radius: 15px; - text-align: center; - } - - /* Animations */ - @keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - /* Responsive Design */ - @media (max-width: 768px) { - .hero h1 { - font-size: 2.5rem; - } - - .hero p { - font-size: 1.1rem; - } - - .cta-buttons { - flex-direction: column; - align-items: center; - } - - .nav-links { - display: none; - } - } \ No newline at end of file + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #ffffff; + background: #1E1E1E; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; +} + +h1 { font-size: 3.5rem; } +h2 { font-size: 2.5rem; } +h3 { font-size: 1.8rem; } +h4 { font-size: 1.4rem; } + +p { + margin-bottom: 1rem; + line-height: 1.7; +} + +/* Gradient Text */ +.gradient-text { + background: linear-gradient(135deg, #FFB400 0%, #FF8C00 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 1rem 2rem; + border: none; + border-radius: 50px; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; + font-size: 1rem; +} + +.btn-primary { + background: #FFB400; + color: #1E1E1E; + font-weight: 700; +} + +.btn-primary:hover { + background: #fdbf2f; + transform: scale(1.1); +} + +.btn-secondary { + background: transparent; + color: #FFB400; + border: 2px solid #FFB400; +} + +.btn-secondary:hover { + background: #FFB400; + color: #1E1E1E; + transform: translateY(-3px); +} + +.btn-small { + padding: 0.6rem 1.5rem; + font-size: 0.9rem; +} + +.btn-large { + padding: 1.25rem 2.5rem; + font-size: 1.1rem; +} + + +/* Navigation */ +nav { + position: fixed; + top: 0; + width: 100%; + background: rgba(46, 46, 46, 0.95); + backdrop-filter: blur(20px); + border-bottom: 1px solid rgba(61, 61, 61, 0.3); + z-index: 1000; + padding: 1rem 0; +} + +.nav-container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 2rem; +} + +.nav-brand { + display: flex; + align-items: center; + gap: 1rem; +} + +.nav-logo { + width: 40px; + height: 40px; +} + +.logo { + font-size: 1.5rem; + font-weight: 700; + color: #FFB400; +} + +.nav-links { + display: flex; + list-style: none; + gap: 2rem; +} + +.nav-links a { + text-decoration: none; + color: #ffffff; + font-weight: 500; + transition: color 0.3s ease; + position: relative; +} + +.nav-links a:hover { + color: #FFB400; +} + +.nav-links a::after { + content: ''; + position: absolute; + bottom: -5px; + left: 0; + width: 0; + height: 2px; + background: linear-gradient(135deg, #FFB400 0%, #FF8C00 100%); + transition: width 0.3s ease; +} + +.nav-links a:hover::after { + width: 100%; +} + +.nav-toggle { + display: none; + cursor: pointer; + font-size: 1.5rem; + color: #ffffff; +} + +/* Hero Section */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + padding: 8rem 2rem 2rem; + background: #1E1E1E; + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.5; +} + +.hero-content { + max-width: 1200px; + margin: 0 auto; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; + position: relative; + z-index: 1; +} + + +.hero-title { + font-size: 4rem; + font-weight: 800; + line-height: 1.1; + margin-bottom: 1.5rem; + color: #ffffff; +} + +.hero-subtitle { + font-size: 1.3rem; + color: #9CA3AF; + margin-bottom: 2rem; + line-height: 1.6; +} + + +/* Hero Visual */ +.hero-visual { + display: flex; +} + +.hero-demo { + display: flex; + gap: 2rem; + background: #2E2E2E; + border-radius: 20px; + padding: 2rem; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3); + border: 1px solid #3D3D3D; +} + +.demo-before h4, +.demo-after h4 { + margin-bottom: 1rem; + font-size: 1rem; + font-weight: 600; +} + +.demo-before h4 { + color: #dc2626; +} + +.demo-after h4 { + color: #059669; +} + +.file-mess { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.file-item { + padding: 0.5rem; + white-space: nowrap; + border-radius: 8px; + font-size: 0.8rem; + font-family: 'Courier New', monospace; + color: #ffffff; + background: #1E1E1E; + border: 1px solid #3D3D3D; + margin-left: 1rem; +} + +.demo-arrow { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 0.5rem; + color: #FFB400; + font-weight: 600; +} + +.demo-arrow i { + font-size: 2rem; + animation: pulse 2s infinite; +} + +.file-organized { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.folder-group { + border: 1px solid #3D3D3D; + border-radius: 8px; + overflow: hidden; +} + +.folder-header { + background: #2E2E2E; + padding: 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + font-size: 0.9rem; + color: #ffffff; +} + +.folder-files { + padding: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.folder-files .file-item { + background: #1E1E1E; + border: 1px solid #3D3D3D; + margin-left: 1rem; +} + + +/* Solution Section */ +.solution { + padding: 6rem 0; + background: #2E2E2E; +} + +.solution-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; +} + + +.benefits-list { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.benefit-item { + display: flex; + gap: 1rem; + align-items: flex-start; +} + +.benefit-item i { + font-size: 1.5rem; + color: #FFB400; + margin-top: 0.25rem; + flex-shrink: 0; +} + +.benefit-item strong { + display: block; + color: #ffffff; + margin-bottom: 0.5rem; +} + +.benefit-item p { + color: #9CA3AF; + margin: 0; +} + +/* Feature Showcase */ +.feature-showcase { + background: #1E1E1E; + border-radius: 16px; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3); + overflow: hidden; + border: 1px solid #3D3D3D; +} + +.showcase-header { + background: #2E2E2E; + padding: 1rem; + display: flex; + align-items: center; + gap: 1rem; + border-bottom: 1px solid #3D3D3D; +} + +.window-controls { + display: flex; + gap: 0.5rem; +} + +.control { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.control.red { background: #ef4444; } +.control.yellow { background: #f59e0b; } +.control.green { background: #10b981; } + +.showcase-header span { + font-weight: 600; + color: #ffffff; +} + +.showcase-content { + padding: 2rem; +} + +.search-bar { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: #2E2E2E; + border-radius: 12px; + border: 1px solid #3D3D3D; + margin-bottom: 1.5rem; +} + +.search-bar i { + color: #9CA3AF; +} + +.search-bar input { + flex: 1; + border: none; + background: transparent; + outline: none; + font-family: inherit; + color: #ffffff; +} + +.search-results { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.result-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: #2E2E2E; + border: 1px solid #3D3D3D; + border-radius: 8px; +} + +.result-item i { + color: #FFB400; +} + +.result-item span:nth-child(2) { + flex: 1; + font-weight: 500; + color: #ffffff; +} + +.relevance { + background: #dcfce7; + color: #166534; + padding: 0.25rem 0.75rem; + border-radius: 50px; + font-size: 0.8rem; + font-weight: 600; +} + +/* Features Section */ +.features { + padding: 6rem 0; + background: #1E1E1E; +} + +.section-header { + text-align: center; + margin-bottom: 4rem; +} + +.section-header h2 { + margin-bottom: 1rem; + color: #ffffff; +} + +.section-header p { + color: #9CA3AF; + font-size: 1.1rem; + max-width: 600px; + margin: 0 auto; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.feature-card { + background: #2E2E2E; + padding: 2.5rem; + border-radius: 16px; + text-align: center; + transition: all 0.3s ease; + border: 1px solid #3D3D3D; + position: relative; +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + + +.feature-icon { + margin-bottom: 1.5rem; +} + +.feature-icon i { + font-size: 3rem; + color: #FFB400; +} + +.feature-card h3 { + margin-bottom: 1rem; + color: #ffffff; +} + +.feature-card p { + color: #9CA3AF; + line-height: 1.6; + margin: 0; +} + + +/* Demo Section */ +.demo-section { + padding: 6rem 0; + background: #2E2E2E; +} + +.demo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 3rem; + margin-top: 3rem; +} + +.demo-card { + text-align: center; + padding: 2rem; + border-radius: 16px; + border: 1px solid #e5e7eb; + transition: all 0.3s ease; +} + +.demo-card:hover { + transform: translateY(-5px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.demo-thumbnail { + width: 100px; + height: 100px; + margin: 0 auto 1.5rem; + background: #FFB400; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 3rem; + color: white; + cursor: pointer; + transition: all 0.3s ease; +} + +.demo-thumbnail:hover { + transform: scale(1.1); +} + +.demo-card h3 { + margin-bottom: 1rem; + color: #FFFFFF; +} + +.demo-card p { + color: #6b7280; + margin-bottom: 1.5rem; +} + +.demo-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: #FFB400; + text-decoration: none; + font-weight: 600; + transition: color 0.3s ease; +} + +.demo-link:hover { + color: #764ba2; +} + +/* Download Section */ +.download-section { + padding: 6rem 0; + background:#1E1E1E; + color: white; + position: relative; + overflow: hidden; +} + +.download-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.5; +} + +.download-header { + text-align: center; + margin-bottom: 4rem; + position: relative; + z-index: 1; +} + +.download-header h2 { + color: white; + margin-bottom: 1rem; +} + +.download-header p { + color: rgba(255, 255, 255, 0.8); + font-size: 1.1rem; + margin-bottom: 2rem; +} + +.urgency-indicators { + display: flex; + justify-content: center; + gap: 2rem; + flex-wrap: wrap; +} + +.indicator { + display: flex; + align-items: center; + gap: 0.5rem; + color: rgba(255, 255, 255, 0.9); + font-size: 0.9rem; +} + +.indicator i { + color: #f59e0b; +} + +.download-platforms { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-bottom: 4rem; + position: relative; + z-index: 1; +} + +.platform-card { + background: #2E2E2E; + color: #ffffff; + padding: 2.5rem; + border-radius: 16px; + text-align: center; + transition: all 0.3s ease; + position: relative; + border: 1px solid #3D3D3D; + max-width: 500px; + margin: 0 auto; +} + +.platform-card:hover { + transform: translateY(-10px); + box-shadow: 0 30px 60px rgba(0, 0, 0, 0.2); +} + +.platform-card.primary { + border: 3px solid #FFB400; +} + +.platform-header { + margin-bottom: 2rem; + position: relative; +} + +.platform-header i { + font-size: 4rem; + margin-bottom: 1rem; + color: #FFB400; + display: block; +} + +.platform-badge { + position: absolute; + top: -15px; + right: -15px; + background: #f59e0b; + color: white; + padding: 0.5rem 1rem; + border-radius: 50px; + font-size: 0.8rem; + font-weight: 600; + transform: rotate(15deg); +} + +.platform-details { + margin-bottom: 2rem; +} + +.platform-details p { + font-weight: 600; + margin-bottom: 1rem; + color: #ffffff; +} + +.specs { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.specs span { + font-size: 0.9rem; + color: #9CA3AF; +} + +.platform-note { + margin-top: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + color: #9CA3AF; + font-size: 0.9rem; + font-style: italic; +} + +.platform-note i { + color: #FFB400; +} + + +/* FAQ Section */ +.faq-section { + padding: 6rem 0; + background: #2E2E2E; +} + +.faq-grid { + display: grid; + gap: 1rem; + max-width: 800px; + margin: 0 auto; +} + +.faq-item { + background: #1E1E1E; + border-radius: 12px; + overflow: hidden; + border: 1px solid #3D3D3D; +} + +.faq-question { + padding: 1.5rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + color: #ffffff; + background: #2E2E2E; + transition: background 0.3s ease; +} + +.faq-question:hover { + background: #3D3D3D; +} + +.faq-question i { + color: #FFB400; + transition: transform 0.3s ease; +} + +.faq-item.active .faq-question i { + transform: rotate(180deg); +} + +.faq-answer { + padding: 0; + max-height: 0; + overflow: hidden; + transition: all 0.3s ease; + background: #1E1E1E; +} + +.faq-item.active .faq-answer { + padding: 1.5rem; + max-height: 200px; +} + +.faq-answer p { + color: #9CA3AF; + line-height: 1.6; + margin: 0; +} + + +/* Footer */ +.footer { + background: #2E2E2E; + color: white; + padding: 3rem 0 2rem; +} + +.footer-content { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.footer-brand { + display: flex; + align-items: center; + gap: 1rem; +} + +.footer-logo { + width: 40px; + height: 40px; +} + +.footer-links { + display: flex; + gap: 2rem; +} + +.footer-links a { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-links a:hover { + color: white; +} + +.footer-bottom { + text-align: center; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; +} + + +/* Animations */ +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .hero-content { + grid-template-columns: 1fr; + text-align: center; + } + + .solution-content { + grid-template-columns: 1fr; + text-align: center; + } + + .hero-stats { + justify-content: center; + } +} + +@media (max-width: 768px) { + .nav-links { + display: none; + } + + .nav-cta { + display: none; + } + + .nav-toggle { + display: block; + } + + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.1rem; + } + + .hero-stats { + gap: 2rem; + } + + .stat-number { + font-size: 2rem; + } + + h2 { + font-size: 2rem; + } + + .features-grid { + grid-template-columns: 1fr; + } + + .pain-grid { + grid-template-columns: 1fr; + } + + .demo-grid { + grid-template-columns: 1fr; + } + + .download-platforms { + grid-template-columns: 1fr; + } + + .urgency-indicators { + flex-direction: column; + gap: 1rem; + } + + .guarantee-box { + flex-direction: column; + text-align: center; + } + + .footer-content { + flex-direction: column; + gap: 2rem; + text-align: center; + } + + .footer-links { + flex-wrap: wrap; + justify-content: center; + } +} + +@media (max-width: 480px) { + .container { + padding: 0 1rem; + } + + .hero { + padding: 6rem 1rem 2rem; + } + + .hero-title { + font-size: 2rem; + } + + .hero-stats { + flex-direction: column; + gap: 1rem; + } + + .btn { + width: 100%; + justify-content: center; + } + +} \ No newline at end of file diff --git a/golang/filesystem/directoryToObject.go b/golang/filesystem/directoryToObject.go index 415abf8..9c54132 100644 --- a/golang/filesystem/directoryToObject.go +++ b/golang/filesystem/directoryToObject.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "time" ) @@ -78,24 +79,62 @@ func exploreDown(folder *Folder, path string) error { } } for _, file := range folder.Files { - if strings.HasPrefix(file.Name, ".") { + if strings.HasPrefix(file.Name, ".") || strings.HasPrefix(file.Name, "~") { file.Lock() - // fmt.Printf("Auto-locked folder '%s' and contents because it contains hidden folder '%s'\n", folder.Path, sub.Name) - break } } + return nil } +func ConvertToWSLPath(p string) string { + p = strings.TrimSpace(p) + if p == "" { + return p + } + p = strings.ReplaceAll(p, "\\", "/") + + if runtime.GOOS == "windows" { + return filepath.Clean(filepath.FromSlash(p)) + } + + if runtime.GOOS == "linux" && isWSL() { + if strings.HasPrefix(p, "/") { + return filepath.Clean(p) + } + + if len(p) >= 2 && p[1] == ':' { + drive := strings.ToLower(string(p[0])) + rest := p[2:] + if rest == "" || rest[0] != '/' { + rest = "/" + rest + } + return filepath.Clean("/mnt/" + drive + rest) + } + + return filepath.Clean(p) + } + + return filepath.Clean(p) +} -func ConvertToWSLPath(winPath string) string { - winPath = strings.TrimSpace(winPath) - winPath = strings.ReplaceAll(winPath, "\\", "/") +func isWSL() bool { + if _, ok := os.LookupEnv("WSL_DISTRO_NAME"); ok { + return true + } + if _, ok := os.LookupEnv("WSL_INTEROP"); ok { + return true + } - if len(winPath) > 2 && winPath[1] == ':' { - drive := strings.ToLower(string(winPath[0])) - rest := winPath[2:] - return "/mnt/" + drive + rest + if data, err := os.ReadFile("/proc/version"); err == nil { + if strings.Contains(strings.ToLower(string(data)), "microsoft") { + return true + } + } + if data, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil { + if strings.Contains(strings.ToLower(string(data)), "microsoft") { + return true + } } - return winPath + return false } diff --git a/golang/filesystem/duplicateFiles.go b/golang/filesystem/duplicateFiles.go index 4fe0c78..d16aea3 100644 --- a/golang/filesystem/duplicateFiles.go +++ b/golang/filesystem/duplicateFiles.go @@ -82,6 +82,8 @@ func collectBySize(folder *Folder, buckets map[int64][]string) { } func findDuplicateFilesHandler(w http.ResponseWriter, r *http.Request) { + // w.WriteHeader(http.StatusOK) + // w.Write([]byte("OK")) name := r.URL.Query().Get("name") mu.Lock() defer mu.Unlock() diff --git a/golang/filesystem/handler_keywordSearch.go b/golang/filesystem/handler_keywordSearch.go index 708d246..a53e8c1 100644 --- a/golang/filesystem/handler_keywordSearch.go +++ b/golang/filesystem/handler_keywordSearch.go @@ -16,8 +16,8 @@ import ( pb "github.com/COS301-SE-2025/Smart-File-Manager/golang/client/protos" ) -const limitKeywordSearch int = 20 -const maxDistKeywordSearch int = 3 +const limitKeywordSearch int = 15 +const maxDistKeywordSearch int = 5 //flow idea: // app starts @@ -166,7 +166,21 @@ func getMatchesByKeywords(searchTerms []string, composite *Folder) *safeResults } res.rankedFiles = unique + sort.SliceStable(res.rankedFiles, func(i, j int) bool { + if res.rankedFiles[i].distance != res.rankedFiles[j].distance { + return res.rankedFiles[i].distance < res.rankedFiles[j].distance + } + ni := strings.ToLower(res.rankedFiles[i].file.Name) + nj := strings.ToLower(res.rankedFiles[j].file.Name) + if ni != nj { + return ni < nj + } + + return filepath.Clean(res.rankedFiles[i].file.Path) < filepath.Clean(res.rankedFiles[j].file.Path) + }) + return res + } func exploreFolderForKeywords(f *Folder, searchTerms []string, c chan<- rankedFile, wg *sync.WaitGroup) { @@ -257,7 +271,7 @@ func LevenshteinDistForKeywords(searchText string, fileKeyword string) int { } func pythonExtractKeywords(c *Folder) { - err := grpcFunc(c, "KEYWORDS") + err := grpcFunc(c, "KEYWORDS", "CAMEL") if err != nil { log.Fatalf("grpcFunc failed: %v", err) } @@ -396,8 +410,6 @@ func ExtractKeywordsRAKE(filePath string, topN int, maxSize int64) ([]*pb.Keywor return ExtractKeywordsFromText(string(data), topN), nil } -// Appends unique keywords from src into dst (by exact Keyword string match), -// and caps the result at maxKeywords (30). Does not alter order of existing dst. func AppendUniqueKeywords(dst, src []*pb.Keyword) []*pb.Keyword { const maxKeywords = 30 diff --git a/golang/filesystem/handler_loadTree.go b/golang/filesystem/handler_loadTree.go index 198c31a..5acd378 100644 --- a/golang/filesystem/handler_loadTree.go +++ b/golang/filesystem/handler_loadTree.go @@ -3,10 +3,12 @@ package filesystem import ( "context" "encoding/json" + "errors" "fmt" "log" "net/http" "os" + "path/filepath" "sort" "strconv" "strings" @@ -46,13 +48,69 @@ type Metadata struct { LastModified string `json:"lastModified"` } -func grpcFunc(c *Folder, requestType string) error { +func loadEnvFile(path string) (map[string]string, error) { + vars := make(map[string]string) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + // remove "export " if present + line = strings.TrimPrefix(line, "export ") + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + vars[parts[0]] = parts[1] + } + } + return vars, nil +} + +func FindProjectRoot(filename string) (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + candidate := filepath.Join(dir, filename) + if _, err := os.Stat(candidate); err == nil { + return candidate, nil + } + + parent := filepath.Dir(dir) + if parent == dir { // reached filesystem root + break + } + dir = parent + } + + return "", fmt.Errorf("%s not found", filename) +} + +func grpcFunc(c *Folder, requestType string, preferredCase string) error { + + path, err := FindProjectRoot("server.env") + if err != nil { + log.Fatalf("failed to find project root: %v", err) + } + + env, err := loadEnvFile(path) + if err != nil { + log.Fatalf("failed to read env file: %v", err) + } + + port := env["PYTHON_SERVER"] if requestType != "METADATA" && requestType != "CLUSTERING" && requestType != "KEYWORDS" { return fmt.Errorf("invalid requestType: %s", requestType) } - conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient("localhost:"+port, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("could not create new grpc go client") } @@ -62,11 +120,19 @@ func grpcFunc(c *Folder, requestType string) error { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) defer cancel() + shh, found := os.LookupEnv("SFM_SERVER_SECRET") + if !found { + fmt.Println("secret not found") + return errors.New("server secret not found error") + } req := &pb.DirectoryRequest{ - Root: convertFolderToProto(*c), - RequestType: requestType, + Root: convertFolderToProto(*c), + RequestType: requestType, + PreferredCase: preferredCase, + ServerSecret: shh, } + // printDirectoryWithMetadata(req.Root, 0) resp, err := client.SendDirectoryStructure(ctx, req) @@ -74,7 +140,7 @@ func grpcFunc(c *Folder, requestType string) error { log.Fatalf("SendDirectoryStructure RPC failed: %v", err) } - fmt.Printf("Server returned root directory: name=%q, path=%q/n", resp.Root.GetName(), resp.Root.GetPath()) + // fmt.Printf("Server returned root directory: name=%q, path=%q/n", resp.Root.GetName(), resp.Root.GetPath()) switch requestType { case "KEYWORDS": @@ -265,26 +331,26 @@ func metadataConverter(metaDataArr []*pb.MetadataEntry) []*MetadataEntry { } // convert filesystem metadataEntry[] to Metadata struct for json response -func extractMetadata(metaDataArr []*MetadataEntry) *Metadata { - // fmt.Println("extractMetadata called. metaDataArr len: " + strconv.Itoa(len(metaDataArr))) +// func extractMetadata(metaDataArr []*MetadataEntry) *Metadata { +// // fmt.Println("extractMetadata called. metaDataArr len: " + strconv.Itoa(len(metaDataArr))) - md := &Metadata{} +// md := &Metadata{} - fieldMap := map[string]*string{ - "size_bytes": &md.Size, - "created": &md.DateCreated, - "mime_type": &md.MimeType, - "modified": &md.LastModified, - } +// fieldMap := map[string]*string{ +// "size_bytes": &md.Size, +// "created": &md.DateCreated, +// "mime_type": &md.MimeType, +// "modified": &md.LastModified, +// } - for _, entry := range metaDataArr { - // fmt.Println(entry) - if ptr, ok := fieldMap[entry.Key]; ok { - *ptr = entry.Value - } - } - return md -} +// for _, entry := range metaDataArr { +// // fmt.Println(entry) +// if ptr, ok := fieldMap[entry.Key]; ok { +// *ptr = entry.Value +// } +// } +// return md +// } // func printDirectoryWithMetadata(dir *pb.Directory, num int) { @@ -367,9 +433,8 @@ func printDirChildren(d *pb.Directory, prefix string) { } fmt.Printf("%s%s%s\n", prefix, branch, fileLabel(f)) fmt.Printf("%s%s%s%s\n", prefix, branch, fileLabel(f), "'s keyw:") - for _, kw := range f.Keywords { - fmt.Printf("%s%s kw is: %s\n", prefix, branch, kw.Keyword) - } + + fmt.Printf("%s%s locked is: %s\n", prefix, branch, strconv.FormatBool(f.IsLocked)) } } @@ -414,7 +479,7 @@ func fileLabel(f *pb.File) string { // endpoint called using no grpc: func loadTreeDataHandlerGoOnly(w http.ResponseWriter, r *http.Request) { - fmt.Println("GOVERSION OF loadTree called") + // fmt.Println("GOVERSION OF loadTree called") w.Header().Set("Content-Type", "application/json") name := r.URL.Query().Get("name") diff --git a/golang/filesystem/handler_search.go b/golang/filesystem/handler_search.go index b0bd97e..44887a8 100644 --- a/golang/filesystem/handler_search.go +++ b/golang/filesystem/handler_search.go @@ -6,12 +6,14 @@ import ( "math" "net/http" "path/filepath" + "sort" "strings" "sync" ) const limit int = 25 -const maxDist int = 25 +const maxDist int = 8 +const similarityThreshold = 0.5 func LevenshteinDist(searchText string, fileName string) int { if len(searchText) == 0 { @@ -48,7 +50,7 @@ func LevenshteinDist(searchText string, fileName string) int { // BOOST exact substrings var boost float32 = 1 //lower boost is better as it makes the distance smaller if strings.Contains(fileName, searchText) { - boost = 0.2 + boost = 0.1 } // now fall back on full Levenshtein @@ -120,9 +122,6 @@ func SearchHandler(w http.ResponseWriter, r *http.Request) { compositeName := r.URL.Query().Get("compositeName") searchText := r.URL.Query().Get("searchText") - w.Header().Set("Access-Control-Allow-Origin", "*") // Allow all origins, or specify your frontend origin - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") for _, comp := range Composites { if comp.Name == compositeName { @@ -209,7 +208,7 @@ func getMatches(text string, composite *Folder) *safeResults { //if current is better if iteratedRankedFile.distance > currentRankedFile.distance { - if len(res.rankedFiles) < limit { + if len(res.rankedFiles) < limit && (currentRankedFile.distance < maxDist) { //insert by shifting over to make the array sorted res.rankedFiles = append( append(res.rankedFiles[:i], currentRankedFile), @@ -233,7 +232,7 @@ func getMatches(text string, composite *Folder) *safeResults { } // if limit is not reached and not inserted then we insert at the end if len(res.rankedFiles) < limit { - if !inserted { + if !inserted && (currentRankedFile.distance < maxDist) { res.rankedFiles = append(res.rankedFiles, currentRankedFile) } } @@ -256,12 +255,21 @@ func getMatches(text string, composite *Folder) *safeResults { } res.rankedFiles = unique + sort.SliceStable(res.rankedFiles, func(i, j int) bool { + if res.rankedFiles[i].distance != res.rankedFiles[j].distance { + return res.rankedFiles[i].distance < res.rankedFiles[j].distance + } + return strings.ToLower(res.rankedFiles[i].file.Name) < strings.ToLower(res.rankedFiles[j].file.Name) + }) + return res } func exploreFolder(f *Folder, text string, c chan<- rankedFile, wg *sync.WaitGroup) { defer wg.Done() + lowSearch := strings.ToLower(text) + for _, folder := range f.Subfolders { wg.Add(1) go exploreFolder(folder, text, c, wg) @@ -269,9 +277,44 @@ func exploreFolder(f *Folder, text string, c chan<- rankedFile, wg *sync.WaitGro for _, file := range f.Files { dist := LevenshteinDist(text, file.Name) - if dist <= maxDist { + if strings.Contains(strings.ToLower(file.Name), lowSearch) { + dist = 0 + } + maxLen := len(text) + if len(file.Name) > maxLen { + maxLen = len(file.Name) + } + if maxLen == 0 { + continue + } + similarity := 1.0 - float64(dist)/float64(maxLen) + + // dynamic minimum similarity depending on search length + minSim := similarityThreshold + switch { + case len(text) == 0: + continue + case len(text) <= 2: + //initial must match + if !strings.HasPrefix(strings.ToLower(file.Name), lowSearch) { + continue + } + minSim = 0.90 + + case len(text) <= 5: + minSim = 0.75 + + case len(text) <= 8: + minSim = 0.60 + + default: + minSim = similarityThreshold + } + + if dist <= maxDist && similarity >= minSim { c <- rankedFile{file: *file, distance: dist} } } + } diff --git a/golang/filesystem/handler_sortTree.go b/golang/filesystem/handler_sortTree.go index e6023e2..5488bf7 100644 --- a/golang/filesystem/handler_sortTree.go +++ b/golang/filesystem/handler_sortTree.go @@ -4,6 +4,7 @@ import ( "encoding/json" "log" "net/http" + "slices" //grpc imports ) @@ -12,13 +13,22 @@ func sortTreeHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") name := r.URL.Query().Get("name") + caseParam := r.URL.Query().Get("case") + if caseParam == "" { + caseParam = "CAMEL" + } + allowedCases := []string{"CAMEL", "SNAKE", "PASCAL", "KEBAB", "SPACE"} + if !slices.Contains(allowedCases, caseParam) { + http.Error(w, "Invalid case type.", http.StatusBadRequest) + return + } mu.Lock() defer mu.Unlock() for _, c := range Composites { if c.Name == name { // build the nested []FileNode - err := grpcFunc(c, "CLUSTERING") + err := grpcFunc(c, "CLUSTERING", caseParam) if err != nil { log.Fatalf("grpcFunc failed: %v", err) http.Error(w, "internal server error, GRPC CALLED WRONG", http.StatusInternalServerError) diff --git a/golang/filesystem/index.html b/golang/filesystem/index.html deleted file mode 100644 index 3d2c3dd..0000000 --- a/golang/filesystem/index.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - File Search - - - -
-

File Search

- -
    -
    - - - - \ No newline at end of file diff --git a/golang/filesystem/moveDirectory.go b/golang/filesystem/moveDirectory.go index d4c6029..47a4e5e 100644 --- a/golang/filesystem/moveDirectory.go +++ b/golang/filesystem/moveDirectory.go @@ -19,31 +19,26 @@ func moveDirectoryHandler(w http.ResponseWriter, r *http.Request) { defer mu.Unlock() for i, item := range Composites { - fmt.Printf("Checking manager: %s\n", item.Name) + // fmt.Printf("Checking manager: %s\n", item.Name) if item.Name == compositeName { - fmt.Printf("found manager: %s\n", item.Name) - // Build the directory structure in original path + // fmt.Printf("found manager: %s\n", item.Name) CreateDirectoryStructure(item) - // Move the content into new manager folder moveContent(item) - //update composite in memory + name := item.Name path := item.Path Composites = append(Composites[:i], Composites[i+1:]...) - // Remove from type storage delete(ObjectMap, item.Path) data, err := os.ReadFile(managersFilePath) var recs []ManagerRecord if err == nil { - // File exists — update entry if err := json.Unmarshal(data, &recs); err != nil { fmt.Println("error in unmarshaling of json") panic(err) } - // Remove the record with the matching name for j := range recs { if recs[j].Name == name { recs = append(recs[:j], recs[j+1:]...) @@ -51,7 +46,7 @@ func moveDirectoryHandler(w http.ResponseWriter, r *http.Request) { } } } else if os.IsNotExist(err) { - // Nothing to do if file doesn't exist + } else { panic(err) } @@ -68,6 +63,13 @@ func moveDirectoryHandler(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error adding manager: %v", err) } + + err = UpdateStoredPathsFromComposite(item) + if err != nil { + log.Printf("Error updating stored paths from composite: %v", err) + } + fmt.Println("responding with true") + w.Write([]byte("true")) return } @@ -77,14 +79,11 @@ func moveDirectoryHandler(w http.ResponseWriter, r *http.Request) { } func moveContent(item *Folder) { - // Get parent directory of original path (e.g. "/home/...") parentDir := filepath.Dir(item.Path) originalPath := item.Path - // New root = parent directory + manager name root = parentDir - // Create new root folder if not exists if err := os.MkdirAll(root, 0755); err != nil { panic(err) } @@ -94,16 +93,14 @@ func moveContent(item *Folder) { item.Path = filepath.Join(root, item.Name) if originalPath != item.Path { os.RemoveAll(originalPath) - } - managersFilePath := filepath.Join(getPath(), "golang", managersFilePath) + managersFilePath := filepath.Join(getPath(), managersFilePath) data, err := os.ReadFile(managersFilePath) var recs []ManagerRecord if err == nil { - // File exists — update entry if err := json.Unmarshal(data, &recs); err != nil { fmt.Println("error in unmarshaling of json") panic(err) @@ -114,7 +111,6 @@ func moveContent(item *Folder) { } } } else if os.IsNotExist(err) { - // Create storage folder if missing if err := os.MkdirAll(filepath.Dir(managersFilePath), 0755); err != nil { panic(err) } @@ -130,7 +126,6 @@ func moveContent(item *Folder) { if err := os.WriteFile(managersFilePath, out, 0644); err != nil { panic(err) } - } func moveContentRecursive(item *Folder) { @@ -142,30 +137,29 @@ func moveContentRecursive(item *Folder) { sourcePath := file.Path targetPath := filepath.Join(root, file.NewPath) - // Create target directory if it doesn't exist targetDir := filepath.Dir(targetPath) os.MkdirAll(targetDir, os.ModePerm) - // Handle duplicate files by generating unique names finalTargetPath := generateUniqueFilePath(targetPath) if err := os.Rename(sourcePath, finalTargetPath); err != nil { log.Printf("Error moving file %s to %s: %v", sourcePath, finalTargetPath, err) + } else { + file.Path = finalTargetPath } } for _, subfolder := range item.Subfolders { + subfolder.Path = filepath.Join(root, subfolder.NewPath) moveContentRecursive(subfolder) } } func generateUniqueFilePath(targetPath string) string { - // If file doesn't exist, return original path if _, err := os.Stat(targetPath); os.IsNotExist(err) { return targetPath } - // File exists, generate unique name dir := filepath.Dir(targetPath) filename := filepath.Base(targetPath) ext := filepath.Ext(filename) @@ -184,7 +178,6 @@ func generateUniqueFilePath(targetPath string) string { } func CreateDirectoryStructure(item *Folder) { - // New root: original directory + manager name root = filepath.Join(item.Path, item.Name) if err := os.MkdirAll(root, 0755); err != nil { panic(err) @@ -205,46 +198,139 @@ func CreateDirectoryStructureRecursive(item *Folder) { return } for _, subfolder := range item.Subfolders { + subfolder.Path = filepath.Join(root, subfolder.NewPath) CreateDirectoryStructureRecursive(subfolder) } } -// helper functions -func getPath() string { - dir, err := os.Getwd() - if err != nil { - panic(err) +func UpdateStoredPathsFromComposite(comp *Folder) error { + filePath := filepath.Join("storage", comp.Name+".json") + data, err := os.ReadFile(filePath) + if os.IsNotExist(err) { + saveCompositeDetails(comp) + return nil + } else if err != nil { + return err } - // Search upward for "Smart-File-Manager" - for { - if filepath.Base(dir) == "Smart-File-Manager" { - return dir + var oldStructure DirectoryTreeJson + if err := json.Unmarshal(data, &oldStructure); err != nil { + return err + } + + newStructure := DirectoryTreeJson{ + Name: comp.Name, + IsFolder: true, + RootPath: comp.Path, + Children: buildNodesWithPreservedMetadata(comp, &oldStructure), + } + + return saveCompositeDetailsToFile(newStructure) +} + +func buildNodesWithPreservedMetadata(folder *Folder, oldStructure *DirectoryTreeJson) []FileNode { + var nodes []FileNode + + oldPathMap := make(map[string]FileNode) + buildPathMap(oldStructure.Children, oldPathMap) + + for _, file := range folder.Files { + node := FileNode{ + Name: file.Name, + Path: file.Path, + IsFolder: false, + Keywords: file.Keywords, + Tags: file.Tags, + Locked: file.Locked, + } + + if oldNode, exists := findNodeByName(oldPathMap, file.Name, false); exists { + if len(node.Keywords) == 0 { + node.Keywords = oldNode.Keywords + } + if len(node.Tags) == 0 { + node.Tags = oldNode.Tags + } + if !node.Locked { + node.Locked = oldNode.Locked + } } - parent := filepath.Dir(dir) - if parent == dir { - break // hit root + + nodes = append(nodes, node) + } + + for _, sub := range folder.Subfolders { + childNodes := buildNodesWithPreservedMetadata(sub, oldStructure) + + node := FileNode{ + Name: sub.Name, + Path: sub.Path, + IsFolder: true, + Tags: sub.Tags, + Children: childNodes, + Locked: sub.Locked, + } + + if oldNode, exists := findNodeByName(oldPathMap, sub.Name, true); exists { + if len(node.Tags) == 0 { + node.Tags = oldNode.Tags + } + if !node.Locked { + node.Locked = oldNode.Locked + } + } + + nodes = append(nodes, node) + } + + return nodes +} + +func buildPathMap(nodes []FileNode, pathMap map[string]FileNode) { + for _, node := range nodes { + pathMap[node.Path] = node + pathMap[node.Name] = node + + if node.IsFolder { + buildPathMap(node.Children, pathMap) } - dir = parent } - panic("could not find Smart-File-Manager root") +} + +func findNodeByName(pathMap map[string]FileNode, name string, isFolder bool) (FileNode, bool) { + if node, exists := pathMap[name]; exists && node.IsFolder == isFolder { + return node, true + } + return FileNode{}, false +} + +func getPath() string { + dir, err := os.Getwd() + if err != nil { + panic(err) + } + return dir } func cleanManagerPrefix(path, managerName string) string { parts := strings.Split(path, string(os.PathSeparator)) - // Remove repeated managerName prefixes cleaned := []string{} seenManager := false for _, p := range parts { if p == managerName { if seenManager { - continue // skip extra occurrences + continue } seenManager = true } cleaned = append(cleaned, p) } - return filepath.Join(cleaned...) + result := filepath.Join(cleaned...) + // Preserve leading slash for absolute paths + if filepath.IsAbs(path) && !strings.HasPrefix(result, string(os.PathSeparator)) { + result = string(os.PathSeparator) + result + } + return result } diff --git a/golang/filesystem/moveDirectory_test.go b/golang/filesystem/moveDirectory_test.go index 4bf60f1..0203b6f 100644 --- a/golang/filesystem/moveDirectory_test.go +++ b/golang/filesystem/moveDirectory_test.go @@ -1,23 +1,248 @@ package filesystem import ( + "encoding/json" "os" "path/filepath" "reflect" + "sync" "testing" + + pb "github.com/COS301-SE-2025/Smart-File-Manager/golang/client/protos" ) -func TestCreateDirectoryStructure(t *testing.T) { - // Create temporary test directory +// func TestMoveDirectoryHandler_Success(t *testing.T) { +// setupTest(t) +// defer cleanupTest(t) + +// tempDir := t.TempDir() +// sourceDir := filepath.Join(tempDir, "source") +// os.MkdirAll(sourceDir, 0755) + +// testFile := filepath.Join(sourceDir, "test.txt") +// os.WriteFile(testFile, []byte("content"), 0644) + +// testComposite := &Folder{ +// Name: "testManager", +// Path: sourceDir, +// Files: []*File{ +// { +// Name: "test.txt", +// Path: testFile, +// NewPath: "testManager/test.txt", +// }, +// }, +// } + +// originalComposites := Composites +// originalObjectMap := ObjectMap +// Composites = []*Folder{testComposite} +// ObjectMap = make(map[string]map[string]object) +// ObjectMap[sourceDir] = make(map[string]object) +// defer func() { +// Composites = originalComposites +// ObjectMap = originalObjectMap +// }() + +// req := httptest.NewRequest("GET", "/move?name=testManager", nil) +// w := httptest.NewRecorder() + +// moveDirectoryHandler(w, req) + +// if w.Code != http.StatusOK { +// t.Errorf("Expected status 200, got %d", w.Code) +// } + +// response := strings.TrimSpace(w.Body.String()) +// if response != "true" { +// t.Errorf("Expected 'true', got %s", response) +// } + +// // Check composite count +// if len(Composites) != 1 { +// t.Errorf("Expected 1 composite after move, got %d", len(Composites)) +// } + +// // Check new directory exists +// expectedDir := filepath.Join(tempDir, "testManager") +// if _, err := os.Stat(expectedDir); os.IsNotExist(err) { +// t.Errorf("Expected new directory %s to exist after move", expectedDir) +// } + +// // Check old directory is removed +// if _, err := os.Stat(sourceDir); !os.IsNotExist(err) { +// t.Errorf("Expected old directory %s to be removed", sourceDir) +// } + +// // Check composite path is updated +// if Composites[0].Path != expectedDir { +// t.Errorf("Expected composite path %s, got %s", expectedDir, Composites[0].Path) +// } +// } + +// func TestMoveDirectoryHandler_NotFound(t *testing.T) { +// setupTest(t) +// defer cleanupTest(t) + +// originalComposites := Composites +// Composites = []*Folder{} +// defer func() { Composites = originalComposites }() + +// req := httptest.NewRequest("GET", "/move?name=nonexistent", nil) +// w := httptest.NewRecorder() + +// moveDirectoryHandler(w, req) + +// response := strings.TrimSpace(w.Body.String()) +// if response != "false" { +// t.Errorf("Expected 'false', got %s", response) +// } +// } + +// func TestMoveContent(t *testing.T) { +// setupTest(t) +// defer cleanupTest(t) + +// tempDir := t.TempDir() +// sourceDir := filepath.Join(tempDir, "source") +// os.MkdirAll(sourceDir, 0755) + +// testFile := filepath.Join(sourceDir, "test.txt") +// os.WriteFile(testFile, []byte("content"), 0644) + +// item := &Folder{ +// Name: "testManager", +// Path: sourceDir, +// Files: []*File{ +// { +// Name: "test.txt", +// Path: testFile, +// NewPath: "organized/test.txt", +// }, +// }, +// } + +// CreateDirectoryStructure(item) +// moveContent(item) + +// expectedPath := filepath.Join(tempDir, "testManager") +// if item.Path != expectedPath { +// t.Errorf("Expected path %s, got %s", expectedPath, item.Path) +// } + +// if _, err := os.Stat(sourceDir); !os.IsNotExist(err) { +// t.Error("Expected original directory to be removed") +// } +// } + +func TestMoveContentRecursive(t *testing.T) { tempDir := t.TempDir() - testPath := filepath.Join(tempDir, "test_manager_path") + root = tempDir + + sourceDir := filepath.Join(tempDir, "source") + os.MkdirAll(sourceDir, 0755) + + testFile := filepath.Join(sourceDir, "test.txt") + os.WriteFile(testFile, []byte("content"), 0644) + + subSourceDir := filepath.Join(sourceDir, "subdir") + os.MkdirAll(subSourceDir, 0755) + + subFile := filepath.Join(subSourceDir, "sub.txt") + os.WriteFile(subFile, []byte("subcontent"), 0644) + + item := &Folder{ + Name: "parent", + Files: []*File{ + { + Name: "test.txt", + Path: testFile, + NewPath: "moved/test.txt", + }, + }, + Subfolders: []*Folder{ + { + Name: "subdir", + NewPath: "moved/subdir", + Files: []*File{ + { + Name: "sub.txt", + Path: subFile, + NewPath: "moved/subdir/sub.txt", + }, + }, + }, + }, + } + + os.MkdirAll(filepath.Join(tempDir, "moved"), 0755) + os.MkdirAll(filepath.Join(tempDir, "moved", "subdir"), 0755) + + moveContentRecursive(item) + + movedFile := filepath.Join(tempDir, "moved", "test.txt") + if _, err := os.Stat(movedFile); os.IsNotExist(err) { + t.Error("Expected moved file to exist") + } + + movedSubFile := filepath.Join(tempDir, "moved", "subdir", "sub.txt") + if _, err := os.Stat(movedSubFile); os.IsNotExist(err) { + t.Error("Expected moved subfolder file to exist") + } + + expectedSubfolderPath := filepath.Join(tempDir, "moved/subdir") + if item.Subfolders[0].Path != expectedSubfolderPath { + t.Errorf("Expected subfolder path %s, got %s", expectedSubfolderPath, item.Subfolders[0].Path) + } +} + +func TestMoveContentRecursive_NilFolder(t *testing.T) { + moveContentRecursive(nil) +} + +func TestGenerateUniqueFilePath(t *testing.T) { + tempDir := t.TempDir() + + nonExistentPath := filepath.Join(tempDir, "new.txt") + result := generateUniqueFilePath(nonExistentPath) + if result != nonExistentPath { + t.Errorf("Expected %s, got %s", nonExistentPath, result) + } + + existingFile := filepath.Join(tempDir, "existing.txt") + os.WriteFile(existingFile, []byte("content"), 0644) + + uniquePath := generateUniqueFilePath(existingFile) + expected := filepath.Join(tempDir, "existing_(1).txt") + if uniquePath != expected { + t.Errorf("Expected %s, got %s", expected, uniquePath) + } + + os.WriteFile(expected, []byte("content"), 0644) + uniquePath2 := generateUniqueFilePath(existingFile) + expected2 := filepath.Join(tempDir, "existing_(2).txt") + if uniquePath2 != expected2 { + t.Errorf("Expected %s, got %s", expected2, uniquePath2) + } +} + +func TestGenerateUniqueFilePath_NoExtension(t *testing.T) { + tempDir := t.TempDir() + existingFile := filepath.Join(tempDir, "noext") + os.WriteFile(existingFile, []byte("content"), 0644) - // Create the original path directory - if err := os.MkdirAll(testPath, 0755); err != nil { - t.Fatalf("failed to create test path: %v", err) + uniquePath := generateUniqueFilePath(existingFile) + expected := filepath.Join(tempDir, "noext_(1)") + if uniquePath != expected { + t.Errorf("Expected %s, got %s", expected, uniquePath) } +} + +func TestCreateDirectoryStructure(t *testing.T) { + tempDir := t.TempDir() + testPath := filepath.Join(tempDir, "test_manager_path") + os.MkdirAll(testPath, 0755) - // Create a mock folder structure with the test path folder := &Folder{ Name: "manager1", Path: testPath, @@ -40,13 +265,9 @@ func TestCreateDirectoryStructure(t *testing.T) { }, } - // Create directory structure CreateDirectoryStructure(folder) - // The structure should be created at testPath/manager1/ managerRoot := filepath.Join(testPath, "manager1") - - // Walk the generated tree var got []string err := filepath.Walk(managerRoot, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -65,7 +286,6 @@ func TestCreateDirectoryStructure(t *testing.T) { t.Fatalf("Error walking manager directory: %v", err) } - // Expected structure within the manager directory want := []string{ ".", "test_root", @@ -79,278 +299,388 @@ func TestCreateDirectoryStructure(t *testing.T) { } } -/* -COMMENTED OUT CAUSE IT BROKE PIPELINE -func TestMoveContent(t *testing.T) { - // Find the actual project root first to avoid getPath() panic - projectRoot := findProjectRoot(t) +func TestCreateDirectoryStructureRecursive_NilFolder(t *testing.T) { + CreateDirectoryStructureRecursive(nil) +} - // Create temporary directories for test within project - tempDir := filepath.Join(projectRoot, "temp_test_"+t.Name()) - sourceDir := filepath.Join(tempDir, "source") +func TestCreateDirectoryStructureRecursive_EmptySubfolders(t *testing.T) { + tempDir := t.TempDir() + root = tempDir + + folder := &Folder{ + Name: "empty", + NewPath: "empty", + } - // Clean up temp directory at end - defer os.RemoveAll(tempDir) + CreateDirectoryStructureRecursive(folder) - // Create source directory structure - if err := os.MkdirAll(sourceDir, 0755); err != nil { - t.Fatal(err) + expectedPath := filepath.Join(tempDir, "empty") + if _, err := os.Stat(expectedPath); os.IsNotExist(err) { + t.Error("Expected directory to be created") } +} + +func TestUpdateStoredPathsFromComposite_NoExistingFile(t *testing.T) { + tempDir := t.TempDir() + storageDir := filepath.Join(tempDir, "storage") + os.MkdirAll(storageDir, 0755) - // Create dummy files in source directory - srcFilename := "hello.txt" - srcPath := filepath.Join(sourceDir, srcFilename) - content := []byte("👋 world") - if err := os.WriteFile(srcPath, content, 0644); err != nil { - t.Fatal(err) + originalWd, _ := os.Getwd() + os.Chdir(tempDir) + defer os.Chdir(originalWd) + + comp := &Folder{ + Name: "test", + Path: "/test/path", + Files: []*File{ + { + Name: "file1.txt", + Path: "/test/path/file1.txt", + Tags: []string{"tag1"}, + }, + }, } - // Create a nested subfolder with its own file - nestedDir := filepath.Join(sourceDir, "inner") - if err := os.MkdirAll(nestedDir, 0755); err != nil { - t.Fatal(err) + err := UpdateStoredPathsFromComposite(comp) + if err != nil { + t.Errorf("Unexpected error: %v", err) } - nestedFilename := "deep.txt" - nestedSrc := filepath.Join(nestedDir, nestedFilename) - nestedContent := []byte("deep content") - if err := os.WriteFile(nestedSrc, nestedContent, 0644); err != nil { - t.Fatal(err) + + filePath := filepath.Join("storage", "test.json") + if _, err := os.Stat(filePath); os.IsNotExist(err) { + t.Error("Expected storage file to be created") } +} - // Create folder structure to move - item := &Folder{ - Name: "myManager", - Path: sourceDir, - Files: []*File{ +func TestUpdateStoredPathsFromComposite_WithExistingFile(t *testing.T) { + tempDir := t.TempDir() + storageDir := filepath.Join(tempDir, "storage") + os.MkdirAll(storageDir, 0755) + + originalWd, _ := os.Getwd() + os.Chdir(tempDir) + defer os.Chdir(originalWd) + + oldStructure := DirectoryTreeJson{ + Name: "test", + IsFolder: true, + RootPath: "/old/path", + Children: []FileNode{ { - Name: srcFilename, - Path: srcPath, - NewPath: "greeting/hi.txt", + Name: "file1.txt", + Path: "/old/path/file1.txt", + IsFolder: false, + Tags: []string{"oldtag"}, + Keywords: []*pb.Keyword{{Keyword: "oldkeyword"}}, + Locked: true, }, }, - Subfolders: []*Folder{ + } + + filePath := filepath.Join("storage", "test.json") + data, _ := json.MarshalIndent(oldStructure, "", " ") + os.WriteFile(filePath, data, 0644) + + comp := &Folder{ + Name: "test", + Path: "/new/path", + Files: []*File{ { - Name: "inner", - Files: []*File{ - { - Name: nestedFilename, - Path: nestedSrc, - NewPath: "greeting/deep/inner_out.txt", - }, - }, + Name: "file1.txt", + Path: "/new/path/file1.txt", }, }, } - // Stay in project root so getPath() can find "Smart-File-Manager" - origWd, err := os.Getwd() + err := UpdateStoredPathsFromComposite(comp) if err != nil { - t.Fatal(err) - } - defer os.Chdir(origWd) - - // First create directory structure (as done in moveDirectoryHandler) - CreateDirectoryStructure(item) - - // Debug: Check what CreateDirectoryStructure created - t.Logf("After CreateDirectoryStructure, checking contents:") - if entries, err := os.ReadDir(tempDir); err == nil { - for _, entry := range entries { - t.Logf(" tempDir contains: %s", entry.Name()) - if entry.IsDir() { - subPath := filepath.Join(tempDir, entry.Name()) - if subEntries, err := os.ReadDir(subPath); err == nil { - for _, subEntry := range subEntries { - t.Logf(" %s contains: %s", entry.Name(), subEntry.Name()) - } - } - } - } + t.Errorf("Unexpected error: %v", err) } - // Then move content (this will update root to parentDir) - moveContent(item) - - // Debug: Check what moveContent created - t.Logf("After moveContent, checking contents:") - if entries, err := os.ReadDir(tempDir); err == nil { - for _, entry := range entries { - t.Logf(" tempDir contains: %s", entry.Name()) - if entry.IsDir() { - subPath := filepath.Join(tempDir, entry.Name()) - if subEntries, err := os.ReadDir(subPath); err == nil { - for _, subEntry := range subEntries { - t.Logf(" %s contains: %s", entry.Name(), subEntry.Name()) - if subEntry.IsDir() { - deepPath := filepath.Join(subPath, subEntry.Name()) - if deepEntries, err := os.ReadDir(deepPath); err == nil { - for _, deepEntry := range deepEntries { - t.Logf(" %s/%s contains: %s", entry.Name(), subEntry.Name(), deepEntry.Name()) - } - } - } - } - } - } - } + newData, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read updated file: %v", err) } - // Log what the item.Path was updated to - t.Logf("item.Path after moveContent: %s", item.Path) + var newStructure DirectoryTreeJson + json.Unmarshal(newData, &newStructure) - // After moveContent: - // Let's see where files actually ended up by checking different possible locations - possibleLocations := []string{ - filepath.Join(tempDir, item.Name, item.Files[0].NewPath), - filepath.Join(tempDir, item.Files[0].NewPath), - filepath.Join(item.Path, item.Files[0].NewPath), + if newStructure.RootPath != "/new/path" { + t.Errorf("Expected root path /new/path, got %s", newStructure.RootPath) } - var actualLocation string - for _, loc := range possibleLocations { - if _, err := os.Stat(loc); err == nil { - actualLocation = loc - t.Logf("Found file at: %s", loc) - break - } else { - t.Logf("File not found at: %s", loc) - } + if len(newStructure.Children) != 1 { + t.Fatalf("Expected 1 child, got %d", len(newStructure.Children)) } - if actualLocation == "" { - t.Fatal("Could not find the moved file at any expected location") + child := newStructure.Children[0] + if child.Path != "/new/path/file1.txt" { + t.Errorf("Expected path /new/path/file1.txt, got %s", child.Path) } - // Read content from actual location to verify it was moved correctly - data, err := os.ReadFile(actualLocation) - if err != nil { - t.Fatalf("failed to read file at %s: %v", actualLocation, err) - } - if string(data) != string(content) { - t.Errorf("file content = %q; want %q", data, content) + if len(child.Tags) == 0 || child.Tags[0] != "oldtag" { + t.Errorf("Expected preserved tag 'oldtag', got %v", child.Tags) } - // Assert original source directory no longer exists - if _, err := os.Stat(sourceDir); !os.IsNotExist(err) { - t.Errorf("expected source directory %s to be gone, got err=%v", sourceDir, err) + if !child.Locked { + t.Error("Expected locked status to be preserved") } } -func TestMoveDirectoryHandler(t *testing.T) { - // Find the project root first - projectRoot := findProjectRoot(t) +func TestBuildNodesWithPreservedMetadata(t *testing.T) { + oldStructure := &DirectoryTreeJson{ + Children: []FileNode{ + { + Name: "file1.txt", + Path: "/old/file1.txt", + IsFolder: false, + Tags: []string{"tag1"}, + Keywords: []*pb.Keyword{{Keyword: "keyword1"}}, + Locked: true, + }, + { + Name: "folder1", + Path: "/old/folder1", + IsFolder: true, + Tags: []string{"foldertag"}, + Locked: true, + Children: []FileNode{ + { + Name: "nested.txt", + Path: "/old/folder1/nested.txt", + IsFolder: false, + Tags: []string{"nested"}, + }, + }, + }, + }, + } - // Create test directory within project - tempDir := filepath.Join(projectRoot, "temp_handler_test_"+t.Name()) - sourceDir := filepath.Join(tempDir, "test_source") + folder := &Folder{ + Files: []*File{ + { + Name: "file1.txt", + Path: "/new/file1.txt", + }, + { + Name: "newfile.txt", + Path: "/new/newfile.txt", + Tags: []string{"newtag"}, + }, + }, + Subfolders: []*Folder{ + { + Name: "folder1", + Path: "/new/folder1", + Files: []*File{ + { + Name: "nested.txt", + Path: "/new/folder1/nested.txt", + }, + }, + }, + }, + } - // Clean up at end - defer os.RemoveAll(tempDir) + nodes := buildNodesWithPreservedMetadata(folder, oldStructure) - // Create source directory with some content - if err := os.MkdirAll(sourceDir, 0755); err != nil { - t.Fatal(err) + if len(nodes) != 3 { + t.Fatalf("Expected 3 nodes, got %d", len(nodes)) } - testFile := filepath.Join(sourceDir, "test.txt") - if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil { - t.Fatal(err) + file1Found := false + folderFound := false + for _, node := range nodes { + if node.Name == "file1.txt" { + file1Found = true + if len(node.Tags) == 0 || node.Tags[0] != "tag1" { + t.Error("Expected preserved tag for file1.txt") + } + if !node.Locked { + t.Error("Expected preserved lock status for file1.txt") + } + if len(node.Keywords) == 0 || node.Keywords[0].Keyword != "keyword1" { + t.Error("Expected preserved keyword for file1.txt") + } + } + if node.Name == "folder1" && node.IsFolder { + folderFound = true + if len(node.Tags) == 0 || node.Tags[0] != "foldertag" { + t.Error("Expected preserved tag for folder1") + } + if !node.Locked { + t.Error("Expected preserved lock status for folder1") + } + } } - // Create a test composite - testComposite := &Folder{ - Name: "testManager", - Path: sourceDir, - Files: []*File{ - { - Name: "test.txt", - Path: testFile, - NewPath: "organized/test.txt", + if !file1Found { + t.Error("file1.txt node not found") + } + if !folderFound { + t.Error("folder1 node not found") + } +} + +func TestBuildPathMap(t *testing.T) { + nodes := []FileNode{ + { + Name: "file1.txt", + Path: "/path/file1.txt", + IsFolder: false, + }, + { + Name: "folder1", + Path: "/path/folder1", + IsFolder: true, + Children: []FileNode{ + { + Name: "nested.txt", + Path: "/path/folder1/nested.txt", + IsFolder: false, + }, }, }, } - // Add to global Composites for testing - originalComposites := Composites - Composites = []*Folder{testComposite} - defer func() { Composites = originalComposites }() + pathMap := make(map[string]FileNode) + buildPathMap(nodes, pathMap) - // Stay in project root so getPath() works - origWd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - defer os.Chdir(origWd) - - // Test the move operation directly (simulating what the handler does) - CreateDirectoryStructure(testComposite) - moveContent(testComposite) - - // Debug: Check actual directory structure after operations - t.Logf("After operations, tempDir contents:") - if entries, err := os.ReadDir(tempDir); err == nil { - for _, entry := range entries { - t.Logf(" - %s", entry.Name()) - if entry.IsDir() { - subPath := filepath.Join(tempDir, entry.Name()) - if subEntries, err := os.ReadDir(subPath); err == nil { - for _, subEntry := range subEntries { - t.Logf(" - %s", subEntry.Name()) - if subEntry.IsDir() { - deepPath := filepath.Join(subPath, subEntry.Name()) - if deepEntries, err := os.ReadDir(deepPath); err == nil { - for _, deepEntry := range deepEntries { - t.Logf(" - %s", deepEntry.Name()) - } - } - } - } - } - } + expectedEntries := []string{ + "/path/file1.txt", + "file1.txt", + "/path/folder1", + "folder1", + "/path/folder1/nested.txt", + "nested.txt", + } + + for _, key := range expectedEntries { + if _, exists := pathMap[key]; !exists { + t.Errorf("Expected key %s to exist in pathMap", key) } } - // Check multiple possible locations for the file - possibleLocations := []string{ - filepath.Join(tempDir, "testManager", "organized", "test.txt"), - filepath.Join(tempDir, "organized", "test.txt"), - filepath.Join(testComposite.Path, "organized", "test.txt"), + if len(pathMap) != len(expectedEntries) { + t.Errorf("Expected %d entries in pathMap, got %d", len(expectedEntries), len(pathMap)) } +} - var foundLocation string - for _, loc := range possibleLocations { - if _, err := os.Stat(loc); err == nil { - foundLocation = loc - t.Logf("Found file at: %s", loc) - break - } else { - t.Logf("File not at: %s", loc) - } +func TestFindNodeByName(t *testing.T) { + pathMap := map[string]FileNode{ + "file1.txt": { + Name: "file1.txt", + IsFolder: false, + Tags: []string{"tag1"}, + }, + "folder1": { + Name: "folder1", + IsFolder: true, + Tags: []string{"foldertag"}, + }, } - if foundLocation == "" { - t.Fatal("Could not find the moved file at any expected location") + node, found := findNodeByName(pathMap, "file1.txt", false) + if !found { + t.Error("Expected to find file1.txt") + } + if node.Name != "file1.txt" { + t.Errorf("Expected name file1.txt, got %s", node.Name) } - // Verify file content - data, err := os.ReadFile(foundLocation) - if err != nil { - t.Fatalf("failed to read file at %s: %v", foundLocation, err) + _, found = findNodeByName(pathMap, "file1.txt", true) + if found { + t.Error("Should not find file1.txt as folder") } - if string(data) != "test content" { - t.Errorf("file content = %q; want %q", data, "test content") + + folderNode, found := findNodeByName(pathMap, "folder1", true) + if !found { + t.Error("Expected to find folder1") + } + if folderNode.Name != "folder1" { + t.Errorf("Expected name folder1, got %s", folderNode.Name) + } + + _, found = findNodeByName(pathMap, "nonexistent", false) + if found { + t.Error("Should not find nonexistent file") + } +} + +// func TestGetPath(t *testing.T) { +// setupTest(t) +// defer cleanupTest(t) + +// path := getPath() +// if !strings.Contains(path, "Smart-File-Manager") { +// t.Errorf("Expected path to contain Smart-File-Manager, got %s", path) +// } +// } + +func TestCleanManagerPrefix(t *testing.T) { + tests := []struct { + path string + managerName string + expected string + }{ + { + path: "/home/user/manager1/manager1/file.txt", + managerName: "manager1", + expected: "/home/user/manager1/file.txt", + }, + { + path: "/home/manager1/folder/manager1/manager1/file.txt", + managerName: "manager1", + expected: "/home/manager1/folder/file.txt", + }, + { + path: "/simple/path/file.txt", + managerName: "manager1", + expected: "/simple/path/file.txt", + }, } - // Verify original source directory was removed - if _, err := os.Stat(sourceDir); !os.IsNotExist(err) { - t.Errorf("expected original source directory %s to be removed", sourceDir) + for _, test := range tests { + result := cleanManagerPrefix(test.path, test.managerName) + if result != test.expected { + t.Errorf("cleanManagerPrefix(%s, %s) = %s; want %s", + test.path, test.managerName, result, test.expected) + } } +} - t.Logf("Test completed successfully - file found at: %s", foundLocation) +// Test helpers and setup functions +var ( + originalMu sync.Mutex + originalComposites []*Folder + originalObjectMap map[string]map[string]object + originalManagerPath string +) + +func setupTest(t *testing.T) { + projectRoot := findProjectRoot(t) + os.Chdir(projectRoot) + + originalMu = mu + originalComposites = Composites + originalObjectMap = ObjectMap + originalManagerPath = managersFilePath + + mu = sync.Mutex{} + Composites = []*Folder{} + ObjectMap = make(map[string]map[string]object) + managersFilePath = "test_managers.json" +} + +func cleanupTest(t *testing.T) { + mu = originalMu + Composites = originalComposites + ObjectMap = originalObjectMap + managersFilePath = originalManagerPath + + os.Remove("test_managers.json") } -*/ -// Helper functions func findProjectRoot(t *testing.T) string { dir, err := os.Getwd() if err != nil { @@ -369,40 +699,3 @@ func findProjectRoot(t *testing.T) string { dir = parent } } - -func clearDirectory(path string) error { - entries, err := os.ReadDir(path) - if err != nil { - return err - } - for _, entry := range entries { - err := os.RemoveAll(filepath.Join(path, entry.Name())) - if err != nil { - return err - } - } - return nil -} - -func mockFolderStructureNamed(managerName string) *Folder { - return &Folder{ - Name: managerName, - NewPath: "test_root", - Subfolders: []*Folder{ - { - Name: "sub1", - NewPath: "test_root/sub1", - Subfolders: []*Folder{ - { - Name: "sub1_1", - NewPath: "test_root/sub1/sub1_1", - }, - }, - }, - { - Name: "sub2", - NewPath: "test_root/sub2", - }, - }, - } -} diff --git a/golang/filesystem/saveComposite.go b/golang/filesystem/saveComposite.go index 6836af1..75bc0bf 100644 --- a/golang/filesystem/saveComposite.go +++ b/golang/filesystem/saveComposite.go @@ -7,14 +7,15 @@ import ( "path/filepath" "sort" "strings" - - pb "github.com/COS301-SE-2025/Smart-File-Manager/golang/client/protos" ) // uses load tree struct directoryTreeJson func saveCompositeDetails(c *Folder) { + if c == nil { + return + } children := compositeToJsonStorageFormat(c) root := DirectoryTreeJson{ @@ -28,6 +29,9 @@ func saveCompositeDetails(c *Folder) { } func compositeToJsonStorageFormat(folder *Folder) []FileNode { + if folder == nil { + return nil + } var nodes []FileNode for _, file := range folder.Files { @@ -140,7 +144,7 @@ func populateKeywordsFromStoredJsonFile(comp *Folder) { func mergeDirectoryTreeToComposite(comp *Folder, directory *DirectoryTreeJson) { for _, node := range directory.Children { if !node.IsFolder { - fmt.Println(node.Name) + // fmt.Println(node.Name) path := node.Path compositeFile := comp.GetFile(path) @@ -158,7 +162,7 @@ func mergeDirectoryTreeToComposite(comp *Folder, directory *DirectoryTreeJson) { helperMergeDirectoryTreeToComposite(comp, &node) } } - fmt.Println("======END OFmergeDirectoryTreeToComposite========") + // fmt.Println("======END OFmergeDirectoryTreeToComposite========") // PrettyPrintFolder(comp, "") @@ -195,16 +199,6 @@ func deleteCompositeDetailsFile(compName string) error { return nil } -func printDirTreeKwTagsLock(root *DirectoryTreeJson, leftPad int) { - if root == nil { - fmt.Println("(nil tree)") - return - } - prefix := strings.Repeat(" ", leftPad) - fmt.Printf("%s[ROOT] %s\n", prefix, safeName(root.Name)) - printFileNodeChildren(root.Children, prefix) -} - func printFileNodeChildren(nodes []FileNode, prefix string) { if len(nodes) == 0 { fmt.Printf("%s└── (empty)\n", prefix) @@ -275,12 +269,6 @@ func nodeLabel(n FileNode) string { } } - // if kws := keywordNames(n.Keywords); kws != "" { - // b.WriteString(" [kw: ") - // b.WriteString(kws) - // b.WriteString("]") - // } - if n.Locked { b.WriteString(" [locked]") } @@ -306,20 +294,3 @@ func joinNonEmpty(ss []string) string { } return strings.Join(out, ", ") } - -func keywordNames(kws []*pb.Keyword) string { - if len(kws) == 0 { - return "" - } - out := make([]string, 0, len(kws)) - for _, k := range kws { - if k == nil { - continue - } - w := strings.TrimSpace(k.Keyword) - if w != "" { - out = append(out, w) - } - } - return strings.Join(out, ", ") -} diff --git a/golang/filesystem/server.go b/golang/filesystem/server.go index 148ea9b..bbadac9 100644 --- a/golang/filesystem/server.go +++ b/golang/filesystem/server.go @@ -2,10 +2,14 @@ package filesystem import ( // "encoding/json" + "bufio" "encoding/json" "fmt" + "net" "net/http" "os" + "path/filepath" + "strings" "sync" ) @@ -15,11 +19,136 @@ var ( mu sync.Mutex ) +// savePortToEnv updates or creates the GO_PORT entry in server.env +func savePortToEnv(port int) error { + // Get the current working directory + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %v", err) + } + + // Navigate to project root + projectRoot := filepath.Dir(cwd) + envFilePath := filepath.Join(projectRoot, "server.env") + + // Try current directory first if the above doesn't work + if _, err := os.Stat(envFilePath); os.IsNotExist(err) { + envFilePath = filepath.Join(cwd, "server.env") + } + + // If still not found, try going up two levels + if _, err := os.Stat(envFilePath); os.IsNotExist(err) { + projectRoot = filepath.Dir(filepath.Dir(cwd)) + envFilePath = filepath.Join(projectRoot, "server.env") + } + + var lines []string + goPortFound := false + + // Read existing file if it exists + if file, err := os.Open(envFilePath); err == nil { + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "GO_PORT=") { + lines = append(lines, fmt.Sprintf("GO_PORT=%d", port)) + goPortFound = true + } else { + lines = append(lines, line) + } + } + } + + // Add GO_PORT if not found + if !goPortFound { + lines = append(lines, fmt.Sprintf("GO_PORT=%d", port)) + } + + // Write back to file + content := strings.Join(lines, "\n") + err = os.WriteFile(envFilePath, []byte(content), 0644) + if err != nil { + return fmt.Errorf("failed to write to server.env: %v", err) + } + + return nil +} + +// findAvailablePort finds an available port starting from a base port +func findAvailablePort(basePort int) (int, error) { + for port := basePort; port < basePort+100; port++ { + addr := fmt.Sprintf(":%d", port) + listener, err := net.Listen("tcp", addr) + if err == nil { + listener.Close() + return port, nil + } + } + return 0, fmt.Errorf("no available port found in range %d-%d", basePort, basePort+99) +} + +func isPathContained(parentPath, childPath string) bool { + parentPath = ConvertToWSLPath(filepath.Clean(parentPath)) + childPath = ConvertToWSLPath(filepath.Clean(childPath)) + + // Convert to absolute paths for accurate comparison + parentAbs, err := filepath.Abs(parentPath) + if err != nil { + return false + } + + childAbs, err := filepath.Abs(childPath) + if err != nil { + return false + } + + // Ensure paths end with separator for accurate prefix checking + if !strings.HasSuffix(parentAbs, string(filepath.Separator)) { + parentAbs += string(filepath.Separator) + } + if !strings.HasSuffix(childAbs, string(filepath.Separator)) { + childAbs += string(filepath.Separator) + } + + // Check if child path starts with parent path + return strings.HasPrefix(childAbs, parentAbs) +} + +func checkDirectoryConflicts(newPath string) (bool, string, error) { + // fmt.Println(" Manager directory conflicts started"); + newPath = ConvertToWSLPath(filepath.Clean(newPath)) + + for _, comp := range Composites { + existingPathAbs := comp.Path + + fmt.Println("New Path: " + newPath) + fmt.Println("Old Path: " + existingPathAbs) + // Exact match + if existingPathAbs == newPath { + return true, fmt.Sprintf("Directory is already managed by '%s'", comp.Name), nil + } + + // New path is inside existing manager + if strings.HasPrefix(newPath+string(os.PathSeparator), existingPathAbs+string(os.PathSeparator)) { + return true, fmt.Sprintf("Directory is already contained within existing manager '%s' at path '%s'", comp.Name, comp.Path), nil + } + + // Existing manager is inside new path + if strings.HasPrefix(existingPathAbs+string(os.PathSeparator), newPath+string(os.PathSeparator)) { + return true, fmt.Sprintf("New directory would contain existing manager '%s' at path '%s'", comp.Name, comp.Path), nil + } + } + + return false, "", nil +} + func addCompositeHandler(w http.ResponseWriter, r *http.Request) { - // fmt.Println("addDirectory called") + fmt.Println(" Manager creation started") managerName := r.URL.Query().Get("name") filePath := r.URL.Query().Get("path") + // Check if manager name already exists for _, comp := range Composites { if comp.Name == managerName { http.Error(w, "A smart file manager with that name already exists", http.StatusBadRequest) @@ -28,14 +157,29 @@ func addCompositeHandler(w http.ResponseWriter, r *http.Request) { } mu.Lock() - // Composites = append(Composites, composite) - //appendng happens in this: - err := AddManager(managerName, filePath) + defer mu.Unlock() + + // Check for directory conflicts + fmt.Println(" Manager directory conflicts called") + + hasConflict, _, err := checkDirectoryConflicts(filePath) + if err != nil { + http.Error(w, fmt.Sprintf("Error checking directory conflicts: %v", err), http.StatusInternalServerError) + return + } + fmt.Println(" Manager directory conflicts ended") + + if hasConflict { + w.Write([]byte("false")) + return + } + + // Proceed with adding the manager + err = AddManager(managerName, filePath) if err != nil { w.Write([]byte("false")) return } - mu.Unlock() w.Write([]byte("true")) } @@ -45,6 +189,10 @@ func addTagHandler(w http.ResponseWriter, r *http.Request) { tag := r.URL.Query().Get("tag") convertedPath := ConvertToWSLPath(filePath) + if filePath == "" || tag == "" { + w.Write([]byte("false")) + return + } mu.Lock() defer mu.Unlock() @@ -168,7 +316,7 @@ func deleteFileHandler(w http.ResponseWriter, r *http.Request) { RootPath: c.Path, Children: children, } - w.WriteHeader(http.StatusOK) + // w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") // Encode the response as JSON if err := json.NewEncoder(w).Encode(root); err != nil { @@ -207,7 +355,7 @@ func deleteFolderHandler(w http.ResponseWriter, r *http.Request) { RootPath: c.Path, Children: children, } - w.WriteHeader(http.StatusOK) + // w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") // Encode the response as JSON if err := json.NewEncoder(w).Encode(root); err != nil { @@ -268,7 +416,7 @@ func deleteManagerHandler(w http.ResponseWriter, r *http.Request) { } //remove storage file deleteCompositeDetailsFile(c.Name) - fmt.Println("Deleted manager") + // fmt.Println("Deleted manager") w.Write([]byte("true")) return } @@ -276,52 +424,79 @@ func deleteManagerHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Manager not found")) } -func HandleRequests() { +func secretMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + secret := r.Header.Get("apiSecret") + + apiSecret, found := os.LookupEnv("SFM_API_SECRET") + if !found { + fmt.Println("api secret not found") + http.Error(w, "Server secret not configured", http.StatusInternalServerError) + return + } - // path, _ := os.Getwd() - // fmt.Println("THE PATH: " + path) - // path = filepath.Dir(path) - // path = filepath.Join(path, "python/testing") - // fmt.Println("THE PATH: " + path) + if secret != apiSecret { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) +} - http.HandleFunc("/addDirectory", addCompositeHandler) +func HandleRequests() { + // Find an available port starting from 51000 + port, err := findAvailablePort(51000) + if err != nil { + fmt.Printf("Error finding available port: %v\n", err) + port = 51000 // Fallback to original port + } else { + // Save the found port to server.env + if err := savePortToEnv(port); err != nil { + fmt.Printf("Warning: Could not save port to server.env: %v\n", err) + } else { + fmt.Printf("Saved port %d to server.env\n", port) + } + } - http.HandleFunc("/addTag", addTagHandler) - http.HandleFunc("/removeTag", removeTagHandler) + http.Handle("/addDirectory", secretMiddleware(http.HandlerFunc(addCompositeHandler))) - http.HandleFunc("/loadTreeData", loadTreeDataHandlerGoOnly) + http.Handle("/addTag", secretMiddleware(http.HandlerFunc(addTagHandler))) + http.Handle("/removeTag", secretMiddleware(http.HandlerFunc(removeTagHandler))) - http.HandleFunc("/sortTree", sortTreeHandler) - http.HandleFunc("/startUp", startUpHandler) + http.Handle("/loadTreeData", secretMiddleware(http.HandlerFunc(loadTreeDataHandlerGoOnly))) - http.HandleFunc("/lock", lockHandler) - http.HandleFunc("/unlock", unlockHandler) + http.Handle("/sortTree", secretMiddleware(http.HandlerFunc(sortTreeHandler))) + http.Handle("/startUp", secretMiddleware(http.HandlerFunc(startUpHandler))) - http.HandleFunc("/search", SearchHandler) + http.Handle("/lock", secretMiddleware(http.HandlerFunc(lockHandler))) + http.Handle("/unlock", secretMiddleware(http.HandlerFunc(unlockHandler))) - http.HandleFunc("/keywordSearch", KeywordSearchHadler) - http.HandleFunc("/isKeywordSearchReady", IsKeywordSearchReadyHander) + http.Handle("/search", secretMiddleware(http.HandlerFunc(SearchHandler))) - http.HandleFunc("/moveDirectory", moveDirectoryHandler) + http.Handle("/keywordSearch", secretMiddleware(http.HandlerFunc(KeywordSearchHadler))) + http.Handle("/isKeywordSearchReady", secretMiddleware(http.HandlerFunc(IsKeywordSearchReadyHander))) - http.HandleFunc("/findDuplicateFiles", findDuplicateFilesHandler) + http.Handle("/moveDirectory", secretMiddleware(http.HandlerFunc(moveDirectoryHandler))) - http.HandleFunc("/bulkAddTag", BulkAddTagHandler) - http.HandleFunc("/bulkRemoveTag", BulkRemoveTagHandler) + http.Handle("/findDuplicateFiles", secretMiddleware(http.HandlerFunc(findDuplicateFilesHandler))) - http.HandleFunc("/deleteFile", deleteFileHandler) - http.HandleFunc("/deleteFolder", deleteFolderHandler) - http.HandleFunc("/bulkDeleteFolders", BulkDeleteFolderHandler) - http.HandleFunc("/bulkDeleteFiles", BulkDeleteFileHandler) - http.HandleFunc("/deleteManager", deleteManagerHandler) + http.Handle("/bulkAddTag", secretMiddleware(http.HandlerFunc(BulkAddTagHandler))) + http.Handle("/bulkRemoveTag", secretMiddleware(http.HandlerFunc(BulkRemoveTagHandler))) - http.HandleFunc("/returnType", ReturnTypeHandler) - http.HandleFunc("/returnStats", StatHandler) + http.Handle("/deleteFile", secretMiddleware(http.HandlerFunc(deleteFileHandler))) + http.Handle("/deleteFolder", secretMiddleware(http.HandlerFunc(deleteFolderHandler))) + http.Handle("/bulkDeleteFolders", secretMiddleware(http.HandlerFunc(BulkDeleteFolderHandler))) + http.Handle("/bulkDeleteFiles", secretMiddleware(http.HandlerFunc(BulkDeleteFileHandler))) + http.Handle("/deleteManager", secretMiddleware(http.HandlerFunc(deleteManagerHandler))) - fmt.Println("Server started on port 51000") + http.Handle("/returnType", secretMiddleware(http.HandlerFunc(ReturnTypeHandler))) - // http.ListenAndServe(":51000", nil) - http.ListenAndServe("0.0.0.0:51000", nil) + http.Handle("/returnStats", secretMiddleware(http.HandlerFunc(StatHandler))) + + addr := fmt.Sprintf("0.0.0.0:%d", port) + if err := http.ListenAndServe(addr, nil); err != nil { + fmt.Printf("Server failed to start: %v\n", err) + } } diff --git a/golang/filesystem/server_test.go b/golang/filesystem/server_test.go index 6f36568..473d9cf 100644 --- a/golang/filesystem/server_test.go +++ b/golang/filesystem/server_test.go @@ -1,918 +1,1097 @@ +// Additional tests to improve server coverage package filesystem import ( + "bytes" "encoding/json" + "fmt" + "io" "net/http" "net/http/httptest" "os" "path/filepath" + "sort" "strings" "testing" + + pb "github.com/COS301-SE-2025/Smart-File-Manager/golang/client/protos" ) -func TestAPI_AddAndRemoveDirectory(t *testing.T) { - req := httptest.NewRequest("GET", "/addDirectory?name=testdir&path=../../testRootFolder", nil) - w := httptest.NewRecorder() - addCompositeHandler(w, req) - if w.Body.String() != "true" { - t.Fatalf("expected true, got %s", w.Body.String()) +// Test addCompositeHandler with directory conflict checking +func TestAPI_AddCompositeHandler_ConflictChecking(t *testing.T) { + // Setup: Create a temporary directory structure + tmp := t.TempDir() + parentDir := filepath.Join(tmp, "parent") + childDir := filepath.Join(parentDir, "child") + if err := os.MkdirAll(childDir, 0755); err != nil { + t.Fatal(err) } - // req = httptest.NewRequest("GET", "/removeDirectory?path=../../testRootFolder", nil) - // w = httptest.NewRecorder() - // removeCompositeHandler(w, req) - // if w.Body.String() != "true" { - // t.Fatalf("expected true, got %s", w.Body.String()) - // } -} + // Backup and restore original Composites + orig := Composites + defer func() { Composites = orig }() + + // Simulate that a manager already exists for parentDir + Composites = []*Folder{ + {Name: "parentManager", Path: parentDir}, + } -func TestAPI_AddAndRemoveTag(t *testing.T) { - // Add folder to operate on - req := httptest.NewRequest("GET", "/addDirectory?name=testdir&path=../../testRootFolder", nil) + // Test 1: Try to add a child directory (should be detected as conflict) + req := httptest.NewRequest("GET", "/addDirectory?name=childManager&path="+childDir, nil) w := httptest.NewRecorder() addCompositeHandler(w, req) - // Tag a file inside the test folder - req = httptest.NewRequest("GET", "/addTag?path=../../testRootFolder/subdir/rb24.rs&tag=important", nil) - w = httptest.NewRecorder() - addTagHandler(w, req) - if w.Body.String() != "true" { - t.Fatalf("addTagHandler: expected true, got %s", w.Body.String()) + // Current implementation writes "false" on conflict (and returns 200 OK) + if w.Code != http.StatusOK { + t.Errorf("expected status 200 for conflict, got %d", w.Code) + } + body := strings.TrimSpace(w.Body.String()) + if body != "false" { + t.Errorf("expected body 'false' on conflict, got %q", body) } - // Remove the tag - req = httptest.NewRequest("GET", "/removeTag?path=../../testRootFolder/subdir/rb24.rs&tag=important", nil) - w = httptest.NewRecorder() - removeTagHandler(w, req) - if w.Body.String() != "true" { - t.Fatalf("removeTagHandler: expected true, got %s", w.Body.String()) - } -} - -// func TestAPI_LockUnlock(t *testing.T) { -// // Add composite -// req := httptest.NewRequest("GET", "/addDirectory?name=testlock&path=../../testRootFolder", nil) -// w := httptest.NewRecorder() -// addCompositeHandler(w, req) - -// // Lock -// // req = httptest.NewRequest("GET", "/lock?name=testlock&path=../../testRootFolder", nil) -// // w = httptest.NewRecorder() -// // lockHandler(w, req) -// // if w.Body.String() != "true" { -// // t.Fatalf("lockHandler: expected true, got %s", w.Body.String()) -// // } - -// // Unlock -// req = httptest.NewRequest("GET", "/unlock?name=testlock&path=../../testRootFolder", nil) -// w = httptest.NewRecorder() -// unlockHandler(w, req) -// if w.Body.String() != "true" { -// t.Fatalf("unlockHandler: expected true, got %s", w.Body.String()) -// } - -// // Cleanup -// req = httptest.NewRequest("GET", "/removeDirectory?path=../../testRootFolder", nil) -// w = httptest.NewRecorder() -// removeCompositeHandler(w, req) -// } - -func TestAPI_EndpointsInvalidCases(t *testing.T) { - // Try removing non-existent composite - req := httptest.NewRequest("GET", "/removeDirectory?path=./invalid", nil) - w := httptest.NewRecorder() - // removeCompositeHandler(w, req) - // if w.Body.String() != "true" { - // t.Errorf("expected true even for non-existent remove, got %s", w.Body.String()) - // } - - // Add tag to nonexistent file - req = httptest.NewRequest("GET", "/addTag?path=./invalid/file.txt&tag=none", nil) + // Test 2: Try to add duplicate manager name (should return 400) + req = httptest.NewRequest("GET", "/addDirectory?name=parentManager&path="+tmp, nil) w = httptest.NewRecorder() - addTagHandler(w, req) - if w.Body.String() != "false" { - t.Errorf("expected false for non-existent file tag, got %s", w.Body.String()) - } -} - -// func TestMoveDirectoryHandler(t *testing.T) { -// // Find the project root first -// projectRoot := findProjectRoot(t) - -// // Create test directory within project -// tempDir := filepath.Join(projectRoot, "temp_handler_test_"+t.Name()) -// sourceDir := filepath.Join(tempDir, "test_source") - -// // Clean up at end -// defer os.RemoveAll(tempDir) - -// // Create source directory with some content -// if err := os.MkdirAll(sourceDir, 0755); err != nil { -// t.Fatal(err) -// } - -// testFile := filepath.Join(sourceDir, "test.txt") -// if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil { -// t.Fatal(err) -// } - -// // Create a test composite -// testComposite := &Folder{ -// Name: "testManager", -// Path: sourceDir, -// Files: []*File{ -// { -// Name: "test.txt", -// Path: testFile, -// NewPath: "organized/test.txt", -// }, -// }, -// } - -// // Add to global Composites for testing -// originalComposites := Composites -// Composites = []*Folder{testComposite} -// defer func() { Composites = originalComposites }() - -// // Stay in project root so getPath() works -// origWd, err := os.Getwd() -// if err != nil { -// t.Fatal(err) -// } -// defer os.Chdir(origWd) - -// // Test the move operation directly (simulating what the handler does) -// CreateDirectoryStructure(testComposite) -// moveContent(testComposite) - -// // Verify the operation succeeded -// parentDir := filepath.Dir(sourceDir) -// newLocation := filepath.Join(parentDir, testComposite.Name, "organized", "test.txt") - -// if _, err := os.Stat(newLocation); os.IsNotExist(err) { -// t.Errorf("expected file to exist at %s", newLocation) -// } - -// if _, err := os.Stat(sourceDir); !os.IsNotExist(err) { -// t.Errorf("expected original source directory %s to be removed", sourceDir) -// } -// } - -// Test the find-duplicates endpoint -func TestAPI_FindDuplicateFilesHandler(t *testing.T) { - tmp := t.TempDir() - projectRoot := filepath.Join(tmp, "Smart-File-Manager") - if err := os.MkdirAll(projectRoot, 0755); err != nil { - t.Fatal(err) - } + addCompositeHandler(w, req) - // Create a data folder with two identical files - dataDir := filepath.Join(projectRoot, "data") - if err := os.MkdirAll(dataDir, 0755); err != nil { - t.Fatal(err) + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for duplicate name, got %d", w.Code) } - fileA := filepath.Join(dataDir, "a.txt") - fileB := filepath.Join(dataDir, "b.txt") - content := []byte("duplicate test") - if err := os.WriteFile(fileA, content, 0644); err != nil { - t.Fatal(err) + respBody := w.Body.String() + if !strings.Contains(respBody, "name already exists") && !strings.Contains(respBody, "already exists") { + t.Errorf("expected duplicate name message, got: %s", respBody) } - if err := os.WriteFile(fileB, content, 0644); err != nil { - t.Fatal(err) +} + +// Test the path containment helper functions +func TestAPI_PathContainmentHelpers(t *testing.T) { + tests := []struct { + parent string + child string + expected bool + desc string + }{ + {"/home/user", "/home/user/documents", true, "child should be contained in parent"}, + {"/home/user/documents", "/home/user", false, "parent should not be contained in child"}, + {"/home/user", "/home/user", true, "identical paths should be contained"}, + {"/home/user", "/home/other", false, "unrelated paths should not be contained"}, + {"/home/user/docs", "/home/user/documents", false, "similar but different paths should not be contained"}, + } + + for _, tt := range tests { + result := isPathContained(tt.parent, tt.child) + if result != tt.expected { + t.Errorf("%s: isPathContained(%q, %q) = %v, expected %v", + tt.desc, tt.parent, tt.child, result, tt.expected) + } } +} - // Register composite - Composites = []*Folder{{ - Name: "dupTest", - Path: "data", - NewPath: "data", - Files: []*File{{ - Name: "a.txt", Path: fileA, - }, { - Name: "b.txt", Path: fileB, - }}, - }} +func TestSecretMiddleware_NoEnv(t *testing.T) { + // unset the env var + os.Unsetenv("SFM_API_SECRET") - // Move into project root so handler finds it - origWd, _ := os.Getwd() - if err := os.Chdir(projectRoot); err != nil { - t.Fatal(err) - } - defer os.Chdir(origWd) + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("apiSecret", "anything") + rr := httptest.NewRecorder() - // Call endpoint - req := httptest.NewRequest("GET", "/findDuplicateFiles?name=dupTest", nil) - w := httptest.NewRecorder() - findDuplicateFilesHandler(w, req) + handler := secretMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("next handler should not be called") + })) + handler.ServeHTTP(rr, req) - // Check response code - if w.Code != http.StatusOK { - t.Fatalf("expected status 200, got %d", w.Code) + if rr.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d", rr.Code) } +} - // Parse JSON - var entries []DuplicateEntry - if err := json.Unmarshal(w.Body.Bytes(), &entries); err != nil { - t.Fatalf("failed to parse JSON: %v", err) - } +func TestSecretMiddleware_WrongSecret(t *testing.T) { + os.Setenv("SFM_API_SECRET", "correctSecret") + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("apiSecret", "wrongSecret") + rr := httptest.NewRecorder() - // Expect one duplicate entry - if len(entries) != 1 { - t.Fatalf("expected 1 duplicate entry, got %d", len(entries)) + handler := secretMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("next handler should not be called") + })) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusUnauthorized { + t.Errorf("expected 401, got %d", rr.Code) } +} + +func TestSecretMiddleware_CorrectSecret(t *testing.T) { + os.Setenv("SFM_API_SECRET", "correctSecret") - exp := DuplicateEntry{ - Name: "b.txt", - Original: fileA, - Duplicate: fileB, + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("apiSecret", "correctSecret") + rr := httptest.NewRecorder() + + called := false + handler := secretMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + w.WriteHeader(http.StatusOK) + })) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) } - if entries[0] != exp { - t.Errorf("unexpected entry: got %+v, want %+v", entries[0], exp) + if !called { + t.Errorf("next handler was not called") } } -func TestAPI_BulkAddTags(t *testing.T) { - // Setup: Create temp folder and file + +// Test checkDirectoryConflicts function +func TestAPI_CheckDirectoryConflicts(t *testing.T) { tmp := t.TempDir() - projectRoot := filepath.Join(tmp, "Smart-File-Manager") - dataDir := filepath.Join(projectRoot, "data") - if err := os.MkdirAll(dataDir, 0755); err != nil { - t.Fatal(err) - } - filePath := filepath.Join(dataDir, "test.txt") - if err := os.WriteFile(filePath, []byte("content"), 0644); err != nil { + existingPath := filepath.Join(tmp, "existing") + if err := os.MkdirAll(existingPath, 0755); err != nil { t.Fatal(err) } - // Register the folder and file in composites + // Setup existing composite Composites = []*Folder{{ - Name: "bulkTest", - Path: "data", - NewPath: "data", - Files: []*File{{ - Name: "test.txt", - Path: filePath, - }}, + Name: "existing", + Path: existingPath, }} - // Change working directory - origWd, _ := os.Getwd() - defer os.Chdir(origWd) - if err := os.Chdir(projectRoot); err != nil { - t.Fatal(err) + // Test no conflict + newPath := filepath.Join(tmp, "separate") + hasConflict, message, err := checkDirectoryConflicts(newPath) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if hasConflict { + t.Errorf("Expected no conflict for separate paths, got: %s", message) } - // JSON payload for tags - jsonBody := `[{ - "file_path": "` + filePath + `", - "tags": ["alpha", "beta", "gamma"] - }]` - - // Create request with JSON body and query parameter ?name=bulkTest - req := httptest.NewRequest("POST", "/bulkAddTag?name=bulkTest", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - BulkAddTagHandler(w, req) - - // Verify response code - if w.Code != http.StatusOK { - t.Fatalf("expected 200 OK, got %d", w.Code) + // Test child conflict + childPath := filepath.Join(existingPath, "child") + hasConflict, message, err = checkDirectoryConflicts(childPath) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !hasConflict { + t.Error("Expected conflict for child path") + } + if !strings.Contains(message, "already contained within") { + t.Errorf("Expected containment message, got: %s", message) } - // if got := w.Body.String(); !strings.Contains(got, "Tags added successfully") { - // t.Errorf("expected success message, got %s", got) - // } - // Check if tags were actually added to the file - var file *File - for _, f := range Composites[0].Files { - if f.Path == filePath { - file = f - break - } + // Test parent conflict + parentPath := filepath.Dir(existingPath) + hasConflict, message, err = checkDirectoryConflicts(parentPath) + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - if file == nil { - t.Fatal("file not found in folder") + if !hasConflict { + t.Error("Expected conflict for parent path") } - expectedTags := map[string]bool{"alpha": false, "beta": false, "gamma": false} - for _, tag := range file.Tags { - if _, exists := expectedTags[tag]; exists { - expectedTags[tag] = true - } + if !strings.Contains(message, "would contain existing manager") { + t.Errorf("Expected parent containment message, got: %s", message) } - for tag, found := range expectedTags { - if !found { - t.Errorf("expected tag %s not found in file.Tags", tag) - } + + // Test exact match + hasConflict, message, err = checkDirectoryConflicts(existingPath) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !hasConflict { + t.Error("Expected conflict for exact path match") + } + if !strings.Contains(message, "already managed by") { + t.Errorf("Expected exact match message, got: %s", message) } } -func TestAPI_DeleteFileHandler(t *testing.T) { - // Setup: Create temp folder and file +// Test lock/unlock handlers edge cases +func TestAPI_LockUnlockHandlers_EdgeCases(t *testing.T) { + // Setup test data tmp := t.TempDir() - projectRoot := filepath.Join(tmp, "Smart-File-Manager") - dataDir := filepath.Join(projectRoot, "data") - if err := os.MkdirAll(dataDir, 0755); err != nil { + testFile := filepath.Join(tmp, "test.txt") + if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { t.Fatal(err) } - // Create a test file - testFileName := "delete_test.txt" - testFilePath := filepath.Join(dataDir, testFileName) - testContent := []byte("file to be deleted") - if err := os.WriteFile(testFilePath, testContent, 0644); err != nil { - t.Fatal(err) - } - - // Register the folder and file in composites - testFile := &File{ - Name: testFileName, - Path: testFilePath, - } - testFolder := &Folder{ - Name: "deleteTest", - Path: dataDir, - NewPath: dataDir, - Files: []*File{testFile}, + Name: "lockTest", + Path: tmp, + Files: []*File{{ + Name: "test.txt", + Path: testFile, + }}, } - Composites = []*Folder{testFolder} - // Change working directory - origWd, _ := os.Getwd() - defer os.Chdir(origWd) - if err := os.Chdir(projectRoot); err != nil { - t.Fatal(err) + // Test missing parameters + req := httptest.NewRequest("GET", "/lock?path="+tmp, nil) // missing name + w := httptest.NewRecorder() + lockHandler(w, req) + if w.Body.String() != "Parameter missing" { + t.Errorf("Expected 'Parameter missing', got %s", w.Body.String()) } - // Verify file exists before deletion - if _, err := os.Stat(testFilePath); os.IsNotExist(err) { - t.Fatal("test file should exist before deletion") + req = httptest.NewRequest("GET", "/lock?name=lockTest", nil) // missing path + w = httptest.NewRecorder() + lockHandler(w, req) + if w.Body.String() != "Parameter missing" { + t.Errorf("Expected 'Parameter missing', got %s", w.Body.String()) } - // Test successful file deletion - req := httptest.NewRequest("GET", "/deleteFile?name=deleteTest&path="+testFilePath, nil) - w := httptest.NewRecorder() - deleteFileHandler(w, req) - - // Verify response - // if w.Body.String() != "true" { - // t.Fatalf("deleteFileHandler: expected true, got %s", w.Body.String()) - // } - - // Verify file was actually deleted from filesystem - if _, err := os.Stat(testFilePath); !os.IsNotExist(err) { - t.Error("file should have been deleted from filesystem") + // Test non-existent manager + req = httptest.NewRequest("GET", "/lock?name=nonexistent&path="+tmp, nil) + w = httptest.NewRecorder() + lockHandler(w, req) + if w.Body.String() != "false" { + t.Errorf("Expected 'false' for non-existent manager, got %s", w.Body.String()) } - // Verify file was removed from composite structure - // Note: This test assumes RemoveFile method works correctly - // You might want to verify the file is no longer in the Files slice -} - -func TestAPI_DeleteFileHandler_MissingParams(t *testing.T) { - // Test missing path parameter - req := httptest.NewRequest("GET", "/deleteFile?name=test", nil) - w := httptest.NewRecorder() - deleteFileHandler(w, req) + // Test successful lock + req = httptest.NewRequest("GET", "/lock?name=lockTest&path="+tmp, nil) + w = httptest.NewRecorder() + lockHandler(w, req) + if w.Body.String() != "true" { + t.Errorf("Expected 'true' for successful lock, got %s", w.Body.String()) + } - if w.Body.String() != "Parameter missing" { - t.Errorf("expected 'Parameter missing', got %s", w.Body.String()) + // Verify file was locked + file := testFolder.GetFile(testFile) + if file == nil || !file.Locked { + t.Error("Expected file to be locked") } - // Test missing name parameter - req = httptest.NewRequest("GET", "/deleteFile?path=/some/path", nil) + // Test unlock with same edge cases + req = httptest.NewRequest("GET", "/unlock?path="+tmp, nil) // missing name w = httptest.NewRecorder() - deleteFileHandler(w, req) - + unlockHandler(w, req) if w.Body.String() != "Parameter missing" { - t.Errorf("expected 'Parameter missing', got %s", w.Body.String()) + t.Errorf("Expected 'Parameter missing', got %s", w.Body.String()) } - // Test both parameters missing - req = httptest.NewRequest("GET", "/deleteFile", nil) + // Test successful unlock + req = httptest.NewRequest("GET", "/unlock?name=lockTest&path="+tmp, nil) w = httptest.NewRecorder() - deleteFileHandler(w, req) - - if w.Body.String() != "Parameter missing" { - t.Errorf("expected 'Parameter missing', got %s", w.Body.String()) + unlockHandler(w, req) + if w.Body.String() != "true" { + t.Errorf("Expected 'true' for successful unlock, got %s", w.Body.String()) } -} - -func TestAPI_DeleteFileHandler_NonExistentManager(t *testing.T) { - // Setup empty composites - Composites = []*Folder{} - - // Test with non-existent manager name - req := httptest.NewRequest("GET", "/deleteFile?name=nonexistent&path=/some/path", nil) - w := httptest.NewRecorder() - deleteFileHandler(w, req) - if w.Body.String() != "false" { - t.Errorf("expected 'false' for non-existent manager, got %s", w.Body.String()) + // Verify file was unlocked + if file.Locked { + t.Error("Expected file to be unlocked") } } -func TestAPI_DeleteFolderHandler(t *testing.T) { - // Setup: Create temp folder structure +// Test addTagHandler edge cases +func TestAPI_AddTagHandler_EdgeCases(t *testing.T) { tmp := t.TempDir() - projectRoot := filepath.Join(tmp, "Smart-File-Manager") - dataDir := filepath.Join(projectRoot, "data") - testDir := filepath.Join(dataDir, "test_folder") - if err := os.MkdirAll(testDir, 0755); err != nil { + testFile := filepath.Join(tmp, "test.txt") + if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { t.Fatal(err) } - // Create files inside the test folder - testFile1 := filepath.Join(testDir, "file1.txt") - testFile2 := filepath.Join(testDir, "file2.txt") - if err := os.WriteFile(testFile1, []byte("content1"), 0644); err != nil { - t.Fatal(err) + testFolder := &Folder{ + Name: "tagTest", + Path: tmp, + Files: []*File{{ + Name: "test.txt", + Path: testFile, + }}, } - if err := os.WriteFile(testFile2, []byte("content2"), 0644); err != nil { - t.Fatal(err) + Composites = []*Folder{testFolder} + + // Test missing parameters + req := httptest.NewRequest("GET", "/addTag?path="+testFile, nil) // missing tag + w := httptest.NewRecorder() + addTagHandler(w, req) + if w.Body.String() != "false" { + t.Errorf("Expected 'false' for missing tag, got %s", w.Body.String()) } - // Create nested subfolder - nestedDir := filepath.Join(testDir, "nested") - if err := os.MkdirAll(nestedDir, 0755); err != nil { - t.Fatal(err) + req = httptest.NewRequest("GET", "/addTag?tag=testtag", nil) // missing path + w = httptest.NewRecorder() + addTagHandler(w, req) + if w.Body.String() != "false" { + t.Errorf("Expected 'false' for missing path, got %s", w.Body.String()) } - nestedFile := filepath.Join(nestedDir, "nested_file.txt") - if err := os.WriteFile(nestedFile, []byte("nested content"), 0644); err != nil { - t.Fatal(err) + + // Test non-existent file + req = httptest.NewRequest("GET", "/addTag?path=/nonexistent/file.txt&tag=testtag", nil) + w = httptest.NewRecorder() + addTagHandler(w, req) + if w.Body.String() != "false" { + t.Errorf("Expected 'false' for non-existent file, got %s", w.Body.String()) } - // Register the folder structure in composites - testFolder := &Folder{ - Name: "deleteFolderTest", - Path: dataDir, - NewPath: dataDir, - Files: []*File{ - {Name: "file1.txt", Path: testFile1}, - {Name: "file2.txt", Path: testFile2}, - {Name: "nested_file.txt", Path: nestedFile}, - }, - Subfolders: []*Folder{ - {Name: "test_folder", Path: testDir}, - {Name: "nested", Path: nestedDir}, - }, + // Test successful tag addition + req = httptest.NewRequest("GET", "/addTag?path="+testFile+"&tag=testtag", nil) + w = httptest.NewRecorder() + addTagHandler(w, req) + if w.Body.String() != "true" { + t.Errorf("Expected 'true' for successful tag addition, got %s", w.Body.String()) } - Composites = []*Folder{testFolder} + // Verify tag was added + file := testFolder.GetFile(testFile) + if file == nil { + t.Fatal("File not found") + } + found := false + for _, tag := range file.Tags { + if tag == "testtag" { + found = true + break + } + } + if !found { + t.Error("Expected tag to be added to file") + } +} - // Change working directory - origWd, _ := os.Getwd() - defer os.Chdir(origWd) - if err := os.Chdir(projectRoot); err != nil { +// Test removeTagHandler with folder tags +func TestAPI_RemoveTagHandler_FolderTags(t *testing.T) { + tmp := t.TempDir() + subDir := filepath.Join(tmp, "subdir") + if err := os.MkdirAll(subDir, 0755); err != nil { t.Fatal(err) } - // Verify folder exists before deletion - if _, err := os.Stat(testDir); os.IsNotExist(err) { - t.Fatal("test folder should exist before deletion") + subfolder := &Folder{ + Name: "subdir", + Path: subDir, + Tags: []string{"foldertag"}, } - // Test successful folder deletion - req := httptest.NewRequest("GET", "/deleteFolder?name=deleteFolderTest&path="+testDir, nil) - w := httptest.NewRecorder() - deleteFolderHandler(w, req) - - // Verify response - // if w.Body.String() != "true" { - // t.Fatalf("deleteFolderHandler: expected true, got %s", w.Body.String()) - // } - - // Verify folder and all its contents were deleted from filesystem - if _, err := os.Stat(testDir); !os.IsNotExist(err) { - t.Error("folder should have been deleted from filesystem") + testFolder := &Folder{ + Name: "tagTest", + Path: tmp, + Subfolders: []*Folder{subfolder}, } + Composites = []*Folder{testFolder} - // Verify nested files were also deleted - if _, err := os.Stat(testFile1); !os.IsNotExist(err) { - t.Error("nested file1 should have been deleted") - } - if _, err := os.Stat(testFile2); !os.IsNotExist(err) { - t.Error("nested file2 should have been deleted") + // Test successful folder tag removal + req := httptest.NewRequest("GET", "/removeTag?path="+subDir+"&tag=foldertag", nil) + w := httptest.NewRecorder() + removeTagHandler(w, req) + if w.Body.String() != "true" { + t.Errorf("Expected 'true' for successful folder tag removal, got %s", w.Body.String()) } - if _, err := os.Stat(nestedFile); !os.IsNotExist(err) { - t.Error("nested file should have been deleted") + + // Verify tag was removed + if len(subfolder.Tags) != 0 { + t.Error("Expected folder tag to be removed") } - // Verify folder was removed from composite structure - // Note: This test assumes RemoveSubfolder method works correctly + // Test removing non-existent tag + req = httptest.NewRequest("GET", "/removeTag?path="+subDir+"&tag=nonexistenttag", nil) + w = httptest.NewRecorder() + removeTagHandler(w, req) + if w.Body.String() != "false" { + t.Errorf("Expected 'false' for non-existent tag removal, got %s", w.Body.String()) + } } -func TestAPI_DeleteFolderHandler_MissingParams(t *testing.T) { - // Test missing path parameter - req := httptest.NewRequest("GET", "/deleteFolder?name=test", nil) - w := httptest.NewRecorder() - deleteFolderHandler(w, req) +// Test deleteManager edge cases +func TestAPI_DeleteManagerHandler_EdgeCases(t *testing.T) { + // Test with empty composites + Composites = []*Folder{} - if w.Body.String() != "Parameter missing" { - t.Errorf("expected 'Parameter missing', got %s", w.Body.String()) + req := httptest.NewRequest("GET", "/deleteManager?name=nonexistent", nil) + w := httptest.NewRecorder() + deleteManagerHandler(w, req) + if w.Body.String() != "Manager not found" { + t.Errorf("Expected 'Manager not found', got %s", w.Body.String()) } // Test missing name parameter - req = httptest.NewRequest("GET", "/deleteFolder?path=/some/path", nil) + req = httptest.NewRequest("GET", "/deleteManager", nil) w = httptest.NewRecorder() - deleteFolderHandler(w, req) - + deleteManagerHandler(w, req) if w.Body.String() != "Parameter missing" { - t.Errorf("expected 'Parameter missing', got %s", w.Body.String()) + t.Errorf("Expected 'Parameter missing', got %s", w.Body.String()) } +} - // Test both parameters missing - req = httptest.NewRequest("GET", "/deleteFolder", nil) - w = httptest.NewRecorder() - deleteFolderHandler(w, req) +// Test GetComposites function +func TestAPI_GetComposites(t *testing.T) { + // Setup test data + testComposite := &Folder{Name: "test", Path: "/test"} + Composites = []*Folder{testComposite} - if w.Body.String() != "Parameter missing" { - t.Errorf("expected 'Parameter missing', got %s", w.Body.String()) + // Test getter function + result := GetComposites() + if len(result) != 1 { + t.Errorf("Expected 1 composite, got %d", len(result)) + } + if result[0].Name != "test" { + t.Errorf("Expected name 'test', got %s", result[0].Name) } } -func TestAPI_DeleteFolderHandler_NonExistentManager(t *testing.T) { - // Setup empty composites - Composites = []*Folder{} +// Test concurrent access to handlers (mutex testing) +func TestAPI_ConcurrentAccess(t *testing.T) { + tmp := t.TempDir() + testFile := filepath.Join(tmp, "test.txt") + if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { + t.Fatal(err) + } - // Test with non-existent manager name - req := httptest.NewRequest("GET", "/deleteFolder?name=nonexistent&path=/some/path", nil) - w := httptest.NewRecorder() - deleteFolderHandler(w, req) + testFolder := &Folder{ + Name: "concurrentTest", + Path: tmp, + Files: []*File{{ + Name: "test.txt", + Path: testFile, + }}, + } + Composites = []*Folder{testFolder} - if w.Body.String() != "false" { - t.Errorf("expected 'false' for non-existent manager, got %s", w.Body.String()) + // Launch multiple goroutines to test concurrent access + done := make(chan bool, 10) + + for i := 0; i < 10; i++ { + go func(i int) { + tagName := fmt.Sprintf("tag%d", i) + req := httptest.NewRequest("GET", "/addTag?path="+testFile+"&tag="+tagName, nil) + w := httptest.NewRecorder() + addTagHandler(w, req) + done <- true + }(i) + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } + + // Verify that some tags were added (exact number may vary due to race conditions) + file := testFolder.GetFile(testFile) + if file == nil { + t.Fatal("File not found") + } + if len(file.Tags) == 0 { + t.Error("Expected at least some tags to be added") } } -func TestAPI_DeleteHandlers_ErrorHandling(t *testing.T) { - // This test verifies that the handlers properly handle OS errors - // Note: The current implementation uses panic() for os.Remove errors - // In a production environment, you might want to handle errors more gracefully +// Test error handling in JSON encoding (edge case) +func TestAPI_JSONEncodingError(t *testing.T) { + // This is harder to test directly, but we can at least ensure + // the handlers set proper content types and handle basic cases - // Setup: Create a composite with a non-existent file path + tmp := t.TempDir() testFolder := &Folder{ - Name: "errorTest", - Path: "/nonexistent/path", - NewPath: "/nonexistent/path", + Name: "jsonTest", + Path: tmp, } - Composites = []*Folder{testFolder} - // Test file deletion with non-existent file - // Note: This will panic in the current implementation - // You might want to modify the handler to return an error instead - defer func() { - if r := recover(); r != nil { - // Expected panic due to os.Remove error - t.Log("deleteFileHandler panicked as expected when file doesn't exist") - } - }() - - req := httptest.NewRequest("GET", "/deleteFile?name=errorTest&path=/nonexistent/file.txt", nil) + // Test delete handlers that return JSON + req := httptest.NewRequest("GET", "/deleteFile?name=jsonTest&path="+filepath.Join(tmp, "nonexistent.txt"), nil) w := httptest.NewRecorder() deleteFileHandler(w, req) - // If we reach here without panic, the test might need adjustment - t.Log("deleteFileHandler completed without panic") + // The handler should attempt to encode JSON even on error + contentType := w.Header().Get("Content-Type") + if !strings.Contains(contentType, "application/json") { + t.Errorf("Expected JSON content type, got %s", contentType) + } } - -func TestAPI_BulkDeleteFolderHandler(t *testing.T) { - // Setup: Create temp folder structure with multiple subfolders +func TestAPI_DeleteManagerHandler_Success(t *testing.T) { tmp := t.TempDir() - projectRoot := filepath.Join(tmp, "Smart-File-Manager") - dataDir := filepath.Join(projectRoot, "data") - - // Create multiple test folders - folder1 := filepath.Join(dataDir, "folder1") - folder2 := filepath.Join(dataDir, "folder2") - folder3 := filepath.Join(dataDir, "folder3") + testFolder := &Folder{Name: "delTest", Path: tmp} + Composites = []*Folder{testFolder} + managersFilePath = filepath.Join(tmp, "main.json") + // Write a valid JSON file with the manager record + recs := []ManagerRecord{{Name: "delTest", Path: tmp}} + data, _ := json.Marshal(recs) + os.WriteFile(managersFilePath, data, 0644) - if err := os.MkdirAll(folder1, 0755); err != nil { - t.Fatal(err) + req := httptest.NewRequest("GET", "/deleteManager?name=delTest", nil) + w := httptest.NewRecorder() + deleteManagerHandler(w, req) + if w.Body.String() != "true" { + t.Errorf("Expected 'true', got %s", w.Body.String()) } - if err := os.MkdirAll(folder2, 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(folder3, 0755); err != nil { - t.Fatal(err) + if len(Composites) != 0 { + t.Errorf("Expected composites to be empty after deletion") } +} - // Add some files to the folders - file1 := filepath.Join(folder1, "test1.txt") - file2 := filepath.Join(folder2, "test2.txt") - file3 := filepath.Join(folder3, "test3.txt") +func TestAPI_DeleteFolderHandler_Success(t *testing.T) { + tmp := t.TempDir() + subDir := filepath.Join(tmp, "subdir") + os.MkdirAll(subDir, 0755) + testFolder := &Folder{Name: "delFolderTest", Path: tmp, Subfolders: []*Folder{{Name: "subdir", Path: subDir}}} + Composites = []*Folder{testFolder} - if err := os.WriteFile(file1, []byte("content1"), 0644); err != nil { - t.Fatal(err) + req := httptest.NewRequest("GET", "/deleteFolder?name=delFolderTest&path="+subDir, nil) + w := httptest.NewRecorder() + deleteFolderHandler(w, req) + if w.Code != http.StatusOK { + t.Errorf("Expected 200, got %d", w.Code) } - if err := os.WriteFile(file2, []byte("content2"), 0644); err != nil { - t.Fatal(err) + if _, err := os.Stat(subDir); !os.IsNotExist(err) { + t.Errorf("Expected subdir to be deleted") } - if err := os.WriteFile(file3, []byte("content3"), 0644); err != nil { - t.Fatal(err) +} + +func TestTagsConversionRoundTrip(t *testing.T) { + in := []*pb.Tag{ + {Name: "one"}, + {Name: "Two"}, + {Name: "three"}, + } + strs := tagsToStrings(in) + if len(strs) != len(in) { + t.Fatalf("expected %d tags, got %d", len(in), len(strs)) + } + out := stringsToTags(strs) + if len(out) != len(in) { + t.Fatalf("expected %d pb.Tags, got %d", len(in), len(out)) + } + for i := range in { + if in[i].Name != out[i].Name { + t.Fatalf("expected tag %q at index %d, got %q", in[i].Name, i, out[i].Name) + } } +} - // Register the folder structure in composites - testFolder := &Folder{ - Name: "bulkDeleteFolderTest", - Path: dataDir, - NewPath: dataDir, - Files: []*File{ - {Name: "test1.txt", Path: file1}, - {Name: "test2.txt", Path: file2}, - {Name: "test3.txt", Path: file3}, +// Ensure nil guards don't panic +func TestMergeProtoNilGuards(t *testing.T) { + // both nil + mergeProtoToFolder(nil, nil) + + // dir nil, existing non-nil + existing := &Folder{Name: "exists", Path: "/tmp"} + mergeProtoToFolder(nil, existing) + + // dir non-nil, existing nil + dir := &pb.Directory{Name: "dir", Path: "/tmp/dir"} + mergeProtoToFolder(dir, nil) + + // helper nil guard + mergeProtoToFolderHelper(nil, nil) + mergeProtoToFolderHelper(nil, &Folder{}) + mergeProtoToFolderHelper(&pb.Directory{}, nil) +} + +// Test mapping of files and nested directories via mergeProtoToFolder and helper +func TestMergeProtoToFolder_Mapping(t *testing.T) { + // Build a proto tree: + // root + // - fileA (with one tag) + // - subdir (contains fileB with keyword) + root := &pb.Directory{ + Name: "root", + Path: "/root", + Files: []*pb.File{ + { + Name: "fileA.txt", + OriginalPath: "/root/fileA.txt", + Tags: []*pb.Tag{{Name: "alpha"}, {Name: "beta"}}, + IsLocked: true, + NewPath: "/newroot/fileA.txt", + // Metadata left nil (metadataConverter should handle nil) + }, }, - Subfolders: []*Folder{ - {Name: "folder1", Path: folder1}, - {Name: "folder2", Path: folder2}, - {Name: "folder3", Path: folder3}, + Directories: []*pb.Directory{ + { + Name: "subdir", + Path: "/root/subdir", + Files: []*pb.File{ + { + Name: "fileB.md", + OriginalPath: "/root/subdir/fileB.md", + Tags: []*pb.Tag{{Name: "gamma"}}, + IsLocked: false, + NewPath: "/newroot/subdir/fileB.md", + Keywords: []*pb.Keyword{{Keyword: "kw1", Score: 2.0}}, + }, + }, + }, }, } - Composites = []*Folder{testFolder} + // existing folder with pre-populated content that should be cleared + existing := &Folder{ + Name: "old", + Path: "/old", + Files: []*File{{Name: "oldfile"}}, + Subfolders: []*Folder{{Name: "oldsub"}}, + } - // Change working directory - origWd, _ := os.Getwd() - defer os.Chdir(origWd) - if err := os.Chdir(projectRoot); err != nil { - t.Fatal(err) + mergeProtoToFolder(root, existing) + + // after merge, existing should reflect proto + if existing.Name != "old" { + // mergeProtoToFolder doesn't change existing.Name at top-level, only subfolders via helper. + // we only assert Files and Subfolders contents here. } - // Verify folders exist before deletion - if _, err := os.Stat(folder1); os.IsNotExist(err) { - t.Fatal("folder1 should exist before deletion") + // Files length and mapping + if len(existing.Files) != 1 { + t.Fatalf("expected 1 file in existing.Files, got %d", len(existing.Files)) } - if _, err := os.Stat(folder2); os.IsNotExist(err) { - t.Fatal("folder2 should exist before deletion") + f := existing.Files[0] + if f.Name != "fileA.txt" { + t.Fatalf("expected file name fileA.txt, got %q", f.Name) } - if _, err := os.Stat(folder3); os.IsNotExist(err) { - t.Fatal("folder3 should exist before deletion") + if f.Path != "/root/fileA.txt" { + t.Fatalf("expected path /root/fileA.txt, got %q", f.Path) } - - // Create JSON payload for bulk deletion (delete folder1 and folder3) - jsonBody := `[ - {"file_path": "` + folder1 + `"}, - {"file_path": "` + folder3 + `"} - ]` - - // Test successful bulk folder deletion - req := httptest.NewRequest("POST", "/bulkDeleteFolder?name=bulkDeleteFolderTest", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFolderHandler(w, req) - - // Verify response - if w.Code != http.StatusOK { - t.Fatalf("expected status 200, got %d", w.Code) + if f.Locked != true { + t.Fatalf("expected Locked true, got %v", f.Locked) + } + // tagsToStrings should have converted tags + if len(f.Tags) != 2 || f.Tags[0] != "alpha" || f.Tags[1] != "beta" { + t.Fatalf("tags not converted correctly: %#v", f.Tags) } - // if got := w.Body.String(); !strings.Contains(got, "Folders removed successfully") { - // t.Errorf("expected success message, got %s", got) - // } - // Verify deleted folders no longer exist - if _, err := os.Stat(folder1); !os.IsNotExist(err) { - t.Error("folder1 should have been deleted") + // Subfolders length and mapping + if len(existing.Subfolders) != 1 { + t.Fatalf("expected 1 subfolder, got %d", len(existing.Subfolders)) } - if _, err := os.Stat(folder3); !os.IsNotExist(err) { - t.Error("folder3 should have been deleted") + sub := existing.Subfolders[0] + if sub.Name != "subdir" { + t.Fatalf("expected subfolder name subdir, got %q", sub.Name) } - - // Verify folder2 still exists (wasn't in deletion list) - if _, err := os.Stat(folder2); os.IsNotExist(err) { - t.Error("folder2 should still exist") + if sub.Path != "/root/subdir" { + t.Fatalf("expected subfolder path /root/subdir, got %q", sub.Path) + } + // child files mapping via helper + if len(sub.Files) != 1 { + t.Fatalf("expected 1 file in sub.Files, got %d", len(sub.Files)) + } + fb := sub.Files[0] + if fb.Name != "fileB.md" { + t.Fatalf("expected sub file name fileB.md, got %q", fb.Name) + } + // Keywords should be preserved by helper + if len(fb.Keywords) != 1 || fb.Keywords[0].Keyword != "kw1" { + t.Fatalf("expected keyword kw1 in child file, got %#v", fb.Keywords) } } -func TestAPI_BulkDeleteFolderHandler_MissingName(t *testing.T) { - jsonBody := `[{"file_path": "/some/path"}]` +// A small test to ensure helper updates an existing folder in-place +func TestMergeProtoToFolderHelper_InPlace(t *testing.T) { + dir := &pb.Directory{ + Name: "X", + Path: filepath.Join("/base", "X"), + Files: []*pb.File{ + { + Name: "a.txt", + OriginalPath: "/base/X/a.txt", + Tags: []*pb.Tag{{Name: "t1"}}, + }, + }, + Directories: []*pb.Directory{ + { + Name: "C", + Path: "/base/X/C", + }, + }, + } - req := httptest.NewRequest("POST", "/bulkDeleteFolder", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFolderHandler(w, req) + existing := &Folder{ + Name: "oldName", + Path: "/oldpath", + Files: []*File{{Name: "should be removed"}}, + Subfolders: []*Folder{{Name: "should be removed"}}, + } - if w.Code != http.StatusBadRequest { - t.Errorf("expected status 400, got %d", w.Code) + mergeProtoToFolderHelper(dir, existing) + + if existing.Name != "X" { + t.Fatalf("expected Name X, got %q", existing.Name) + } + if existing.Path != "/base/X" { + t.Fatalf("expected Path /base/X, got %q", existing.Path) } - if got := w.Body.String(); !strings.Contains(got, "Missing 'name' parameter") { - t.Errorf("expected missing name error, got %s", got) + if len(existing.Files) != 1 || existing.Files[0].Name != "a.txt" { + t.Fatalf("file mapping failed, got %#v", existing.Files) + } + if len(existing.Subfolders) != 1 || existing.Subfolders[0].Name != "C" { + t.Fatalf("subfolder mapping failed, got %#v", existing.Subfolders) } } -func TestAPI_BulkDeleteFolderHandler_InvalidJSON(t *testing.T) { - invalidJSON := `{"invalid": json}` +func captureOutput(fn func()) string { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w - req := httptest.NewRequest("POST", "/bulkDeleteFolder?name=test", strings.NewReader(invalidJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFolderHandler(w, req) + outC := make(chan string) + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + outC <- buf.String() + }() - if w.Code != http.StatusBadRequest { - t.Errorf("expected status 400, got %d", w.Code) + fn() + _ = w.Close() + os.Stdout = old + return <-outC +} + +func TestDirLabel_UnnamedAndLocked(t *testing.T) { + d := &pb.Directory{ + Name: " ", + Path: "/some/path", + Directories: []*pb.Directory{}, + Files: []*pb.File{}, + IsLocked: true, + } + s := dirLabel(d) + if !strings.Contains(s, "") { + t.Fatalf("expected in label, got: %s", s) + } + if !strings.Contains(s, "[locked]") { + t.Fatalf("expected [locked] in label, got: %s", s) } - if got := w.Body.String(); !strings.Contains(got, "Invalid request body") { - t.Errorf("expected invalid request body error, got %s", got) + if !strings.Contains(s, "files=0") || !strings.Contains(s, "dirs=0") { + t.Fatalf("expected counts in label, got: %s", s) } } -func TestAPI_BulkDeleteFolderHandler_NonExistentManager(t *testing.T) { - Composites = []*Folder{} - - jsonBody := `[{"file_path": "/some/path"}]` - - req := httptest.NewRequest("POST", "/bulkDeleteFolder?name=nonexistent", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFolderHandler(w, req) +func TestConvertFolderToProto_And_MetadataConverter(t *testing.T) { + f := Folder{ + Name: "root", + Path: "/root", + Files: []*File{ + { + Name: "a.txt", + Path: "/root/a.txt", + Tags: []string{"one", "two"}, + Locked: true, + NewPath: "/new/a.txt", + }, + }, + Subfolders: []*Folder{ + {Name: "sub", Path: "/root/sub"}, + }, + } - if w.Code != http.StatusNotFound { - t.Errorf("expected status 404, got %d", w.Code) + proto := convertFolderToProto(f) + if proto.Name != "root" || proto.Path != "/root" { + t.Fatalf("proto folder header incorrect: %#v", proto) + } + if len(proto.Files) != 1 || proto.Files[0].Name != "a.txt" { + t.Fatalf("file mapping failed: %#v", proto.Files) } - if got := w.Body.String(); !strings.Contains(got, "Folder not found") { - t.Errorf("expected folder not found error, got %s", got) + // metadataConverter: simple round-trip + metaIn := []*pb.MetadataEntry{{Key: "size", Value: "123"}} + metaOut := metadataConverter(metaIn) + if len(metaOut) != 1 || metaOut[0].Key != "size" || metaOut[0].Value != "123" { + t.Fatalf("metadataConverter failed: %#v", metaOut) } } -func TestAPI_BulkDeleteFolderHandler_EmptyList(t *testing.T) { - // Setup minimal composite - testFolder := &Folder{ - Name: "emptyTest", - Path: "/test", - NewPath: "/test", +func TestPrintDirectoryWithMetadata_And_PrintDirChildren(t *testing.T) { + dir := &pb.Directory{ + Name: "root", + Path: "/root", + Directories: []*pb.Directory{ + { + Name: "subdir", + Path: "/root/subdir", + Files: []*pb.File{}, + }, + }, + Files: []*pb.File{ + { + Name: "file1.txt", + OriginalPath: "/root/file1.txt", + Tags: []*pb.Tag{{Name: "t1"}}, + IsLocked: false, + }, + }, + IsLocked: false, } - Composites = []*Folder{testFolder} - jsonBody := `[]` + out := captureOutput(func() { + printDirectoryWithMetadata(dir, 0) + }) - req := httptest.NewRequest("POST", "/bulkDeleteFolder?name=emptyTest", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFolderHandler(w, req) + // basic assertions that important pieces are printed + if !strings.Contains(out, "[DIR] root") { + t.Fatalf("expected root label printed, got: %s", out) + } + if !strings.Contains(out, "subdir") { + t.Fatalf("expected subdir printed, got: %s", out) + } + if !strings.Contains(out, "file1.txt") { + t.Fatalf("expected file1.txt printed, got: %s", out) + } - if w.Code != http.StatusOK { - t.Errorf("expected status 200 for empty list, got %d", w.Code) + // test empty directory prints "(empty)" + empty := &pb.Directory{Name: "empty", Path: "/empty"} + out2 := captureOutput(func() { + printDirChildren(empty, "") + }) + if !strings.Contains(out2, "(empty)") { + t.Fatalf("expected (empty) printed for empty dir, got: %s", out2) } } -func TestAPI_BulkDeleteFileHandler(t *testing.T) { - // Setup: Create temp folder and multiple test files +func TestLoadEnvFile_ParsesFileCorrectly(t *testing.T) { tmp := t.TempDir() - projectRoot := filepath.Join(tmp, "Smart-File-Manager") - dataDir := filepath.Join(projectRoot, "data") - if err := os.MkdirAll(dataDir, 0755); err != nil { + fpath := filepath.Join(tmp, "test.env") + content := `# comment line +export FOO=bar +BAZ=qux +EMPTY= +# trailing comment +WITH_EQ=part1=part2 +` + if err := os.WriteFile(fpath, []byte(content), 0644); err != nil { t.Fatal(err) } - // Create multiple test files - file1 := filepath.Join(dataDir, "file1.txt") - file2 := filepath.Join(dataDir, "file2.txt") - file3 := filepath.Join(dataDir, "file3.txt") + vars, err := loadEnvFile(fpath) + if err != nil { + t.Fatalf("loadEnvFile error: %v", err) + } - if err := os.WriteFile(file1, []byte("content1"), 0644); err != nil { - t.Fatal(err) + if vars["FOO"] != "bar" { + t.Fatalf("expected FOO=bar got %q", vars["FOO"]) } - if err := os.WriteFile(file2, []byte("content2"), 0644); err != nil { - t.Fatal(err) + if vars["BAZ"] != "qux" { + t.Fatalf("expected BAZ=qux got %q", vars["BAZ"]) } - if err := os.WriteFile(file3, []byte("content3"), 0644); err != nil { + // EMPTY present as empty string + if val, ok := vars["EMPTY"]; !ok || val != "" { + t.Fatalf("expected EMPTY present as empty string, got %#v, ok=%v", val, ok) + } + // WITH_EQ should preserve everything after first = + if vars["WITH_EQ"] != "part1=part2" { + t.Fatalf("expected WITH_EQ=part1=part2 got %q", vars["WITH_EQ"]) + } +} + +func TestFindProjectRoot_FoundAndNotFound(t *testing.T) { + tmp := t.TempDir() + targetName := "marker.txt" + targetPath := filepath.Join(tmp, targetName) + if err := os.WriteFile(targetPath, []byte("x"), 0644); err != nil { t.Fatal(err) } - // Register the files in composites - testFolder := &Folder{ - Name: "bulkDeleteFileTest", - Path: dataDir, - NewPath: dataDir, - Files: []*File{ - {Name: "file1.txt", Path: file1}, - {Name: "file2.txt", Path: file2}, - {Name: "file3.txt", Path: file3}, - }, + // create nested cwd + nested := filepath.Join(tmp, "a", "b") + if err := os.MkdirAll(nested, 0755); err != nil { + t.Fatal(err) } - Composites = []*Folder{testFolder} + origWD, _ := os.Getwd() + defer os.Chdir(origWD) - // Change working directory - origWd, _ := os.Getwd() - defer os.Chdir(origWd) - if err := os.Chdir(projectRoot); err != nil { + if err := os.Chdir(nested); err != nil { t.Fatal(err) } - // Verify files exist before deletion - if _, err := os.Stat(file1); os.IsNotExist(err) { - t.Fatal("file1 should exist before deletion") + found, err := FindProjectRoot(targetName) + if err != nil { + t.Fatalf("expected to find %s, got error: %v", targetName, err) } - if _, err := os.Stat(file2); os.IsNotExist(err) { - t.Fatal("file2 should exist before deletion") + if filepath.Clean(found) != filepath.Clean(targetPath) { + t.Fatalf("expected %q, got %q", targetPath, found) } - if _, err := os.Stat(file3); os.IsNotExist(err) { - t.Fatal("file3 should exist before deletion") + + // Not found case + if err := os.Chdir(tmp); err != nil { + t.Fatal(err) + } + _, err = FindProjectRoot("definitely_not_present_12345") + if err == nil { + t.Fatalf("expected error when file not found") } +} - // Create JSON payload for bulk deletion (delete file1 and file3) - jsonBody := `[ - {"file_path": "` + file1 + `"}, - {"file_path": "` + file3 + `"} - ]` +func TestLoadTreeDataHandlerGoOnly_NoManager(t *testing.T) { + // ensure Composites is empty for this test + orig := Composites + defer func() { Composites = orig }() + Composites = []*Folder{} - // Test successful bulk file deletion - req := httptest.NewRequest("POST", "/bulkDeleteFiles?name=bulkDeleteFileTest", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFileHandler(w, req) + req := httptest.NewRequest("GET", "/loadTreeData?name=nonexistent", nil) + rr := httptest.NewRecorder() - // Verify response - if w.Code != http.StatusOK { - t.Fatalf("expected status 200, got %d", w.Code) - } - // if got := w.Body.String(); !strings.Contains(got, "Files removed successfully") { - // t.Errorf("expected success message, got %s", got) - // } + loadTreeDataHandlerGoOnly(rr, req) - // Verify deleted files no longer exist - if _, err := os.Stat(file1); !os.IsNotExist(err) { - t.Error("file1 should have been deleted") - } - if _, err := os.Stat(file3); !os.IsNotExist(err) { - t.Error("file3 should have been deleted") + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400 for missing manager, got %d", rr.Code) } - - // Verify file2 still exists (wasn't in deletion list) - if _, err := os.Stat(file2); os.IsNotExist(err) { - t.Error("file2 should still exist") + if !strings.Contains(rr.Body.String(), "No smart manager") { + t.Fatalf("unexpected body: %s", rr.Body.String()) } } -func TestAPI_BulkDeleteFileHandler_MissingName(t *testing.T) { - jsonBody := `[{"file_path": "/some/path"}]` +func TestPrettyPrintFolder_OutputContainsExpected(t *testing.T) { + // build sample folder + root := &Folder{ + Name: "root", + Path: "/root", + Files: []*File{ + {Name: "a.txt", Path: "/root/a.txt", Tags: []string{"t1", "t2"}}, + }, + Subfolders: []*Folder{ + {Name: "sub", Path: "/root/sub", Files: []*File{{Name: "inner.txt"}}}, + }, + } - req := httptest.NewRequest("POST", "/bulkDeleteFiles", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFileHandler(w, req) + out := captureOutput(func() { + PrettyPrintFolder(root, "") + }) - if w.Code != http.StatusBadRequest { - t.Errorf("expected status 400, got %d", w.Code) + if !strings.Contains(out, "📁 root") { + t.Fatalf("expected root folder printed, got: %s", out) } - if got := w.Body.String(); !strings.Contains(got, "Missing 'name' parameter") { - t.Errorf("expected missing name error, got %s", got) + if !strings.Contains(out, "📄 a.txt") { + t.Fatalf("expected file a.txt printed, got: %s", out) + } + if !strings.Contains(out, "TAG: t1") { + t.Fatalf("expected tag printed, got: %s", out) + } + if !strings.Contains(out, "sub") { + t.Fatalf("expected subfolder printed, got: %s", out) } } -func TestAPI_BulkDeleteFileHandler_InvalidJSON(t *testing.T) { - invalidJSON := `{"invalid": json}` +func TestRemoveFileOrderPreserving_BasicAndRecursive(t *testing.T) { + // top-level removal + root := &Folder{ + Name: "root", + Files: []*File{ + {Name: "one", Path: "/one"}, + {Name: "two", Path: "/two"}, + {Name: "three", Path: "/three"}, + }, + Subfolders: []*Folder{ + { + Name: "child", + Files: []*File{ + {Name: "c1", Path: "/child/c1"}, + }, + }, + }, + } - req := httptest.NewRequest("POST", "/bulkDeleteFiles?name=test", strings.NewReader(invalidJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFileHandler(w, req) + if err := root.RemoveFileOrderPreserving("/two"); err != nil { + t.Fatalf("unexpected error removing /two: %v", err) + } + if len(root.Files) != 2 { + t.Fatalf("expected 2 files after removal, got %d", len(root.Files)) + } + if root.Files[0].Path != "/one" || root.Files[1].Path != "/three" { + t.Fatalf("order incorrect after removal: %#v", root.Files) + } - if w.Code != http.StatusBadRequest { - t.Errorf("expected status 400, got %d", w.Code) + // recursive removal from subfolder + if err := root.RemoveFileOrderPreserving("/child/c1"); err != nil { + t.Fatalf("unexpected error removing child file: %v", err) + } + // ensure file removed from subfolder + if len(root.Subfolders[0].Files) != 0 { + t.Fatalf("expected subfolder files empty after removal, got %#v", root.Subfolders[0].Files) } - if got := w.Body.String(); !strings.Contains(got, "Invalid request body") { - t.Errorf("expected invalid request body error, got %s", got) + + // attempt to remove non-existent file + if err := root.RemoveFileOrderPreserving("/nope"); err == nil { + t.Fatalf("expected error when removing non-existent file") } } -func TestAPI_BulkDeleteFileHandler_NonExistentManager(t *testing.T) { - Composites = []*Folder{} +func TestPrintFileNodeChildren_SortsAndPrints(t *testing.T) { + // create some file nodes: folders and files + nodes := []FileNode{ + { + Name: "ZFolder", + IsFolder: true, + Children: []FileNode{{Name: "afile", IsFolder: false}}, + }, + { + Name: "afolder", + IsFolder: true, + Children: []FileNode{}, + }, + { + Name: "bfile.txt", + IsFolder: false, + }, + { + Name: "Afile.txt", + IsFolder: false, + }, + } - jsonBody := `[{"file_path": "/some/path"}]` + // shuffle to ensure sorting matters + sort.Slice(nodes, func(i, j int) bool { return i < j }) - req := httptest.NewRequest("POST", "/bulkDeleteFiles?name=nonexistent", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFileHandler(w, req) + out := captureOutput(func() { + printFileNodeChildren(nodes, "") + }) - if w.Code != http.StatusNotFound { - t.Errorf("expected status 404, got %d", w.Code) + // expect directories printed (afolder, ZFolder) and files (Afile.txt, bfile.txt) in case-insensitive order + if !strings.Contains(out, "afolder") { + t.Fatalf("expected afolder printed, got: %s", out) + } + if !strings.Contains(out, "ZFolder") { + t.Fatalf("expected ZFolder printed, got: %s", out) } - if got := w.Body.String(); !strings.Contains(got, "Files not found") { - t.Errorf("expected files not found error, got %s", got) + if !strings.Contains(out, "Afile.txt") || !strings.Contains(out, "bfile.txt") { + t.Fatalf("expected files printed, got: %s", out) } } -func TestAPI_BulkDeleteFileHandler_EmptyList(t *testing.T) { - // Setup minimal composite - testFolder := &Folder{ - Name: "emptyTest", - Path: "/test", - NewPath: "/test", +func TestSafeName(t *testing.T) { + tests := []struct { + in string + want string + }{ + {"hello", "hello"}, + {" spaced ", "spaced"}, + {"", ""}, + {" ", ""}, + } + for _, tt := range tests { + got := safeName(tt.in) + if got != tt.want { + t.Errorf("safeName(%q) = %q, want %q", tt.in, got, tt.want) + } } - Composites = []*Folder{testFolder} +} - jsonBody := `[]` +func TestJoinNonEmpty(t *testing.T) { + got := joinNonEmpty([]string{"a", " ", "", "b"}) + if got != "a, b" { + t.Errorf("joinNonEmpty = %q, want %q", got, "a, b") + } - req := httptest.NewRequest("POST", "/bulkDeleteFiles?name=emptyTest", strings.NewReader(jsonBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - BulkDeleteFileHandler(w, req) + got = joinNonEmpty([]string{" ", ""}) + if got != "" { + t.Errorf("joinNonEmpty with blanks = %q, want empty string", got) + } +} - if w.Code != http.StatusOK { - t.Errorf("expected status 200 for empty list, got %d", w.Code) +func TestNodeLabel(t *testing.T) { + tests := []struct { + node FileNode + want string + }{ + {FileNode{Name: "file.txt"}, "[FILE] file.txt"}, + {FileNode{Name: "folder", IsFolder: true}, "[DIR] folder"}, + {FileNode{Name: "f", Tags: []string{"tag1", " ", "tag2"}}, "[FILE] f [tags: tag1, tag2]"}, + {FileNode{Name: "locked", Locked: true}, "[FILE] locked [locked]"}, + } + for _, tt := range tests { + got := nodeLabel(tt.node) + if got != tt.want { + t.Errorf("nodeLabel(%+v) = %q, want %q", tt.node, got, tt.want) + } } } diff --git a/golang/filesystem/sortTree_test.go b/golang/filesystem/sortTree_test.go new file mode 100644 index 0000000..6734309 --- /dev/null +++ b/golang/filesystem/sortTree_test.go @@ -0,0 +1,53 @@ +package filesystem + +import ( + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" +) + +func TestSortTreeHandler_InvalidCase(t *testing.T) { + req := httptest.NewRequest("GET", "/sortTree?name=TestComp&case=INVALID", nil) + w := httptest.NewRecorder() + + sortTreeHandler(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected 400 for invalid case") + } + if !strings.Contains(w.Body.String(), "Invalid case type.") { + t.Errorf("expected error body, got: %s", w.Body.String()) + } +} + +func TestSortTreeHandler_ValidCaseSnake(t *testing.T) { + mu = sync.Mutex{} + Composites = []*Folder{} // nothing added + + req := httptest.NewRequest("GET", "/sortTree?name=TestComp&case=SNAKE", nil) + w := httptest.NewRecorder() + + sortTreeHandler(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected 400 BadRequest for missing composite, got %d", resp.StatusCode) + } +} + +func TestSortTreeHandler_NoSuchComposite(t *testing.T) { + + req := httptest.NewRequest("GET", "/sortTree?name=DoesNotExist", nil) + w := httptest.NewRecorder() + + sortTreeHandler(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected 400 for missing composite") + } + if !strings.Contains(w.Body.String(), "No smart manager with that name") { + t.Errorf("unexpected body: %s", w.Body.String()) + } +} diff --git a/golang/filesystem/tests/search_test.go b/golang/filesystem/tests/search_test.go index 1cc360b..83f3098 100644 --- a/golang/filesystem/tests/search_test.go +++ b/golang/filesystem/tests/search_test.go @@ -23,7 +23,7 @@ func TestLevenshteinDist(t *testing.T) { {"Report", "report", 0, "Case-insensitive exact match"}, // Substring match (should return low score) - {"jack", "jacks homework.txt", 2, "Substring match boosts score"}, + {"jack", "jacks homework.txt", 1, "Substring match boosts score"}, // Extensions ignored {"budget", "budget.pdf", 0, "Extension stripped - exact match"}, @@ -33,10 +33,10 @@ func TestLevenshteinDist(t *testing.T) { {"dog", "cat", 3, "Different strings"}, // Prefix match - {"home", "homework.txt", 1, "Prefix substring boosts"}, + {"home", "homework.txt", 0, "Prefix substring boosts"}, // Suffix match - {"work", "homework.txt", 1, "Suffix substring boosts"}, + {"work", "homework.txt", 0, "Suffix substring boosts"}, // Shorter query, longer file {"a", "abcdefgh.txt", 1, "Very short search in long filename"}, @@ -100,13 +100,11 @@ func TestSearchHandlerNotFound(t *testing.T) { filesystem.SearchHandler(rr, req) - if rr.Code != http.StatusBadRequest { t.Errorf("expected status 400; got %d", rr.Code) } } - // TestSearchHandlerEmpty tests that a known composite with no files returns empty children list func TestSearchHandlerEmpty(t *testing.T) { filesystem.Composites = []*filesystem.Folder{fakeFolder("test")} diff --git a/golang/go.mod b/golang/go.mod index 5b7ae5f..a57a46c 100644 --- a/golang/go.mod +++ b/golang/go.mod @@ -3,35 +3,14 @@ module github.com/COS301-SE-2025/Smart-File-Manager/golang go 1.24 require ( - github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect - github.com/bits-and-blooms/bitset v1.22.0 // indirect - github.com/blevesearch/bleve/v2 v2.5.2 // indirect - github.com/blevesearch/bleve_index_api v1.2.8 // indirect - github.com/blevesearch/geo v0.2.3 // indirect - github.com/blevesearch/go-faiss v1.0.25 // indirect - github.com/blevesearch/go-porterstemmer v1.0.3 // indirect - github.com/blevesearch/gtreap v0.1.1 // indirect - github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect - github.com/blevesearch/segment v0.9.1 // indirect - github.com/blevesearch/snowballstem v0.9.0 // indirect - github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.1.0 // indirect - github.com/blevesearch/zapx/v11 v11.4.2 // indirect - github.com/blevesearch/zapx/v12 v12.4.2 // indirect - github.com/blevesearch/zapx/v13 v13.4.2 // indirect - github.com/blevesearch/zapx/v14 v14.4.2 // indirect - github.com/blevesearch/zapx/v15 v15.4.2 // indirect - github.com/blevesearch/zapx/v16 v16.2.4 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect - github.com/mschoch/smat v0.2.0 // indirect - go.etcd.io/bbolt v1.4.0 // indirect + google.golang.org/grpc v1.72.1 + google.golang.org/protobuf v1.36.6 +) + +require ( + github.com/google/go-cmp v0.7.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/golang/go.sum b/golang/go.sum index 211b2df..49393c0 100644 --- a/golang/go.sum +++ b/golang/go.sum @@ -1,80 +1,34 @@ -github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= -github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= -github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= -github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/blevesearch/bleve/v2 v2.5.2 h1:Ab0r0MODV2C5A6BEL87GqLBySqp/s9xFgceCju6BQk8= -github.com/blevesearch/bleve/v2 v2.5.2/go.mod h1:5Dj6dUQxZM6aqYT3eutTD/GpWKGFSsV8f7LDidFbwXo= -github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y= -github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= -github.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg= -github.com/blevesearch/geo v0.2.3/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= -github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U= -github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= -github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= -github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= -github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= -github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= -github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= -github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.3.10 h1:Yqk0XD1mE0fDZAJXTjawJ8If/85JxnLd8v5vG/jWE/s= -github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8= -github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= -github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= -github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= -github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= -github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= -github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= -github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= -github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= -github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= -github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= -github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= -github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= -github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= -github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= -github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= -github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= -github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= -github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= -github.com/blevesearch/zapx/v16 v16.2.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww= -github.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede h1:YrgBGwxMRK0Vq0WSCWFaZUnTsrA/PZE/xs1QZh+/edg= -github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= -github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= -go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/golang/grpc/server/grpcServer.go b/golang/grpc/server/grpcServer.go index 5f196bd..8ee50a6 100644 --- a/golang/grpc/server/grpcServer.go +++ b/golang/grpc/server/grpcServer.go @@ -8,6 +8,9 @@ import ( "fmt" "log" "net" + "os" + "path/filepath" + "strings" pb "github.com/COS301-SE-2025/Smart-File-Manager/golang/client/protos" "google.golang.org/grpc" @@ -47,15 +50,72 @@ func (s *server) SendDirectoryStructure(ctx context.Context, in *pb.DirectoryReq return &pb.DirectoryResponse{Root: root}, nil } +func loadEnvFile(path string) (map[string]string, error) { + vars := make(map[string]string) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + // remove "export " if present + line = strings.TrimPrefix(line, "export ") + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + vars[parts[0]] = parts[1] + } + } + return vars, nil +} +func FindProjectRoot(filename string) (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + candidate := filepath.Join(dir, filename) + if _, err := os.Stat(candidate); err == nil { + return candidate, nil + } + + parent := filepath.Dir(dir) + if parent == dir { // reached filesystem root + break + } + dir = parent + } + + return "", fmt.Errorf("%s not found", filename) +} + func main() { - fmt.Println("running go server") - lis, err := net.Listen("tcp", ":50051") + fmt.Println("Starting go server...") + // Read port from environment + path, err := FindProjectRoot("server.env") + if err != nil { + log.Fatalf("failed to find project root: %v", err) + } + + env, err := loadEnvFile(path) if err != nil { - log.Fatalf("failed to listen on port 50051: %v", err) + log.Fatalf("failed to read env file: %v", err) + } + + port := env["PYTHON_SERVER"] + + lis, err := net.Listen("tcp", ":"+port) + if err != nil { + log.Fatalf("failed to listen on port %s: %v", port, err) } s := grpc.NewServer() pb.RegisterDirectoryServiceServer(s, &server{}) + log.Printf("gRPC server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) diff --git a/makefile b/makefile index df05b7f..125d830 100644 --- a/makefile +++ b/makefile @@ -38,10 +38,26 @@ go_grpc_client: go run grpc/client/grpcClient.go go_test: - @echo "Running filesystem tests..." - cd golang/filesystem && go test -tags=test -v - @echo "Running filesystem/tests..." - cd golang/filesystem/tests && go test -tags=test -v + @echo "Running all filesystem tests..." + cd golang && go test -tags=test ./filesystem/... -v + +go_coverage: + @cd golang && \ + go test -tags=test -coverpkg=./filesystem/... -coverprofile=coverage.out -covermode=atomic ./filesystem/... || true; \ + if [ -f coverage.out ]; then \ + echo "Coverage details:"; \ + go tool cover -func=coverage.out | sed -n '$p'; \ + echo -n "TOTAL COVERAGE: " ; go tool cover -func=coverage.out | awk '/^total:/ {print $$3}'; \ + else \ + echo "coverage.out not generated"; \ + fi + +go_coverage_funcs: + cd golang && \ + go test -tags=test -coverpkg=./filesystem/... -coverprofile=coverage.out -covermode=atomic ./filesystem/... && \ + echo "Coverage per function:" && \ + go tool cover -func=coverage.out + go_api: @@ -52,7 +68,7 @@ python: python3 python/src/main.py python_test: - pytest -v -s --color=yes --tb=short python/testing/ + pytest -vv -s --color=yes --tb=short python/testing/ python_test_pyinstrument: pyinstrument -r html -o profiling/profile_report.html -m pytest -v -s --color=yes --tb=short python/testing/ @@ -87,5 +103,6 @@ python_master_temp: python_locked_temp: pytest -v -s --color=yes --tb=short python/testing/test_locked_request.py -python_fn_temp: - pytest -v -s --color=yes --tb=short python/testing/test_folder_name_creator.py +python_non_functional: + pytest -v -s --color=yes --tb=short python/non_functional_tests/ + diff --git a/protos/message_structure.proto b/protos/message_structure.proto index 5fd2cee..b85c3d8 100644 --- a/protos/message_structure.proto +++ b/protos/message_structure.proto @@ -43,6 +43,9 @@ message Directory { message DirectoryRequest { Directory root = 1; string requestType = 2; + + string preferredCase = 3; + string serverSecret = 4; } // Python to go @@ -54,4 +57,4 @@ message DirectoryResponse { service DirectoryService { rpc SendDirectoryStructure(DirectoryRequest) returns (DirectoryResponse); -} +} \ No newline at end of file diff --git a/python/non_functional_tests/conftest.py b/python/non_functional_tests/conftest.py new file mode 100644 index 0000000..74e14d4 --- /dev/null +++ b/python/non_functional_tests/conftest.py @@ -0,0 +1,27 @@ +# tests/conftest.py +import os +import sys +import pytest +from concurrent import futures +import grpc + +# ensure your `src/` is on the import path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from src import message_structure_pb2_grpc +from src.request_handler import RequestHandler + + +@pytest.fixture(scope="session") +def grpc_test_server(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + message_structure_pb2_grpc.add_DirectoryServiceServicer_to_server(RequestHandler(), server) + port = server.add_insecure_port("localhost:0") # OS assigns free port + server.start() + + channel = grpc.insecure_channel(f"localhost:{port}") + stub = message_structure_pb2_grpc.DirectoryServiceStub(channel) + + yield stub # This is used in the test function + + server.stop(None) diff --git a/python/non_functional_tests/test_performance_clustering.py b/python/non_functional_tests/test_performance_clustering.py new file mode 100644 index 0000000..a781e83 --- /dev/null +++ b/python/non_functional_tests/test_performance_clustering.py @@ -0,0 +1,45 @@ +import pytest +import sys +import os +import time +from typing import List + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from src.message_structure_pb2 import Directory, File, DirectoryRequest + + +def create_real_files(tmp_path, num_files: int) -> List[File]: + """Create real dummy text files and return as gRPC File objects.""" + files: List[File] = [] + for i in range(num_files): + file_path = tmp_path / f"file_{i}.txt" + file_path.write_text(f"This is dummy file {i}\n" * 5) + files.append(File(name=file_path.name, original_path=str(file_path), tags=[])) + return files + + +def make_request(request_type: str, files: List[File], test_dir: str) -> DirectoryRequest: + root = Directory(name="scalability_test", path=test_dir, files=files) + return DirectoryRequest( + root=root, + requestType=request_type, + serverSecret=os.environ["SFM_SERVER_SECRET"] + ) + + +@pytest.mark.parametrize("num_files", [10, 50, 100, 200, 400]) +def test_scalability_clustering(grpc_test_server, num_files, tmp_path): + """Benchmark clustering request with increasing number of real files.""" + files = create_real_files(tmp_path, num_files) + req = make_request("CLUSTERING", files, str(tmp_path)) + + start = time.perf_counter() + response = grpc_test_server.SendDirectoryStructure(req) + duration = time.perf_counter() - start + + print(f"[CLUSTERING] {num_files} files -> {duration:.4f} seconds") + + assert response.response_code == 200 + assert response.response_msg != "No file could be opened" + assert len(response.root.files) == num_files diff --git a/python/non_functional_tests/test_performance_keywords.py b/python/non_functional_tests/test_performance_keywords.py new file mode 100644 index 0000000..29155a3 --- /dev/null +++ b/python/non_functional_tests/test_performance_keywords.py @@ -0,0 +1,49 @@ +import pytest +import sys +import os +import time +from typing import List + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from src.message_structure_pb2 import Directory, File, DirectoryRequest + + +def create_real_files(tmp_path, num_files: int) -> List[File]: + """Create real dummy text files with content for keyword extraction.""" + files: List[File] = [] + for i in range(num_files): + file_path = tmp_path / f"file_{i}.txt" + file_path.write_text( + f"This is file {i}. It contains some test text for keyword extraction. " + f"Keywords like project, data, clustering, testing, analysis.\n" + ) + files.append(File(name=file_path.name, original_path=str(file_path))) + return files + + +def make_request(request_type: str, files: List[File], test_dir: str) -> DirectoryRequest: + """Helper to build a DirectoryRequest with real files.""" + root = Directory(name="scalability_test", path=test_dir, files=files) + return DirectoryRequest( + root=root, + requestType=request_type, + serverSecret=os.environ["SFM_SERVER_SECRET"] + ) + + +@pytest.mark.parametrize("num_files", [10, 50, 100, 200]) +def test_scalability_keywords(grpc_test_server, num_files, tmp_path): + """Benchmark keywords request with increasing number of real files.""" + files = create_real_files(tmp_path, num_files) + req = make_request("KEYWORDS", files, str(tmp_path)) + + start = time.perf_counter() + response = grpc_test_server.SendDirectoryStructure(req) + duration = time.perf_counter() - start + + print(f"[KEYWORDS] {num_files} files -> {duration:.4f} seconds") + + assert response.response_code == 200 + assert response.response_msg != "No file could be opened" + assert len(response.root.files) == num_files diff --git a/python/non_functional_tests/test_reliability.py b/python/non_functional_tests/test_reliability.py new file mode 100644 index 0000000..36d116e --- /dev/null +++ b/python/non_functional_tests/test_reliability.py @@ -0,0 +1,135 @@ +import pytest +import sys +import os +import time +import hashlib + +# Add src to path temporarily so the generated grpc file can find message_structure_pb2 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from src.message_structure_pb2 import Directory, File, Tag, DirectoryRequest + +def file_hash(path: str) -> str: + """Calculate SHA256 hash of a file.""" + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + +# ---------- FIXTURES ---------- +@pytest.fixture(scope="module") +def createDirectoryRequest(): + TEST_DIR = os.path.dirname(__file__) + TEST_FILE_DIR = os.path.join(TEST_DIR, "../testing/test_files_3") + + def get_path(name): + return os.path.join(TEST_FILE_DIR, name) + + files1 = [ + File(name="Apr8TODO.txt", original_path=get_path("Apr8TODO.txt"), tags=[]), + File(name="Apr18 meeting.txt", original_path=get_path("Apr18 meeting.txt"), tags=[] ), + File(name="architecture_diagram.png", original_path=get_path("architecture_diagram.png"), tags=[]), + File(name="Assignment2.pdf", original_path=get_path("Assignment2.pdf"), tags=[]), + File(name="collection_page_wireframe.png", original_path=get_path("collection_page_wireframe.png"), tags=[]), + File(name="COS 301 - Mini-Project - Demo 1 Instructions.pdf", original_path=get_path("COS 301 - Mini-Project - Demo 1 Instructions.pdf"), tags=[]), + File(name="COS 301 - Mini-Project - Demo 2 Instructions.pdf", original_path=get_path("COS 301 - Mini-Project - Demo 2 Instructions.pdf"), tags=[]), + File(name="COS122 Tutorial 4 Sept 7-8, 2023.pdf", original_path=get_path("COS122 Tutorial 4 Sept 7-8, 2023.pdf"), tags=[]), + File(name="COS221 Assignment 1 2025.pdf", original_path=get_path("COS221 Assignment 1 2025.pdf"), tags=[]), + File(name="cpp_api.md", original_path=get_path("cpp_api.md"), tags=[]), + File(name="DeeBee.png", original_path=get_path("DeeBee.png"), tags=[]), + File(name="Importing the Database.md", original_path=get_path("Importing the Database.md"), tags=[]), + File(name="L01_Ch01a(1).pdf", original_path=get_path("L01_Ch01a(1).pdf"), tags=[]), + File(name="L05_Ch02c.pdf", original_path=get_path("L05_Ch02c.pdf"), tags=[]), + File(name="login_wireframe.png", original_path=get_path("login_wireframe.png"), tags=[]), + File(name="MP Progress report.txt", original_path=get_path("MP Progress report.txt"), tags=[]), + File(name="mp11_design_specification.md", original_path=get_path("mp11_design_specification.md"), tags=[]), + File(name="mp11_requirement_spec.md", original_path=get_path("mp11_requirement_spec.md"), tags=[]), + File(name="MPChecklist.txt", original_path=get_path("MPChecklist.txt"), tags=[]), + File(name="Prac1Triggers.txt", original_path=get_path("Prac1Triggers.txt"), tags=[]), + File(name="Screenshot_2025-02-26_at_15.36.48.png", original_path=get_path("Screenshot_2025-02-26_at_15.36.48.png"), tags=[]), + File(name="statistics_page_wireframe.png", original_path=get_path("statistics_page_wireframe.png"), tags=[]), + File(name="TODO mar30 Meeting.txt", original_path=get_path("TODO mar30 Meeting.txt"), tags=[]), + File(name="Tututorial_2.pdf", original_path=get_path("Tututorial_2.pdf"), tags=[]), + File(name="UseCase.png", original_path=get_path("UseCase.png"), tags=[]), + File(name="~$ecutive summary", original_path=get_path("~$ecutive summary.docx"), tags=[]), + File(name="~WRL0005.tmp", original_path=get_path("~WRL0005.tmp"), tags=[]), + File(name="~WRL1847.tmp", original_path=get_path("~WRL1847.tmp"), tags=[]), + File(name="3.6.4 Survey data to be analysed and visualised", original_path=get_path("3.6.4 Survey data to be analysed and visualised for project report mine.xlsx"), tags=[]), + File(name="Document[1]", original_path=get_path("Document[1].pdf"), tags=[]), + File(name="ENjoyment", original_path=get_path("ENjoyment.png"), tags=[]), + File(name="Gantt chart", original_path=get_path("Gantt chart.png"), tags=[]), + File(name="Gauteng", original_path=get_path("Gauteng.png"), tags=[]), + File(name="most challanging", original_path=get_path("most challanging.png"), tags=[]), + File(name="Most rewarding", original_path=get_path("Most rewarding.png"), tags=[]), + File(name="Picture1", original_path=get_path("Picture1.png"), tags=[]), + File(name="Picture2", original_path=get_path("Picture2.png"), tags=[]), + File(name="Presentation speech", original_path=get_path("Presentation speech.docx"), tags=[]), + File(name="Project Budget Form 2024", original_path=get_path("Project Budget Form 2024.pdf"), tags=[]), + File(name="Taiichi ohno", original_path=get_path("Taiichi ohno.jpeg"), tags=[]), + File(name="Week 3_Tutorial_2024_with Answers", original_path=get_path("Week 3_Tutorial_2024_with Answers.pdf"), tags=[]), + File(name="Week 4_Tutorial_with answers", original_path=get_path("Week 4_Tutorial_with answers.pdf"), tags=[]), + File(name="Week 5_Tutorial_2024_with answers", original_path=get_path("Week 5_Tutorial_2024_with answers.pdf"), tags=[]), + File(name="Form", original_path=get_path("Main.form"), tags=[]), + File(name="Main", original_path=get_path("Main.java"), tags=[]), + File(name="astro_1", original_path=get_path("astro_1.pdf"), tags=[]), + File(name="astro_2", original_path=get_path("astro_2.pdf"), tags=[]), + File(name="astro_3", original_path=get_path("astro_3.pdf"), tags=[]), + File(name="astro_4", original_path=get_path("astro_4.pdf"), tags=[]), + File(name="astro_5", original_path=get_path("astro_5.pdf"), tags=[]), + File(name="astro_6", original_path=get_path("astro_6.pdf"), tags=[]), + File(name="econ_1", original_path=get_path("econ_1.pdf"), tags=[]), + File(name="econ_2", original_path=get_path("econ_2.pdf"), tags=[]), + File(name="econ_3", original_path=get_path("econ_3.pdf"), tags=[]), + File(name="econ_4", original_path=get_path("econ_4.pdf"), tags=[]), + File(name="econ_5", original_path=get_path("econ_5.pdf"), tags=[]), + File(name="ee_1", original_path=get_path("ee_1.pdf"), tags=[]), + File(name="ee_2", original_path=get_path("ee_2.pdf"), tags=[]), + File(name="ee_3", original_path=get_path("ee_3.pdf"), tags=[]), + File(name="ee_4", original_path=get_path("ee_4.pdf"), tags=[]), + File(name="ee_5", original_path=get_path("ee_5.pdf"), tags=[]), + File(name="math_1", original_path=get_path("math_1.pdf"), tags=[]), + File(name="math_2", original_path=get_path("math_2.pdf"), tags=[]), + File(name="math_3", original_path=get_path("math_3.pdf"), tags=[]), + File(name="math_4", original_path=get_path("math_4.pdf"), tags=[]), + File(name="math_5", original_path=get_path("math_5.pdf"), tags=[]) + ] + + root_dir = Directory( + name="test_files_3", + path=get_path("test_files_3"), + files=files1 + ) + + req = DirectoryRequest( + root=root_dir, + requestType="CLUSTERING", + serverSecret=os.environ["SFM_SERVER_SECRET"], + preferredCase="SPACE" + ) + yield req + +# ---------- RELIABILITY TEST ---------- +def test_file_integrity_during_sort(grpc_test_server, createDirectoryRequest): + req = createDirectoryRequest + + pre_hashes = {} + for f in req.root.files: + pre_hashes[f.name] = file_hash(f.original_path) + + start_time = time.time() + response = grpc_test_server.SendDirectoryStructure(req) + end_time = time.time() + print(f"SendDirectoryStructure took {end_time - start_time:.3f}s\n") + + post_hashes = {} + for f in req.root.files: + post_hashes[f.name] = file_hash(f.original_path) + + print(f"{'File Name':<50} {'Before Hash':<64} {'After Hash':<64} {'Status'}") + print("="*190) + for name in pre_hashes: + status = "OK" if pre_hashes[name] == post_hashes[name] else "MODIFIED" + print(f"{name:<50} {pre_hashes[name]:<64} {post_hashes[name]:<64} {status}") + assert pre_hashes[name] == post_hashes[name], f"File {name} was modified during processing!" diff --git a/python/src/config.py b/python/src/config.py new file mode 100644 index 0000000..3278c60 --- /dev/null +++ b/python/src/config.py @@ -0,0 +1,7 @@ +import os + +# Load secrets from env +try: + SERVER_SECRET = os.environ['SFM_SERVER_SECRET'] +except KeyError: + SERVER_SECRET = None \ No newline at end of file diff --git a/python/src/create_folder_name.py b/python/src/create_folder_name.py index 48e2bc6..8b8be1d 100644 --- a/python/src/create_folder_name.py +++ b/python/src/create_folder_name.py @@ -1,42 +1,40 @@ import os -import re from pathlib import Path -from sklearn.metrics.pairwise import cosine_similarity -import numpy as np -import nltk +import re +from typing import Dict +import unicodedata +from collections import Counter, defaultdict +import nltk from nltk.corpus import wordnet from nltk import pos_tag from nltk.stem import WordNetLemmatizer -#adding these imports could be slow -from typing import Dict -from collections import Counter - -from collections import defaultdict - class FolderNameCreator: - def __init__(self, model): + def __init__(self, model, case_convention: str): self.model = model - self.max_keywords = 200 # for folder name creation + self.max_keywords = 200 + self.foldername_length = 2 self.lemmatizer = WordNetLemmatizer() - # Weighting of different vars + + # Weighting self.weights = { "keywords":0.9, # If there are keywords they should really be different to not be together - "filename":0.002, # Should only make a small difference compared to keywrods (when they are used) + "filename":1.0, # Should only make a small difference compared to keywrods (when they are used) "tags":1.5, # Even though they should already be weighted significantly "original_parent":0.01, # Metadata which can be considered "created":0.5 } - self.foldername_length = 2 - self.filename_scores = {} - self.parent_name_scores = {} - self.keyword_scores = {} - self._ensure_nltk_data() + supported_cases = ["CAMEL", "SNAKE", "PASCAL", "KEBAB", "SPACE"] + cleaned_case_convention = case_convention.strip().upper() + self.case_convention = ( + cleaned_case_convention if cleaned_case_convention in supported_cases else "CAMEL" + ) + self._ensure_nltk_data() def _ensure_nltk_data(self): try: @@ -50,141 +48,84 @@ def _ensure_nltk_data(self): nltk.download("wordnet", quiet=True) nltk.download("omw-1.4", quiet=True) - # Remove all types of extensions - .png, .tar.gz, etc. - def remove_all_extensions(self,filename : str) -> str: - while True: - filename, ext = os.path.splitext(filename) - if not ext: - break - return filename - + # ---------- CLEANING ---------- + def _normalize_text(self, text: str) -> str: + """Normalize unicode, strip accents, and lowercase""" + text = unicodedata.normalize("NFKD", text) + text = text.encode("ascii", "ignore").decode("utf-8", "ignore") + return text.lower().strip() + + def _split_words(self, text: str): + """Split on camelCase, PascalCase, snake_case, kebab-case, dots, numbers""" + text = re.sub(r"([a-z])([A-Z])", r"\1 \2", text) # camelCase -> camel Case + text = re.sub(r"[^a-zA-Z0-9]+", " ", text) # Discard non alpha numeric + return [w for w in text.split() if w] + # ---------- CORE ---------- def generateFolderName(self, files): - # Try to generate name based on most common tag or keyword - tags = [] - keywords = [] + tags, keywords = [], [] for file in files: tags.extend(file.get("tags", [])) keywords.extend([kw for kw, _ in file.get("keywords", [])]) - # Use most common tag if available + candidates = [] if tags: - tag_names = [tag.lower().strip() for tag in tags if isinstance(tag, str)] - most_common_tag, _ = Counter(tag_names).most_common(1)[0] - return self._clean_name(most_common_tag) - - # Else, fallback to most common keyword - if keywords: - keyword_names = [kw.lower().strip() for kw in keywords if isinstance(kw, str)] - most_common_kw, _ = Counter(keyword_names).most_common(1)[0] - return self._clean_name(most_common_kw) - - # Final fallback - return "Group" - - def _clean_name(self, name: str) -> str: - # Remove non-alphanumeric characters and collapse spaces - name = re.sub(r'[^a-zA-Z0-9\s]', '', name) - name = re.sub(r'\s+', '_', name).strip('_') - name = name.lower() - - # Truncate to avoid absurdly long names - return name[:30] if name else "Group" - - - def assignParentScores(self, absolute_path : str, parent_name_scores : Dict[str,float]) -> None: - path = Path(absolute_path) - parents = list(path.parents) - depth = 1 - - for parent in parents: - name = parent.name.lower() - if name in self.keyword_scores or name in self.filename_scores: - continue - if name == "": - continue - if name not in parent_name_scores: - parent_name_scores[name] = 0 - parent_name_scores[name] += self.weights["original_parent"] ** depth - depth += 1 - - - def combine_lists(self, keywords : Dict[str, float], filenames : Dict[str,float], parent_names:Dict[str,float]) -> Dict[str,float]: - scores = defaultdict(float) - - for kw,score in keywords: - scores[kw] += score - - for fn,score in filenames: - scores[fn] += score - - for pn,score in parent_names: - scores[pn] += score - - return sorted(scores.items(), key=lambda x: -x[1]) - - - def getRepresentativeKeywords(self, scores : Dict[str, float]): - if not scores: - return [] - - # Top weighted keywrods - sorted_keywords = sorted(scores.items(), key=lambda x: x[1], reverse=True) - top_keywords = [kw for kw, _ in sorted_keywords[:self.max_keywords]] - - # Encode file names with sentence transformer - embeddings = self.model.encode(top_keywords) - centroid = np.mean(embeddings, axis=0, keepdims=True) - - # Find most representative name (closest to centroid) - sims = cosine_similarity(centroid, embeddings).flatten() - best_idx = sims.argsort()[-self.foldername_length:][::-1] - - # folder_keyword = folder_names[best_idx] - folder_keyword = [top_keywords[i] for i in best_idx] + most_common_tag, _ = Counter(map(self._normalize_text, tags)).most_common(1)[0] + candidates = [most_common_tag] + elif keywords: + most_common_kw, _ = Counter(map(self._normalize_text, keywords)).most_common(1)[0] + candidates = [most_common_kw] + + if not candidates: + candidates = ["misc"] + + # Lemmatize kws separately + words = self.lemmatize(candidates) + + # Flatten list of lists into one word sequence + flat_words = [w for group in words for w in group] + + return self.format_case(flat_words) + + def lemmatize(self, raw_keywords): + """Return list of lists: each keyword -> list of lemma words""" + results = [] + for kw in raw_keywords: + tokens = self._split_words(kw) + tagged = pos_tag(tokens) + lemmas = [] + for word, tag in tagged: + wn_tag = self.get_wordnet_pos(tag) + lemma = self.lemmatizer.lemmatize(word, pos=wn_tag) + lemmas.append(lemma) + if lemmas: + results.append(lemmas) + return results or [["misc"]] - return [(word, scores[word]) for word in folder_keyword] - - # Helper function to map NLTK POS to WordNet POS def get_wordnet_pos(self, treebank_tag): - if treebank_tag.startswith('J'): + if treebank_tag.startswith("J"): return wordnet.ADJ - elif treebank_tag.startswith('V'): + elif treebank_tag.startswith("V"): return wordnet.VERB - elif treebank_tag.startswith('N'): + elif treebank_tag.startswith("N"): return wordnet.NOUN - elif treebank_tag.startswith('R'): + elif treebank_tag.startswith("R"): return wordnet.ADV - else: - return wordnet.NOUN # fallback to noun - - - def lemmatize(self, folder_keyword): - seen = set() - normalized_keywords = [] - for kw in folder_keyword: - words = re.split(r'[\s\._\-]+', kw.lower()) - # Tag parts of speech - tagged = pos_tag(words) - for word, tag in tagged: - wn_tag = self.get_wordnet_pos(tag) - lemma = self.lemmatizer.lemmatize(word, pos=wn_tag) - if lemma not in seen: - seen.add(lemma) - normalized_keywords.append(lemma) - return normalized_keywords - - - def lemmatize_with_scores(self, folder_keywords_with_scores): - seen = {} - for kw, score in folder_keywords_with_scores: - words = re.split(r'[\s\._\-]+', kw.lower()) - tagged = pos_tag(words) - for word, tag in tagged: - wn_tag = self.get_wordnet_pos(tag) - lemma = self.lemmatizer.lemmatize(word, pos=wn_tag) - if lemma not in seen or score > seen[lemma]: - seen[lemma] = score - return sorted(seen.items(), key=lambda x: -x[1]) + return wordnet.NOUN + + def format_case(self, words): + if not words: + return "misc" + + if self.case_convention == "CAMEL": + return words[0].lower() + "".join(w.capitalize() for w in words[1:]) + if self.case_convention == "SNAKE": + return "_".join(w.lower() for w in words) + if self.case_convention == "PASCAL": + return "".join(w.capitalize() for w in words) + if self.case_convention == "KEBAB": + return "-".join(w.lower() for w in words) + if self.case_convention == "SPACE": + return " ".join(w.capitalize() for w in words) \ No newline at end of file diff --git a/python/src/full_vector.py b/python/src/full_vector.py index 8a4908d..57ea624 100644 --- a/python/src/full_vector.py +++ b/python/src/full_vector.py @@ -5,13 +5,24 @@ class FullVector: - def __init__(self, transformer): + def __init__(self, transformer, weights=None): self.scaler_size = MinMaxScaler() self.scaler_created = MinMaxScaler() self.model = transformer + if weights == None: + self.weights = { + "size_bytes": 1, + "keywords":2, + "created":1, + "tags":4, + "filename":5 + + } + else: + self.weights = weights def create_full_vector(self, files: List[Dict]) -> None: - features = ["size_bytes", "keywords", "created", "tags"] + features = ["size_bytes", "keywords", "created", "tags", "filename"] feature_data = self._gather_feature_data(files, features) # Normalize numerical features @@ -53,6 +64,10 @@ def create_full_vector(self, files: List[Dict]) -> None: idx += 1 kw_vectors_per_file.append(np.mean(vectors, axis=0)) + #Filename vectors + all_filenames = feature_data["filename"] + + filename_vectors = self.model.encode(all_filenames, show_progress_bar=False, batch_size=64) # Construct final vectors embedding_dim = self.model.get_sentence_embedding_dimension() @@ -63,11 +78,16 @@ def create_full_vector(self, files: List[Dict]) -> None: else [0.0] * embedding_dim ) - weighted_tags = np.array(tag_vectors[i]) * 3 + weighted_kw_vec = (np.array(kw_vec) * self.weights["keywords"]).tolist() + fn_vec = (np.array(filename_vectors[i]) * self.weights["filename"]).tolist() + weighted_created = (normalized_created[i]) * self.weights["created"] + weighted_sizes = (normalized_sizes[i]) * self.weights["size_bytes"] + weighted_tags = np.array(tag_vectors[i]) * self.weights["tags"] full_vector = ( - kw_vec + - [normalized_created[i]] + - [normalized_sizes[i]] + + weighted_kw_vec + + fn_vec + + [weighted_created] + + [weighted_sizes] + weighted_tags.tolist() ) diff --git a/python/src/k_means.py b/python/src/k_means.py index 83d7687..5a48a07 100644 --- a/python/src/k_means.py +++ b/python/src/k_means.py @@ -15,11 +15,11 @@ np.random.seed(42) class KMeansCluster: - def __init__(self, numClusters, max_depth, model, parent_folder): + def __init__(self, numClusters, max_depth, model, parent_folder, case_convention : str): self.base_clusters = numClusters self.min_size = 2 # hardcoded for now # even numbers are good self.max_depth = max_depth - self.folder_namer = FolderNameCreator(model) + self.folder_namer = FolderNameCreator(model, case_convention) self.parent_folder = parent_folder self.locked_files = [] diff --git a/python/src/main.py b/python/src/main.py index 5ff0285..ae210a0 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,6 +1,13 @@ import request_handler +import config def main(): + + # Check server secret setup + if config.SERVER_SECRET == None: + print("Server secret not found. Cancelling Server Startup...") + return + # Start the gRPC server handler = request_handler.RequestHandler() handler.serve() diff --git a/python/src/master.py b/python/src/master.py index 1538bc8..e2fd0af 100644 --- a/python/src/master.py +++ b/python/src/master.py @@ -8,6 +8,7 @@ import os from k_means import KMeansCluster +import config import time # Master class @@ -15,11 +16,11 @@ # Takes submitted gRPC requests and assigns them to a slave for processing before returning the response class Master(): - def __init__(self, maxSlaves, transformer): + def __init__(self, maxSlaves, transformer, weights: dict): self.slaves = ThreadPoolExecutor(maxSlaves) self.scraper = MetaDataScraper() self.kw_extractor = KWExtractor() - self.full_vec = FullVector(transformer) + self.full_vec = FullVector(transformer,None) # Takes gRPC request's root and sends it to be processed by a slave def submit_task(self, request : DirectoryRequest): @@ -30,6 +31,14 @@ def submit_task(self, request : DirectoryRequest): # Handles request appropriately def process(self, request : DirectoryRequest) -> DirectoryResponse: + # Validate server secret + request_secret = request.serverSecret + if(request_secret != config.SERVER_SECRET): + response = DirectoryResponse() + response.response_code = 401 + response.response_msg = "Unauthorized: Incorrect server secret" + return response + # Map request type to method and call requestHandler = { "CLUSTERING" : self.handle_clustering_request, @@ -44,10 +53,10 @@ def process(self, request : DirectoryRequest) -> DirectoryResponse: if not handler == None: return handler(request) else: - reponse = DirectoryResponse() - reponse.response_code = 400 - reponse.response_msg = "Unknown Request type: Must be in [CLUSTERING, METADATA, KEYWORDS]" - return reponse + response = DirectoryResponse() + response.response_code = 400 + response.response_msg = "Unknown Request type: Must be in [CLUSTERING, METADATA, KEYWORDS]" + return response def handle_clustering_request(self, request : DirectoryRequest) -> DirectoryResponse: @@ -73,7 +82,7 @@ def handle_clustering_request(self, request : DirectoryRequest) -> DirectoryResp print("Full vectors appended: " + str(self.full_vector_time_2 - self.start_time)) # Recursively cluster and return a directory - kmeans = KMeansCluster(int(len(full_vecs) / 3 ), 10, self.full_vec.model, request.root.name) + kmeans = KMeansCluster(int(len(full_vecs) / 3 ), 10, self.full_vec.model, request.root.name, request.preferredCase) response_directory = kmeans.dirCluster(full_vecs,files) self.clustering_time = time.time() print("Clustering complete: " + str(self.clustering_time - self.start_time)) diff --git a/python/src/request_handler.py b/python/src/request_handler.py index 6e7520e..030bc9c 100644 --- a/python/src/request_handler.py +++ b/python/src/request_handler.py @@ -1,4 +1,5 @@ from concurrent import futures +import threading from sentence_transformers import SentenceTransformer import grpc import message_structure_pb2_grpc @@ -13,18 +14,34 @@ class RequestHandler(message_structure_pb2_grpc.DirectoryServiceServicer): def __init__(self): # Early initialize sentence transformer transformer = SentenceTransformer('all-MiniLM-L6-v2') - self.master = master.Master(10, transformer) - + # If weights are none then they use a default value + self.master = master.Master(10, transformer, None) def SendDirectoryStructure(self, request, context): response = self.master.submit_task(request).result() return response def serve(self): - port = "50051" server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) message_structure_pb2_grpc.add_DirectoryServiceServicer_to_server(self, server) - server.add_insecure_port("[::]:" + port) + port = server.add_insecure_port("0.0.0.0:0") + server.start() - print("Server started, listening on " + port) - server.wait_for_termination() + print(f"export PYTHON_SERVER={port}") + with open("server.env", "w") as f: + f.write(f"PYTHON_SERVER={port}\n") + + print("Server ready and listening...") + + # Background thread listens for shutdown + def wait_for_stop(): + while True: + cmd = input().strip().upper() + if cmd == "STOP": + print("Stopping server...") + server.stop(grace=5) + break + + threading.Thread(target=wait_for_stop, daemon=True).start() + + server.wait_for_termination() \ No newline at end of file diff --git a/python/testing/conftest.py b/python/testing/conftest.py index 74e14d4..31298de 100644 --- a/python/testing/conftest.py +++ b/python/testing/conftest.py @@ -5,7 +5,6 @@ from concurrent import futures import grpc -# ensure your `src/` is on the import path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) from src import message_structure_pb2_grpc diff --git a/python/testing/test_clustering_request.py b/python/testing/test_clustering_request.py index c69f449..b1e8655 100644 --- a/python/testing/test_clustering_request.py +++ b/python/testing/test_clustering_request.py @@ -66,7 +66,28 @@ def get_path(name): File(name="Week 4_Tutorial_with answers", original_path=get_path("Week 4_Tutorial_with answers.pdf"), tags=[]), File(name="Week 5_Tutorial_2024_with answers", original_path=get_path("Week 5_Tutorial_2024_with answers.pdf"), tags=[]), File(name="Form", original_path=get_path("Main.form"), tags=[]), - File(name="Main", original_path=get_path("Main.java"), tags=[]) + File(name="Main", original_path=get_path("Main.java"), tags=[]), + File(name="astro_1", original_path=get_path("astro_1.pdf"), tags=[]), + File(name="astro_2", original_path=get_path("astro_2.pdf"), tags=[]), + File(name="astro_3", original_path=get_path("astro_3.pdf"), tags=[]), + File(name="astro_4", original_path=get_path("astro_4.pdf"), tags=[]), + File(name="astro_5", original_path=get_path("astro_5.pdf"), tags=[]), + File(name="astro_6", original_path=get_path("astro_6.pdf"), tags=[]), + File(name="econ_1", original_path=get_path("econ_1.pdf"), tags=[]), + File(name="econ_2", original_path=get_path("econ_2.pdf"), tags=[]), + File(name="econ_3", original_path=get_path("econ_3.pdf"), tags=[]), + File(name="econ_4", original_path=get_path("econ_4.pdf"), tags=[]), + File(name="econ_5", original_path=get_path("econ_5.pdf"), tags=[]), + File(name="ee_1", original_path=get_path("ee_1.pdf"), tags=[]), + File(name="ee_2", original_path=get_path("ee_2.pdf"), tags=[]), + File(name="ee_3", original_path=get_path("ee_3.pdf"), tags=[]), + File(name="ee_4", original_path=get_path("ee_4.pdf"), tags=[]), + File(name="ee_5", original_path=get_path("ee_5.pdf"), tags=[]), + File(name="math_1", original_path=get_path("math_1.pdf"), tags=[]), + File(name="math_2", original_path=get_path("math_2.pdf"), tags=[]), + File(name="math_3", original_path=get_path("math_3.pdf"), tags=[]), + File(name="math_4", original_path=get_path("math_4.pdf"), tags=[]), + File(name="math_5", original_path=get_path("math_5.pdf"), tags=[]) ] # Some tags for the files @@ -81,7 +102,7 @@ def get_path(name): files = files1 ) - req = DirectoryRequest(root=root_dir, requestType="CLUSTERING") + req = DirectoryRequest(root=root_dir, requestType="CLUSTERING", serverSecret=os.environ["SFM_SERVER_SECRET"], preferredCase = "SPACE") yield req @@ -113,8 +134,14 @@ def test_send_real_dir(grpc_test_server, createDirectoryRequest): # print("Second took: " + str(end_second - start_second)) # Check if response contains all files - assert responseSizeChecker(response.root) == 45 + assert responseSizeChecker(response.root) == 66 # Check if response is well formed assert response.response_code == 200 assert response.response_msg != "No file could be opened" + +def test_invalid_credential_req(grpc_test_server): + req = DirectoryRequest(root=None, requestType = "CLUSTERING", serverSecret = "wrongSecret") + response = grpc_test_server.SendDirectoryStructure(req) + assert response.response_code == 401 + assert response.response_msg == "Unauthorized: Incorrect server secret" diff --git a/python/testing/test_files_3/astro_1.pdf b/python/testing/test_files_3/astro_1.pdf new file mode 100644 index 0000000..15a366b Binary files /dev/null and b/python/testing/test_files_3/astro_1.pdf differ diff --git a/python/testing/test_files_3/astro_2.pdf b/python/testing/test_files_3/astro_2.pdf new file mode 100644 index 0000000..07e9c97 Binary files /dev/null and b/python/testing/test_files_3/astro_2.pdf differ diff --git a/python/testing/test_files_3/astro_3.pdf b/python/testing/test_files_3/astro_3.pdf new file mode 100644 index 0000000..74703c1 Binary files /dev/null and b/python/testing/test_files_3/astro_3.pdf differ diff --git a/python/testing/test_files_3/astro_4.pdf b/python/testing/test_files_3/astro_4.pdf new file mode 100644 index 0000000..f520b4d Binary files /dev/null and b/python/testing/test_files_3/astro_4.pdf differ diff --git a/python/testing/test_files_3/astro_5.pdf b/python/testing/test_files_3/astro_5.pdf new file mode 100644 index 0000000..14295ff Binary files /dev/null and b/python/testing/test_files_3/astro_5.pdf differ diff --git a/python/testing/test_files_3/astro_6.pdf b/python/testing/test_files_3/astro_6.pdf new file mode 100644 index 0000000..26c7680 Binary files /dev/null and b/python/testing/test_files_3/astro_6.pdf differ diff --git a/python/testing/test_files_3/econ_1.pdf b/python/testing/test_files_3/econ_1.pdf new file mode 100644 index 0000000..aa8e7a5 Binary files /dev/null and b/python/testing/test_files_3/econ_1.pdf differ diff --git a/python/testing/test_files_3/econ_2.pdf b/python/testing/test_files_3/econ_2.pdf new file mode 100644 index 0000000..61a49be Binary files /dev/null and b/python/testing/test_files_3/econ_2.pdf differ diff --git a/python/testing/test_files_3/econ_3.pdf b/python/testing/test_files_3/econ_3.pdf new file mode 100644 index 0000000..b53c9de Binary files /dev/null and b/python/testing/test_files_3/econ_3.pdf differ diff --git a/python/testing/test_files_3/econ_4.pdf b/python/testing/test_files_3/econ_4.pdf new file mode 100644 index 0000000..6bcf48b Binary files /dev/null and b/python/testing/test_files_3/econ_4.pdf differ diff --git a/python/testing/test_files_3/econ_5.pdf b/python/testing/test_files_3/econ_5.pdf new file mode 100644 index 0000000..e66d799 Binary files /dev/null and b/python/testing/test_files_3/econ_5.pdf differ diff --git a/python/testing/test_files_3/ee_1.pdf b/python/testing/test_files_3/ee_1.pdf new file mode 100644 index 0000000..0134ad1 Binary files /dev/null and b/python/testing/test_files_3/ee_1.pdf differ diff --git a/python/testing/test_files_3/ee_2.pdf b/python/testing/test_files_3/ee_2.pdf new file mode 100644 index 0000000..c282728 Binary files /dev/null and b/python/testing/test_files_3/ee_2.pdf differ diff --git a/python/testing/test_files_3/ee_3.pdf b/python/testing/test_files_3/ee_3.pdf new file mode 100644 index 0000000..0f9b971 Binary files /dev/null and b/python/testing/test_files_3/ee_3.pdf differ diff --git a/python/testing/test_files_3/ee_4.pdf b/python/testing/test_files_3/ee_4.pdf new file mode 100644 index 0000000..d52bd96 Binary files /dev/null and b/python/testing/test_files_3/ee_4.pdf differ diff --git a/python/testing/test_files_3/ee_5.pdf b/python/testing/test_files_3/ee_5.pdf new file mode 100644 index 0000000..8c640c0 Binary files /dev/null and b/python/testing/test_files_3/ee_5.pdf differ diff --git a/python/testing/test_files_3/math_1.pdf b/python/testing/test_files_3/math_1.pdf new file mode 100644 index 0000000..1a4bb92 Binary files /dev/null and b/python/testing/test_files_3/math_1.pdf differ diff --git a/python/testing/test_files_3/math_2.pdf b/python/testing/test_files_3/math_2.pdf new file mode 100644 index 0000000..49cbf3c Binary files /dev/null and b/python/testing/test_files_3/math_2.pdf differ diff --git a/python/testing/test_files_3/math_3.pdf b/python/testing/test_files_3/math_3.pdf new file mode 100644 index 0000000..b499745 Binary files /dev/null and b/python/testing/test_files_3/math_3.pdf differ diff --git a/python/testing/test_files_3/math_4.pdf b/python/testing/test_files_3/math_4.pdf new file mode 100644 index 0000000..2d2580a Binary files /dev/null and b/python/testing/test_files_3/math_4.pdf differ diff --git a/python/testing/test_files_3/math_5.pdf b/python/testing/test_files_3/math_5.pdf new file mode 100644 index 0000000..7c4c62c Binary files /dev/null and b/python/testing/test_files_3/math_5.pdf differ diff --git a/python/testing/test_files_5/astro_1.pdf b/python/testing/test_files_5/astro_1.pdf new file mode 100644 index 0000000..15a366b Binary files /dev/null and b/python/testing/test_files_5/astro_1.pdf differ diff --git a/python/testing/test_files_5/astro_2.pdf b/python/testing/test_files_5/astro_2.pdf new file mode 100644 index 0000000..07e9c97 Binary files /dev/null and b/python/testing/test_files_5/astro_2.pdf differ diff --git a/python/testing/test_files_5/astro_3.pdf b/python/testing/test_files_5/astro_3.pdf new file mode 100644 index 0000000..74703c1 Binary files /dev/null and b/python/testing/test_files_5/astro_3.pdf differ diff --git a/python/testing/test_files_5/astro_4.pdf b/python/testing/test_files_5/astro_4.pdf new file mode 100644 index 0000000..f520b4d Binary files /dev/null and b/python/testing/test_files_5/astro_4.pdf differ diff --git a/python/testing/test_files_5/astro_5.pdf b/python/testing/test_files_5/astro_5.pdf new file mode 100644 index 0000000..14295ff Binary files /dev/null and b/python/testing/test_files_5/astro_5.pdf differ diff --git a/python/testing/test_files_5/astro_6.pdf b/python/testing/test_files_5/astro_6.pdf new file mode 100644 index 0000000..26c7680 Binary files /dev/null and b/python/testing/test_files_5/astro_6.pdf differ diff --git a/python/testing/test_files_5/econ_1.pdf b/python/testing/test_files_5/econ_1.pdf new file mode 100644 index 0000000..aa8e7a5 Binary files /dev/null and b/python/testing/test_files_5/econ_1.pdf differ diff --git a/python/testing/test_files_5/econ_2.pdf b/python/testing/test_files_5/econ_2.pdf new file mode 100644 index 0000000..61a49be Binary files /dev/null and b/python/testing/test_files_5/econ_2.pdf differ diff --git a/python/testing/test_files_5/econ_3.pdf b/python/testing/test_files_5/econ_3.pdf new file mode 100644 index 0000000..b53c9de Binary files /dev/null and b/python/testing/test_files_5/econ_3.pdf differ diff --git a/python/testing/test_files_5/econ_4.pdf b/python/testing/test_files_5/econ_4.pdf new file mode 100644 index 0000000..6bcf48b Binary files /dev/null and b/python/testing/test_files_5/econ_4.pdf differ diff --git a/python/testing/test_files_5/econ_5.pdf b/python/testing/test_files_5/econ_5.pdf new file mode 100644 index 0000000..e66d799 Binary files /dev/null and b/python/testing/test_files_5/econ_5.pdf differ diff --git a/python/testing/test_files_5/ee_1.pdf b/python/testing/test_files_5/ee_1.pdf new file mode 100644 index 0000000..0134ad1 Binary files /dev/null and b/python/testing/test_files_5/ee_1.pdf differ diff --git a/python/testing/test_files_5/ee_2.pdf b/python/testing/test_files_5/ee_2.pdf new file mode 100644 index 0000000..c282728 Binary files /dev/null and b/python/testing/test_files_5/ee_2.pdf differ diff --git a/python/testing/test_files_5/ee_3.pdf b/python/testing/test_files_5/ee_3.pdf new file mode 100644 index 0000000..0f9b971 Binary files /dev/null and b/python/testing/test_files_5/ee_3.pdf differ diff --git a/python/testing/test_files_5/ee_4.pdf b/python/testing/test_files_5/ee_4.pdf new file mode 100644 index 0000000..d52bd96 Binary files /dev/null and b/python/testing/test_files_5/ee_4.pdf differ diff --git a/python/testing/test_files_5/ee_5.pdf b/python/testing/test_files_5/ee_5.pdf new file mode 100644 index 0000000..8c640c0 Binary files /dev/null and b/python/testing/test_files_5/ee_5.pdf differ diff --git a/python/testing/test_files_5/math_1.pdf b/python/testing/test_files_5/math_1.pdf new file mode 100644 index 0000000..1a4bb92 Binary files /dev/null and b/python/testing/test_files_5/math_1.pdf differ diff --git a/python/testing/test_files_5/math_2.pdf b/python/testing/test_files_5/math_2.pdf new file mode 100644 index 0000000..49cbf3c Binary files /dev/null and b/python/testing/test_files_5/math_2.pdf differ diff --git a/python/testing/test_files_5/math_3.pdf b/python/testing/test_files_5/math_3.pdf new file mode 100644 index 0000000..b499745 Binary files /dev/null and b/python/testing/test_files_5/math_3.pdf differ diff --git a/python/testing/test_files_5/math_4.pdf b/python/testing/test_files_5/math_4.pdf new file mode 100644 index 0000000..2d2580a Binary files /dev/null and b/python/testing/test_files_5/math_4.pdf differ diff --git a/python/testing/test_files_5/math_5.pdf b/python/testing/test_files_5/math_5.pdf new file mode 100644 index 0000000..7c4c62c Binary files /dev/null and b/python/testing/test_files_5/math_5.pdf differ diff --git a/python/testing/test_folder_name_creator.py b/python/testing/test_folder_name_creator.py index 48a3ebd..219cfcc 100644 --- a/python/testing/test_folder_name_creator.py +++ b/python/testing/test_folder_name_creator.py @@ -14,38 +14,14 @@ def encode(self, texts): @pytest.fixture def creator(dummy_model): - return FolderNameCreator(model=dummy_model) - -def test_remove_all_extensions(creator): - assert creator.remove_all_extensions("file.tar.gz") == "file" - assert creator.remove_all_extensions("doc.pdf") == "doc" - assert creator.remove_all_extensions("image.backup.png") == "image" - -def test_assign_parent_scores(creator): - scores = {} - creator.assignParentScores("/home/user/project/docs/file.txt", scores) - assert "docs" in scores - assert scores["docs"] > 0 - assert scores["home"] < scores["docs"] # deeper folders weighted more - -def test_combine_lists_merges_and_sorts(creator): - k = [("alpha", 0.3), ("beta", 0.1)] - f = [("beta", 0.2)] - p = [("alpha", 0.2), ("gamma", 0.4)] - combined = creator.combine_lists(k, f, p) - assert combined[0][0] == "alpha" # alpha has 0.3+0.2 = 0.5 - assert combined[1][0] == "gamma" - assert combined[2][0] == "beta" + return FolderNameCreator(model=dummy_model, case_convention="CAMEL") + def test_lemmatize_deduplicates(creator): result = creator.lemmatize(["running.cases", "run-case", "run"]) - assert set(result) =={"run", "case"} - assert len(result) == 2 + assert result == [['run', 'case'], ['run', 'case'], ['run']] + assert len(result) == 3 -def test_lemmatize_with_scores_prefers_high_score(creator): - result = creator.lemmatize_with_scores([("running", 1.0), ("run", 2.0)]) - assert result[0][0] == "run" - assert result[0][1] == 2.0 def test_generate_folder_name_basic(creator): files = [ @@ -58,6 +34,13 @@ def test_generate_folder_name_basic(creator): assert "_" not in name or len(name.split("_")) <= creator.foldername_length def test_generate_folder_name_empty(creator): + assert creator.generateFolderName([]) == "misc" + +def test_format_case(dummy_model): + words = ["test", "case"] + + assert FolderNameCreator(dummy_model, "CAMEL").format_case(words) == "testCase" + assert FolderNameCreator(dummy_model, "SNAKE").format_case(words) == "test_case" + assert FolderNameCreator(dummy_model, "PASCAL").format_case(words) == "TestCase" + assert FolderNameCreator(dummy_model, "KEBAB").format_case(words) == "test-case" -# <------ Integration TESTING -----> - assert creator.generateFolderName([]) == "Group" diff --git a/python/testing/test_fullvector.py b/python/testing/test_fullvector.py index 1beb981..97a9450 100644 --- a/python/testing/test_fullvector.py +++ b/python/testing/test_fullvector.py @@ -19,12 +19,14 @@ def test_create_full_vector(self): { "size_bytes": 1000, "keywords": [("keyword1", 0.5), ("keyword2", 1.0)], + "filename" : "file1", "created": "2023-01-01T00:00:00", "tags": ["tag1", "tag2"] }, { "size_bytes": 2000, "keywords": [], + "filename" : "file2", "created": "2023-02-01T00:00:00", "tags": ["tag3"] } @@ -36,14 +38,14 @@ def test_create_full_vector(self): for file in files: self.assertIn("full_vector", file) self.assertIsInstance(file["full_vector"], list) - self.assertEqual(len(file["full_vector"]), 8) # 3 (kw) + 1 (created) + 1 (size) + 3 (tags) + self.assertEqual(len(file["full_vector"]), 11) # 3 (kw) + 1 (created) + 1 (size) + 3 (tags) + 2 (fn) def test_gather_feature_data(self): files = [ - {"size_bytes": 1000, "keywords": [("key1", 0.5)], "created": "2023-01-01T00:00:00", "tags": ["tag1", "tag2"]}, - {"size_bytes": 2000, "keywords": [], "created": "2023-02-01T00:00:00", "tags": ["tag3"]} + {"size_bytes": 1000, "keywords": [("key1", 0.5)],"filename" : "file1", "created": "2023-01-01T00:00:00", "tags": ["tag1", "tag2"]}, + {"size_bytes": 2000, "keywords": [],"filename":"file2", "created": "2023-02-01T00:00:00", "tags": ["tag3"]} ] - features = ["size_bytes", "keywords", "created", "tags"] + features = ["size_bytes", "keywords", "created", "tags", "filename"] fv = FullVector(self.mock_transformer) result = fv._gather_feature_data(files, features) @@ -52,6 +54,7 @@ def test_gather_feature_data(self): self.assertEqual(len(result["keywords"]), 2) self.assertEqual(len(result["created"]), 2) self.assertEqual(len(result["tags"]), 2) + self.assertEqual(len(result["filename"]), 2) def test_normalize_feature(self): values = [1000, 2000] diff --git a/python/testing/test_keywords_request.py b/python/testing/test_keywords_request.py index 2e98171..160225e 100644 --- a/python/testing/test_keywords_request.py +++ b/python/testing/test_keywords_request.py @@ -69,7 +69,7 @@ def get_path(name): files = files1, ) - req = DirectoryRequest(root=root_dir, requestType="KEYWORDS") + req = DirectoryRequest(root=root_dir, requestType="KEYWORDS", serverSecret=os.environ["SFM_SERVER_SECRET"]) yield req @@ -109,3 +109,8 @@ def test_send_real_dir(grpc_test_server, createDirectoryRequest): assert response.response_msg != "No file could be opened" +def test_invalid_credential_req(grpc_test_server): + req = DirectoryRequest(root=None, requestType = "KEYWORDS", serverSecret = "wrongSecret") + response = grpc_test_server.SendDirectoryStructure(req) + assert response.response_code == 401 + assert response.response_msg == "Unauthorized: Incorrect server secret" diff --git a/python/testing/test_locked_request.py b/python/testing/test_locked_request.py index 9200a52..0e0e0ef 100644 --- a/python/testing/test_locked_request.py +++ b/python/testing/test_locked_request.py @@ -127,7 +127,7 @@ def get_locked_path_inner(name): files = files1 ) - req = DirectoryRequest(root=root_dir, requestType="CLUSTERING") + req = DirectoryRequest(root=root_dir, requestType="CLUSTERING", serverSecret=os.environ["SFM_SERVER_SECRET"]) yield req @@ -164,3 +164,4 @@ def test_send_real_dir(grpc_test_server, createDirectoryRequest): # Check if response is well formed assert response.response_code == 200 assert response.response_msg != "No file could be opened" + diff --git a/python/testing/test_metadata_request.py b/python/testing/test_metadata_request.py index 9647641..959266b 100644 --- a/python/testing/test_metadata_request.py +++ b/python/testing/test_metadata_request.py @@ -99,7 +99,7 @@ def get_path(name): directories= [uni_dir, pers_dir] ) - req = DirectoryRequest(root=root_dir, requestType="METADATA") + req = DirectoryRequest(root=root_dir, requestType="METADATA", serverSecret=os.environ["SFM_SERVER_SECRET"]) yield req @@ -128,3 +128,10 @@ def test_send_real_dir(grpc_test_server, createDirectoryRequest): assert response.response_msg != "No file could be opened" +def test_invalid_credential_req(grpc_test_server): + req = DirectoryRequest(root=None, requestType = "METADATA", serverSecret = "wrongSecret") + response = grpc_test_server.SendDirectoryStructure(req) + assert response.response_code == 401 + assert response.response_msg == "Unauthorized: Incorrect server secret" + + diff --git a/python/testing/test_research_papers.py b/python/testing/test_research_papers.py new file mode 100644 index 0000000..3aa57f6 --- /dev/null +++ b/python/testing/test_research_papers.py @@ -0,0 +1,78 @@ +import pytest +import sys +import os +import time + +# Add src to path temporarily so the generated grpc file can find message_structure_pb2 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + + +from src.message_structure_pb2 import Directory, File, Tag, DirectoryRequest + + +# <------ INTEGRATION TESTING -----> +@pytest.fixture(scope="module") +def createDirectoryRequest(): + TEST_DIR = os.path.dirname(__file__) + TEST_FILE_DIR = os.path.join(TEST_DIR, "test_files_5") + + def get_path(name): + return os.path.join(TEST_FILE_DIR, name) + + # gRPC objects for files we want to test + files1 = [ + File(name="astro_1", original_path=get_path("astro_1.pdf"), tags=[]), + File(name="astro_2", original_path=get_path("astro_2.pdf"), tags=[]), + File(name="astro_3", original_path=get_path("astro_3.pdf"), tags=[]), + File(name="astro_4", original_path=get_path("astro_4.pdf"), tags=[]), + File(name="astro_5", original_path=get_path("astro_5.pdf"), tags=[]), + File(name="astro_6", original_path=get_path("astro_6.pdf"), tags=[]), + File(name="econ_1", original_path=get_path("econ_1.pdf"), tags=[]), + File(name="econ_2", original_path=get_path("econ_2.pdf"), tags=[]), + File(name="econ_3", original_path=get_path("econ_3.pdf"), tags=[]), + File(name="econ_4", original_path=get_path("econ_4.pdf"), tags=[]), + File(name="econ_5", original_path=get_path("econ_5.pdf"), tags=[]), + File(name="ee_1", original_path=get_path("ee_1.pdf"), tags=[]), + File(name="ee_2", original_path=get_path("ee_2.pdf"), tags=[]), + File(name="ee_3", original_path=get_path("ee_3.pdf"), tags=[]), + File(name="ee_4", original_path=get_path("ee_4.pdf"), tags=[]), + File(name="ee_5", original_path=get_path("ee_5.pdf"), tags=[]), + File(name="math_1", original_path=get_path("math_1.pdf"), tags=[]), + File(name="math_2", original_path=get_path("math_2.pdf"), tags=[]), + File(name="math_3", original_path=get_path("math_3.pdf"), tags=[]), + File(name="math_4", original_path=get_path("math_4.pdf"), tags=[]), + File(name="math_5", original_path=get_path("math_5.pdf"), tags=[]), + ] + + + root_dir = Directory( + name = "test_files_5", + path = get_path("test_files_5"), + files = files1 + ) + + req = DirectoryRequest(root=root_dir, requestType="CLUSTERING", serverSecret=os.environ["SFM_SERVER_SECRET"]) + yield req + + + +# Sends an actual directory and checks if metadata was correctly attached to files +def test_send_real_dir(grpc_test_server, createDirectoryRequest): + req = createDirectoryRequest # Accessing req from the fixture + + start_first = time.time() + response = grpc_test_server.SendDirectoryStructure(req) + end_first = time.time() + + # start_second = time.time() + # response2 = grpc_test_server.SendDirectoryStructure(req) + # end_second = time.time() + + print("First took: " + str(end_first - start_first)) + # print("Second took: " + str(end_second - start_second)) + + # Check if response contains all files + + # Check if response is well formed + assert response.response_code == 200 + assert response.response_msg != "No file could be opened" \ No newline at end of file diff --git a/server.env b/server.env new file mode 100644 index 0000000..81223cb --- /dev/null +++ b/server.env @@ -0,0 +1 @@ +export PYTHON_SERVER=50051