Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JTE/PKFE-46 #58

Merged
merged 12 commits into from
Sep 10, 2024
1 change: 1 addition & 0 deletions app/back-end/src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
WORKSPACE_DELETE_ROUTE = "/workspace/delete"
WORKSPACE_AGGREGATE_ROUTE = "/workspace/aggregate"
WORKSPACE_IMPORT_ROUTE = "/workspace/import"
WORKSPACE_EXPORT_ROUTE = "/workspace/export"

# Events
CONSOLE_FEEDBACK_EVENT = "console_feedback"
Expand Down
75 changes: 74 additions & 1 deletion app/back-end/src/routes/workspace_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
import os
import shutil
import csv
from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, send_file

from src.setup.extensions import compress, logger
from src.utils.helpers import socketio_emit_to_user_session, build_workspace_structure
Expand All @@ -99,6 +99,7 @@
CONSOLE_FEEDBACK_EVENT,
WORKSPACE_FILE_SAVE_FEEDBACK_EVENT,
WORKSPACE_IMPORT_ROUTE,
WORKSPACE_EXPORT_ROUTE,
)

workspace_route_bp = Blueprint("workspace_route", __name__)
Expand Down Expand Up @@ -1168,3 +1169,75 @@ def import_file(relative_path=None):
return jsonify({"error": "An internal error occurred"}), 500

return jsonify({'message': 'File imported successfully'}), 200

@workspace_route_bp.route(f"{WORKSPACE_EXPORT_ROUTE}/<path:relative_path>", methods=["GET"])
def export_file(relative_path):

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

try:
user_workspace_dir = os.path.join(WORKSPACE_DIR, uuid)
file_path = os.path.join(user_workspace_dir, relative_path)

#Emit a feedback to the user's console
socketio_emit_to_user_session(
CONSOLE_FEEDBACK_EVENT,
{"type": "info", "message": f"Exporting file '{relative_path}'..."},
uuid,
sid,
)

response = send_file(file_path, as_attachment=True)

return response

except FileNotFoundError as e:
logger.error("FileNotFoundError: %s while accessing %s", e, user_workspace_dir)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edit everywhere where logger and console feedback: accessing -> exporting

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch

# 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


Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const FileTreeItemContextMenu: React.FC<FileTreeItemContextMenuProps> = (
);
} else {
menuItems.push(
<MenuItem key='export' onClick={() => handleActionContextMenu('export')} disabled>
<MenuItem key='export' onClick={() => handleActionContextMenu('export')}>
Export...
</MenuItem>,
<Divider key='divider-export' />
Expand Down Expand Up @@ -112,8 +112,7 @@ export const FileTreeItemContextMenu: React.FC<FileTreeItemContextMenuProps> = (
setFileImportDialogOpen(true);
break;
case 'export':
// TODO: Implement file export
console.log('export');
handleExport();
break;
case 'rename':
setRenameDialogOpen(true);
Expand Down Expand Up @@ -158,6 +157,30 @@ export const FileTreeItemContextMenu: React.FC<FileTreeItemContextMenuProps> = (
filesHistoryStateUpdate(undefined, { id: item.id, label: item.label, type: item.fileType || FileTypes.FILE });
};

const handleExport = async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use hook useCallback to memorize functions to reduce recreation every render

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea

try {
const response = await axios.get(`${Endpoints.WORKSPACE_EXPORT}/${item.id}`, {
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([response.data]));

const fileName = item.id.match(/[^/\\]+$/)?.[0] || item.id; // Extracts only the file name, otherwise uses the full path

const link = Object.assign(document.createElement('a'), {
href: url,
download: fileName,
});

link.click();
window.URL.revokeObjectURL(url);

// TODO: Implement socket console event for successful file export
console.log('Exported:', fileName);
} catch (error) {
console.error('Error exporting file:', error);
}
};

return (
<>
<Menu
Expand Down
1 change: 1 addition & 0 deletions app/front-end/src/types/constants/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ export const Endpoints = {
WORKSPACE_DELETE: `/workspace/delete`,
WORKSPACE_AGGREGATE: `/workspace/aggregate`,
WORKSPACE_IMPORT: `/workspace/import`,
WORKSPACE_EXPORT: `/workspace/export`,
};
Loading