diff --git a/server/src/fs/create_dir.rs b/server/src/fs/create_dir.rs new file mode 100644 index 0000000..0d110a1 --- /dev/null +++ b/server/src/fs/create_dir.rs @@ -0,0 +1,41 @@ +use std::fs; + +use axum::{extract::rejection::JsonRejection, Extension, Json}; +use axum_auth::AuthBearer; +use homedisk_database::Database; +use homedisk_types::fs::create_dir::{Request, Response}; +use homedisk_types::{ + config::types::Config, + errors::{FsError, ServerError}, +}; + +use crate::fs::validate_path; +use crate::middleware::{find_user, validate_json, validate_jwt}; + +pub async fn handle( + Extension(db): Extension, + Extension(config): Extension, + AuthBearer(token): AuthBearer, + request: Result, JsonRejection>, +) -> Result, ServerError> { + let Json(request) = validate_json::(request)?; + let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?; + + // validate the `path` can be used + validate_path(&request.path)?; + + // search for a user by UUID from a token + let user = find_user(db, token.claims.sub).await?; + + // directory where the file will be placed + let path = format!( + "{user_dir}/{req_dir}", + user_dir = user.user_dir(&config.storage.path), + req_dir = request.path + ); + + fs::create_dir_all(path) + .map_err(|err| ServerError::FsError(FsError::CreateDirectory(err.to_string())))?; + + Ok(Json(Response { created: true })) +} diff --git a/server/src/fs/download.rs b/server/src/fs/download.rs index 24b1366..d22097e 100644 --- a/server/src/fs/download.rs +++ b/server/src/fs/download.rs @@ -6,10 +6,7 @@ use axum::Extension; use axum_auth::AuthBearer; use homedisk_database::Database; use homedisk_types::fs::upload::Pagination; -use homedisk_types::{ - config::types::Config, - errors::ServerError, -}; +use homedisk_types::{config::types::Config, errors::ServerError}; use crate::middleware::{find_user, validate_jwt}; diff --git a/server/src/fs/mod.rs b/server/src/fs/mod.rs index fa7bb1c..af71085 100644 --- a/server/src/fs/mod.rs +++ b/server/src/fs/mod.rs @@ -1,3 +1,4 @@ +pub mod create_dir; pub mod delete; pub mod download; pub mod list; @@ -11,6 +12,7 @@ pub fn app() -> axum::Router { .route("/upload", post(upload::handle)) .route("/delete", delete(upload::handle)) .route("/download", get(download::handle)) + .route("/createdir", post(create_dir::handle)) } pub fn validate_path(path: &str) -> Result<(), homedisk_types::errors::ServerError> { diff --git a/types/src/errors/fs.rs b/types/src/errors/fs.rs index fa9e591..343886d 100644 --- a/types/src/errors/fs.rs +++ b/types/src/errors/fs.rs @@ -14,6 +14,9 @@ pub enum Error { #[error("create file - {0}")] CreateFile(String), + #[error("create dir - {0}")] + CreateDirectory(String), + #[error("delete file - {0}")] DeleteFile(String), diff --git a/types/src/errors/mod.rs b/types/src/errors/mod.rs index 4d411ff..0e6ba7d 100644 --- a/types/src/errors/mod.rs +++ b/types/src/errors/mod.rs @@ -55,6 +55,7 @@ impl axum::response::IntoResponse for ServerError { FsError::FileDoesNotExist => StatusCode::BAD_REQUEST, FsError::MultipartError => StatusCode::BAD_REQUEST, FsError::CreateFile(_) => StatusCode::INTERNAL_SERVER_ERROR, + FsError::CreateDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR, FsError::DeleteFile(_) => StatusCode::INTERNAL_SERVER_ERROR, FsError::DeleteDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR, FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR, diff --git a/types/src/fs/create_dir.rs b/types/src/fs/create_dir.rs new file mode 100644 index 0000000..e55f924 --- /dev/null +++ b/types/src/fs/create_dir.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Request { + pub path: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Response { + pub created: bool, +} diff --git a/types/src/fs/mod.rs b/types/src/fs/mod.rs index 20c4358..94a5e57 100644 --- a/types/src/fs/mod.rs +++ b/types/src/fs/mod.rs @@ -1,3 +1,4 @@ +pub mod create_dir; pub mod delete; pub mod download; pub mod list; diff --git a/website/api_utils/create-directory.ts b/website/api_utils/create-directory.ts new file mode 100644 index 0000000..7278bfe --- /dev/null +++ b/website/api_utils/create-directory.ts @@ -0,0 +1,34 @@ +import axios from './axios' + +export default async function createDir(path: string, token: string): Promise { + const request = axios.post("/fs/createdir", { + path + }, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + + const response = request + .then(response => { + const { data } = response + return data + }) + .catch(err => { + if (err.response?.data?.error_message) { + const error = err.response.data.error_message + + if (error.toString() == "[object Object]") { + Object.keys(error).forEach(key => { + throw new Error(key) + }) + } + + throw new Error(error) + } + + throw new Error(err) + }) + + return response +} diff --git a/website/api_utils/index.ts b/website/api_utils/index.ts index 016c43a..551f66d 100644 --- a/website/api_utils/index.ts +++ b/website/api_utils/index.ts @@ -2,7 +2,8 @@ import list from "./list" import login from "./login" import register from "./register" import upload from "./upload" +import createDir from "./create-directory" -const api = { list, login, register, upload } +const api = { list, login, register, upload, createDir } export default api diff --git a/website/components/user/modals/create-folder.tsx b/website/components/user/modals/create-folder.tsx new file mode 100644 index 0000000..196bd2f --- /dev/null +++ b/website/components/user/modals/create-folder.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react' +import { Backdrop, Box, Button, Fade, Link, Modal, TextField } from "@mui/material" +import { useCookies } from "react-cookie" +import api from '../../../api_utils' +import style from './style' + +function CreateFolderModal({ open, setOpen, refresh }: Props) { + const [name, setName] = useState("") + + const [cookies] = useCookies(["token"]) + + const handleChange: React.ChangeEventHandler = event => { + const value = event.target.value + setName(value) + } + + // handle click "Enter (Return)" + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.keyCode === 13 || event.which === 13 || event.charCode === 13) { + handle() + } + } + + const handle = () => { + setOpen(false) + + const request = api.createDir(name, cookies.token) + + request + .then(refresh) + } + + return ( + setOpen(false)} + closeAfterTransition + BackdropComponent={Backdrop} + BackdropProps={{ + timeout: 500, + }} + > + + + + + + + + + + + ) +} + +export type Props = { + open: boolean, + setOpen: React.Dispatch>, + refresh: () => void, +} + +export default CreateFolderModal diff --git a/website/pages/user/files.tsx b/website/pages/user/files.tsx index ebe37d8..600d204 100644 --- a/website/pages/user/files.tsx +++ b/website/pages/user/files.tsx @@ -1,5 +1,5 @@ import { faFile, faFolder } from "@fortawesome/free-solid-svg-icons" -import { CloudUpload } from "@mui/icons-material" +import { CloudUpload, CreateNewFolder } from "@mui/icons-material" import { IconButton, Link as MuiLink } from "@mui/material" import Head from 'next/head' import Link from 'next/link' @@ -10,6 +10,7 @@ import api from '../../api_utils' import Icon from "../../components/other/icon" import Table from "../../components/user/table" import UploadModal from "../../components/user/modals/upload" +import CreateFolderModal from "../../components/user/modals/create-folder" export default function Files() { const [cookies] = useCookies(["token"]) @@ -45,6 +46,7 @@ export default function Files() { // modals const [uploadModal, setUploadModal] = useState(false) + const [createFolderModal, setCreateFolderModal] = useState(false) return ( <> @@ -52,18 +54,30 @@ export default function Files() { Files - HomeDisk - setUploadModal(true)}> + setUploadModal(true)} > + + setCreateFolderModal(true)} + > + + +