feat: add create folder feature
This commit is contained in:
parent
ab7f1bc4ea
commit
403ed4199e
|
@ -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<Database>,
|
||||||
|
Extension(config): Extension<Config>,
|
||||||
|
AuthBearer(token): AuthBearer,
|
||||||
|
request: Result<Json<Request>, JsonRejection>,
|
||||||
|
) -> Result<Json<Response>, ServerError> {
|
||||||
|
let Json(request) = validate_json::<Request>(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 }))
|
||||||
|
}
|
|
@ -6,10 +6,7 @@ use axum::Extension;
|
||||||
use axum_auth::AuthBearer;
|
use axum_auth::AuthBearer;
|
||||||
use homedisk_database::Database;
|
use homedisk_database::Database;
|
||||||
use homedisk_types::fs::upload::Pagination;
|
use homedisk_types::fs::upload::Pagination;
|
||||||
use homedisk_types::{
|
use homedisk_types::{config::types::Config, errors::ServerError};
|
||||||
config::types::Config,
|
|
||||||
errors::ServerError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{find_user, validate_jwt};
|
use crate::middleware::{find_user, validate_jwt};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod create_dir;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
@ -11,6 +12,7 @@ pub fn app() -> axum::Router {
|
||||||
.route("/upload", post(upload::handle))
|
.route("/upload", post(upload::handle))
|
||||||
.route("/delete", delete(upload::handle))
|
.route("/delete", delete(upload::handle))
|
||||||
.route("/download", get(download::handle))
|
.route("/download", get(download::handle))
|
||||||
|
.route("/createdir", post(create_dir::handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_path(path: &str) -> Result<(), homedisk_types::errors::ServerError> {
|
pub fn validate_path(path: &str) -> Result<(), homedisk_types::errors::ServerError> {
|
||||||
|
|
|
@ -14,6 +14,9 @@ pub enum Error {
|
||||||
#[error("create file - {0}")]
|
#[error("create file - {0}")]
|
||||||
CreateFile(String),
|
CreateFile(String),
|
||||||
|
|
||||||
|
#[error("create dir - {0}")]
|
||||||
|
CreateDirectory(String),
|
||||||
|
|
||||||
#[error("delete file - {0}")]
|
#[error("delete file - {0}")]
|
||||||
DeleteFile(String),
|
DeleteFile(String),
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ impl axum::response::IntoResponse for ServerError {
|
||||||
FsError::FileDoesNotExist => StatusCode::BAD_REQUEST,
|
FsError::FileDoesNotExist => StatusCode::BAD_REQUEST,
|
||||||
FsError::MultipartError => StatusCode::BAD_REQUEST,
|
FsError::MultipartError => StatusCode::BAD_REQUEST,
|
||||||
FsError::CreateFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
FsError::CreateFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
FsError::CreateDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
FsError::DeleteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
FsError::DeleteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
FsError::DeleteDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
FsError::DeleteDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod create_dir;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import axios from './axios'
|
||||||
|
|
||||||
|
export default async function createDir(path: string, token: string): Promise<any> {
|
||||||
|
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
|
||||||
|
}
|
|
@ -2,7 +2,8 @@ import list from "./list"
|
||||||
import login from "./login"
|
import login from "./login"
|
||||||
import register from "./register"
|
import register from "./register"
|
||||||
import upload from "./upload"
|
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
|
export default api
|
||||||
|
|
|
@ -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<HTMLInputElement> = 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 (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
closeAfterTransition
|
||||||
|
BackdropComponent={Backdrop}
|
||||||
|
BackdropProps={{
|
||||||
|
timeout: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Fade in={open}>
|
||||||
|
<Box sx={style}>
|
||||||
|
<TextField
|
||||||
|
label="Folder"
|
||||||
|
placeholder="Folder"
|
||||||
|
margin="normal"
|
||||||
|
value={name}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link>
|
||||||
|
<Button variant="outlined" onClick={handle}>
|
||||||
|
Create Folder
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
open: boolean,
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
refresh: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateFolderModal
|
|
@ -1,5 +1,5 @@
|
||||||
import { faFile, faFolder } from "@fortawesome/free-solid-svg-icons"
|
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 { IconButton, Link as MuiLink } from "@mui/material"
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
@ -10,6 +10,7 @@ import api from '../../api_utils'
|
||||||
import Icon from "../../components/other/icon"
|
import Icon from "../../components/other/icon"
|
||||||
import Table from "../../components/user/table"
|
import Table from "../../components/user/table"
|
||||||
import UploadModal from "../../components/user/modals/upload"
|
import UploadModal from "../../components/user/modals/upload"
|
||||||
|
import CreateFolderModal from "../../components/user/modals/create-folder"
|
||||||
|
|
||||||
export default function Files() {
|
export default function Files() {
|
||||||
const [cookies] = useCookies(["token"])
|
const [cookies] = useCookies(["token"])
|
||||||
|
@ -45,6 +46,7 @@ export default function Files() {
|
||||||
|
|
||||||
// modals
|
// modals
|
||||||
const [uploadModal, setUploadModal] = useState(false)
|
const [uploadModal, setUploadModal] = useState(false)
|
||||||
|
const [createFolderModal, setCreateFolderModal] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -52,18 +54,30 @@ export default function Files() {
|
||||||
<title>Files - HomeDisk</title>
|
<title>Files - HomeDisk</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<MuiLink onClick={() => setUploadModal(true)}>
|
<MuiLink>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="logo"
|
aria-label="logo"
|
||||||
|
onClick={() => setUploadModal(true)}
|
||||||
>
|
>
|
||||||
<CloudUpload />
|
<CloudUpload />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="logo"
|
||||||
|
onClick={() => setCreateFolderModal(true)}
|
||||||
|
>
|
||||||
|
<CreateNewFolder />
|
||||||
|
</IconButton>
|
||||||
</MuiLink>
|
</MuiLink>
|
||||||
|
|
||||||
<UploadModal open={uploadModal} setOpen={setUploadModal} path={path} refresh={refreshFolder} />
|
<UploadModal open={uploadModal} setOpen={setUploadModal} path={path} refresh={refreshFolder} />
|
||||||
|
<CreateFolderModal open={createFolderModal} setOpen={setCreateFolderModal} refresh={refreshFolder} />
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
Loading…
Reference in New Issue