Skip to content

Commit

Permalink
Implemented file uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
justinnas committed Sep 6, 2024
1 parent 3e8cb90 commit ae6774b
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 17 deletions.
1 change: 1 addition & 0 deletions app/back-end/src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
105 changes: 105 additions & 0 deletions app/back-end/src/routes/workspace_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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}/<path:relative_path>", 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
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const FileTreeItemContextMenu: React.FC<FileTreeItemContextMenuProps> = (
<FileTreeItemContextMenuFileImportDialog
open={Boolean(fileImportDialogOpen)}
onClose={() => setFileImportDialogOpen(false)}
onConfirm={() => {}}
item={item}
/>
<FileTreeItemContextMenuTextfieldDialog
open={Boolean(newFileDialogOpen)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { FileTreeItemContextMenuStyledDialog } from '@/features/editor/components/fileTreeView/fileTreeItem';
import { useWorkspaceContext } from '@/features/editor/hooks';
import { FileTreeViewItemProps } from '@/features/editor/types';
import { doesFileExist } from '@/features/editor/utils';
import { findUniqueFileName, getFileExtension } from '@/features/editor/utils/helpers';
import { axios } from '@/lib';
import { Endpoints } from '@/types';
import { Close as CloseIcon, UploadFile as UploadFileIcon } from '@mui/icons-material';
import {
Box,
Expand All @@ -16,24 +22,80 @@ import { ChangeEvent, useState } from 'react';
export interface FileTreeItemContextMenuFileImportDialogProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
item: FileTreeViewItemProps;
}

export const FileTreeItemContextMenuFileImportDialog: React.FC<FileTreeItemContextMenuFileImportDialogProps> = ({
open,
onClose,
onConfirm,
item,
}) => {
const Theme = useTheme();
const { fileTree } = useWorkspaceContext();

const [filename, setFilename] = useState('');
const [file, setFile] = useState<File | null>(null);

const [newInfoFileName, setNewInfoFileName] = useState('');
const [isIncorrectFileType, setIsIncorrectFileType] = useState(false);

const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
setFilename('');
setNewInfoFileName('');
setIsIncorrectFileType(false);

const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
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 (
Expand All @@ -56,28 +118,51 @@ export const FileTreeItemContextMenuFileImportDialog: React.FC<FileTreeItemConte
</Grid>
</DialogTitle>
<DialogContent sx={{ py: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Button component='label' variant='outlined' startIcon={<UploadFileIcon />} sx={{ marginRight: '1rem' }}>
<Typography sx={{ color: Theme.palette.primary.main, fontSize: '1rem', mb: '1rem' }}>
Importing to location:{' '}
<b>
root/{item.id}
{item.id !== '' && '/'}
</b>
...
</Typography>
<Box>
<Button
component='label'
variant='outlined'
startIcon={<UploadFileIcon />}
sx={{ width: '100%', marginRight: '1rem' }}
>
Select a file...
<input type='file' accept='.csv, .txt' hidden onChange={handleFileUpload} />
<input type='file' accept='.csv, .txt' hidden onChange={handleFileChange} />
</Button>
<Typography sx={{ fontSize: '1rem' }}>
{filename === '' ? (
'No file selected'
) : (
<Typography style={{ fontSize: '1rem', marginTop: '0.5rem', wordWrap: 'break-word' }}>
{filename !== '' && (
<>
Selected file: <span style={{ fontWeight: 'bold' }}>{filename}</span>
Selected file: "<b>{filename}</b>"
</>
)}
</Typography>
</Box>
{!isIncorrectFileType && newInfoFileName !== '' && (
<Typography sx={{ fontSize: '1rem', color: Theme.palette.error.main, mt: '1rem' }}>
File will be saved as: "<b>{newInfoFileName}</b>".
</Typography>
)}
{isIncorrectFileType && (
<Typography sx={{ fontSize: '1rem', color: Theme.palette.error.main, mt: '1rem' }}>
<b>Incorrect file extension!</b>
<br /> Accepted file extensions: '<b>.csv</b>', '<b>.txt</b>'
</Typography>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<Typography sx={{ fontSize: '1rem', color: Theme.palette.text.secondary }}>Cancel</Typography>
</Button>
<Button
onClick={onConfirm}
onClick={handleSubmit}
disabled={filename === '' || isIncorrectFileType}
variant='outlined'
sx={{
borderColor: Theme.palette.primary.main,
Expand All @@ -87,7 +172,14 @@ export const FileTreeItemContextMenuFileImportDialog: React.FC<FileTreeItemConte
},
}}
>
<Typography sx={{ fontSize: '1rem', color: Theme.palette.primary.main }}>Import</Typography>
<Typography
sx={{
fontSize: '1rem',
color: isIncorrectFileType || filename === '' ? Theme.palette.text.secondary : Theme.palette.primary.main,
}}
>
Import
</Typography>
</Button>
</DialogActions>
</FileTreeItemContextMenuStyledDialog>
Expand Down
27 changes: 27 additions & 0 deletions app/front-end/src/features/editor/utils/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,30 @@ export const doesFileExist = (fileTreeView: TreeViewBaseItem<FileTreeViewItemPro
}
return false;
};

export const findUniqueFileName = (fileTreeView: TreeViewBaseItem<FileTreeViewItemProps>[], 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() : '';
};
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 @@ -38,4 +38,5 @@ export const Endpoints = {
WORKSPACE_RENAME: `/workspace/rename`,
WORKSPACE_DELETE: `/workspace/delete`,
WORKSPACE_AGGREGATE: `/workspace/aggregate`,
WORKSPACE_IMPORT: `/workspace/import`,
};

0 comments on commit ae6774b

Please sign in to comment.