diff --git a/app/back-end/src/constants.py b/app/back-end/src/constants.py index 1dac729..5729675 100644 --- a/app/back-end/src/constants.py +++ b/app/back-end/src/constants.py @@ -33,6 +33,7 @@ WORKSPACE_RENAME_ROUTE = "/workspace/rename" WORKSPACE_DELETE_ROUTE = "/workspace/delete" WORKSPACE_AGGREGATE_ROUTE = "/workspace/aggregate" +WORKSPACE_IMPORT_ROUTE = "/workspace/import" # Events CONSOLE_FEEDBACK_EVENT = "console_feedback" diff --git a/app/back-end/src/routes/workspace_route.py b/app/back-end/src/routes/workspace_route.py index 91e65a7..c02c003 100644 --- a/app/back-end/src/routes/workspace_route.py +++ b/app/back-end/src/routes/workspace_route.py @@ -98,6 +98,7 @@ WORKSPACE_UPDATE_FEEDBACK_EVENT, CONSOLE_FEEDBACK_EVENT, WORKSPACE_FILE_SAVE_FEEDBACK_EVENT, + WORKSPACE_IMPORT_ROUTE, ) workspace_route_bp = Blueprint("workspace_route", __name__) @@ -1063,3 +1064,107 @@ def put_workspace_delete(relative_path): sid, ) return jsonify({"error": "An internal error occurred"}), 500 + +@workspace_route_bp.route(f"{WORKSPACE_IMPORT_ROUTE}", methods=["POST"]) +@workspace_route_bp.route(f"{WORKSPACE_IMPORT_ROUTE}/", methods=["POST"]) +@compress.compressed() +def import_file(relative_path=None): + + uuid = request.headers.get("uuid") + sid = request.headers.get("sid") + + # Ensure the uuid header is present + if not uuid: + return jsonify({"error": "UUID header is missing"}), 400 + + # Ensure the sid header is present + if not sid: + return jsonify({"error": "SID header is missing"}), 400 + + + if 'file' not in request.files: + return jsonify({'error': 'No file part in the request'}), 400 + + file = request.files['file'] + + if file.filename == '': + return jsonify({'error': 'No file selected for importing'}), 400 + + file_extension = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else None + if file_extension not in ['csv', 'txt']: + return jsonify({'error': f"FileImportError: Incorrect file type for '{file.filename}'. Accepted file types: 'csv', 'txt'."}), 400 + + relative_path_title = relative_path + + if relative_path is None: + relative_path = "" + relative_path_title = "root folder" + + socketio_emit_to_user_session( + CONSOLE_FEEDBACK_EVENT, + {"type": "info", "message": f"Importing file '{file.filename}' to '{relative_path_title}'..."}, + uuid, + sid, + ) + + try: + user_workspace_dir = os.path.join(WORKSPACE_DIR, uuid) + folder_path = os.path.join(user_workspace_dir, relative_path) + destination_path = os.path.join(folder_path, file.filename) + file.save(destination_path) + + socketio_emit_to_user_session( + CONSOLE_FEEDBACK_EVENT, + {"type": "succ", "message": f"File {file.filename} was imported successfully."}, + uuid, + sid, + ) + + socketio_emit_to_user_session( + WORKSPACE_UPDATE_FEEDBACK_EVENT, + {"status": "updated"}, + uuid, + sid, + ) + + except FileNotFoundError as e: + logger.error("FileNotFoundError: %s while accessing %s", e, user_workspace_dir) + # Emit a feedback to the user's console + socketio_emit_to_user_session( + CONSOLE_FEEDBACK_EVENT, + { + "type": "errr", + "message": f"FileNotFoundError: {e} while accessing {user_workspace_dir}", + }, + uuid, + sid, + ) + return jsonify({"error": "Requested file not found"}), 404 + except PermissionError as e: + logger.error("PermissionError: %s while accessing %s", e, user_workspace_dir) + # Emit a feedback to the user's console + socketio_emit_to_user_session( + CONSOLE_FEEDBACK_EVENT, + { + "type": "errr", + "message": f"PermissionError: {e} while accessing {user_workspace_dir}", + }, + uuid, + sid, + ) + return jsonify({"error": "Permission denied"}), 403 + except UnexpectedError as e: + logger.error("UnexpectedError: %s while accessing %s", e.message, user_workspace_dir) + # Emit a feedback to the user's console + socketio_emit_to_user_session( + CONSOLE_FEEDBACK_EVENT, + { + "type": "errr", + "message": f"UnexpectedError: {e.message} while accessing {user_workspace_dir}", + }, + uuid, + sid, + ) + return jsonify({"error": "An internal error occurred"}), 500 + + return jsonify({'message': 'File imported successfully'}), 200 diff --git a/app/front-end/src/features/editor/components/fileTreeView/fileTreeItem/fileTreeItemContextMenu.tsx b/app/front-end/src/features/editor/components/fileTreeView/fileTreeItem/fileTreeItemContextMenu.tsx index 14b850d..82f1321 100644 --- a/app/front-end/src/features/editor/components/fileTreeView/fileTreeItem/fileTreeItemContextMenu.tsx +++ b/app/front-end/src/features/editor/components/fileTreeView/fileTreeItem/fileTreeItemContextMenu.tsx @@ -172,7 +172,7 @@ export const FileTreeItemContextMenu: React.FC = ( setFileImportDialogOpen(false)} - onConfirm={() => {}} + item={item} /> void; - onConfirm: () => void; + item: FileTreeViewItemProps; } export const FileTreeItemContextMenuFileImportDialog: React.FC = ({ open, onClose, - onConfirm, + item, }) => { const Theme = useTheme(); + const { fileTree } = useWorkspaceContext(); + const [filename, setFilename] = useState(''); + const [file, setFile] = useState(null); + + const [newInfoFileName, setNewInfoFileName] = useState(''); + const [isIncorrectFileType, setIsIncorrectFileType] = useState(false); + + const handleFileChange = (e: ChangeEvent) => { + setFilename(''); + setNewInfoFileName(''); + setIsIncorrectFileType(false); - const handleFileUpload = (e: ChangeEvent) => { if (!e.target.files) { return; } - const file = e.target.files[0]; - const { name } = file; - setFilename(name); + + const selectedFile = e.target.files?.[0] || null; + setFile(selectedFile); + setFilename(selectedFile.name); + + const filePath = item.id === '' ? selectedFile.name : `${item.id}/${selectedFile.name}`; + + const fileExtension = getFileExtension(selectedFile.name); + if (fileExtension !== 'csv' && fileExtension !== 'txt') { + setIsIncorrectFileType(true); + return; + } + + if (doesFileExist(fileTree, filePath)) { + const newFileName = findUniqueFileName(fileTree, filePath); + setNewInfoFileName(newFileName); + + const newFile = new File([selectedFile], newFileName, { type: selectedFile.type }); + + console.log(newFile.name); + setFile(newFile); + } + }; + + const handleSubmit = async () => { + if (!file) { + console.error('No file selected'); + return; + } + + const formData = new FormData(); + formData.append('file', file); + + try { + let parentPath = item.id.match(/^(.*)(?=\/[^/]*$)/)?.[0] || ''; + if (parentPath === '') parentPath = item.id; + + const url = parentPath ? `${Endpoints.WORKSPACE_IMPORT}/${item.id}` : Endpoints.WORKSPACE_IMPORT; + + await axios.post(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + onClose(); + } catch (error) { + console.error('Error uploading file:', error); + } }; return ( @@ -56,28 +118,51 @@ export const FileTreeItemContextMenuFileImportDialog: React.FC - - - - {filename === '' ? ( - 'No file selected' - ) : ( + + {filename !== '' && ( <> - Selected file: {filename} + Selected file: "{filename}" )} + {!isIncorrectFileType && newInfoFileName !== '' && ( + + File will be saved as: "{newInfoFileName}". + + )} + {isIncorrectFileType && ( + + Incorrect file extension! +
Accepted file extensions: '.csv', '.txt' +
+ )}
diff --git a/app/front-end/src/features/editor/utils/helpers.tsx b/app/front-end/src/features/editor/utils/helpers.tsx index 11c55d7..2dea027 100644 --- a/app/front-end/src/features/editor/utils/helpers.tsx +++ b/app/front-end/src/features/editor/utils/helpers.tsx @@ -39,3 +39,30 @@ export const doesFileExist = (fileTreeView: TreeViewBaseItem[], path: string): string => { + const dotIndex = path.lastIndexOf('.'); + const filePath = path.substring(0, dotIndex); + const fileExtension = path.substring(dotIndex + 1); + + let newFilePath = filePath; + let newFullPath = `${newFilePath}.${fileExtension}`; + + let i = 1; + + while (doesFileExist(fileTreeView, newFullPath)) { + newFilePath = `${filePath} (${i})`; + newFullPath = `${newFilePath}.${fileExtension}`; + i++; + } + + const lastSlashIndex = newFullPath.lastIndexOf('/'); + const newFileName = newFullPath.substring(lastSlashIndex + 1); + + return newFileName; +}; + +export const getFileExtension = (filename: string): string => { + const dotIndex = filename.lastIndexOf('.'); + return dotIndex !== -1 ? filename.substring(dotIndex + 1).toLowerCase() : ''; +}; diff --git a/app/front-end/src/types/constants/endpoints.ts b/app/front-end/src/types/constants/endpoints.ts index 6805ff7..b1accc2 100644 --- a/app/front-end/src/types/constants/endpoints.ts +++ b/app/front-end/src/types/constants/endpoints.ts @@ -38,4 +38,5 @@ export const Endpoints = { WORKSPACE_RENAME: `/workspace/rename`, WORKSPACE_DELETE: `/workspace/delete`, WORKSPACE_AGGREGATE: `/workspace/aggregate`, + WORKSPACE_IMPORT: `/workspace/import`, };