diff --git a/app.py b/app.py index fe50e73..9a06d7a 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ -from fastapi import FastAPI, HTTPException, Body, Query, Request, BackgroundTasks -from fastapi.responses import JSONResponse, Response, StreamingResponse +from fastapi import FastAPI, HTTPException, Body, Query +from fastapi.middleware.cors import CORSMiddleware +from typing import Annotated, Optional, List from pydantic import BaseModel from pathlib import Path import shutil @@ -284,11 +285,11 @@ async def rename_directory(paths: dict[str, str], verbose: Optional[bool] = Quer successful_paths = {} failed_paths = {} - for old_name, new_name in paths.items(): - old_directory = secure_path(old_name) - new_directory = secure_path(new_name) + for old_name, new_name in files.items(): + old_path = secure_path(old_name) + new_path = secure_path(new_name) - if not old_directory.exists() or not old_directory.is_dir(): + if not old_path.exists() or not old_path.exists(): #raise HTTPException(status_code=404, detail="Directory not found") failed_paths[old_name] = new_name continue @@ -298,7 +299,7 @@ async def rename_directory(paths: dict[str, str], verbose: Optional[bool] = Quer continue try: - old_directory.rename(new_directory) + old_path.rename(new_path) except Exception as e: #raise HTTPException(status_code=500, detail=str(e)) failed_paths[old_name] = new_name diff --git a/client/package-lock.json b/client/package-lock.json index f9bf4cc..e914ef1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "my-project", "version": "0.0.0", "dependencies": { + "axios": "^1.7.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.26.2" @@ -1628,6 +1629,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -1682,6 +1689,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1890,6 +1908,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2064,6 +2094,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2761,6 +2800,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2788,6 +2847,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3791,6 +3864,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4374,6 +4468,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/client/package.json b/client/package.json index b79c5de..0bd0eec 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.26.2" diff --git a/client/src/components/home/BreadCrumb.tsx b/client/src/components/home/BreadCrumb.tsx index bb8dcd2..77b2d5b 100644 --- a/client/src/components/home/BreadCrumb.tsx +++ b/client/src/components/home/BreadCrumb.tsx @@ -30,9 +30,9 @@ const BreadCrumb = () => { > @@ -55,9 +55,9 @@ const BreadCrumb = () => { > diff --git a/client/src/components/home/ConfirmationModal.tsx b/client/src/components/home/ConfirmationModal.tsx new file mode 100644 index 0000000..35fea44 --- /dev/null +++ b/client/src/components/home/ConfirmationModal.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +interface ConfirmationModalProps { + message: string; + onConfirm: () => void; + onCancel: () => void; +} + +const ConfirmationModal: React.FC = ({ message, onConfirm, onCancel }) => { + return ( +
+
+

+ {message} +

+
+ + +
+
+
+ ); +}; + +export default ConfirmationModal; diff --git a/client/src/components/home/ContextMenu.tsx b/client/src/components/home/ContextMenu.tsx index d6df1e5..4c43188 100644 --- a/client/src/components/home/ContextMenu.tsx +++ b/client/src/components/home/ContextMenu.tsx @@ -1,27 +1,38 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect, useState } from "react"; +import ConfirmationModal from "./ConfirmationModal"; +import Notification from "./Notification"; + interface ContextMenuProps { fileName: string; open: string; - modify: string; + onDelete: string; rename: string; + mime_type: string; isOpen: boolean; toggleMenu: () => void; + refreshData: () => void; } const ContextMenu: React.FC = ({ fileName, open, - modify, onDelete, rename, + mime_type, isOpen, toggleMenu, + refreshData, + }) => { const menuRef = useRef(null); const buttonRef = useRef(null); + const [showConfirmation, setShowConfirmation] = useState(false); + const [notificationMessage, setNotificationMessage] = useState(""); + const [showNotification, setShowNotification] = useState(false); + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( @@ -45,13 +56,27 @@ const ContextMenu: React.FC = ({ }; }, [isOpen, toggleMenu]); - const handleDelete = async () => { + const showNotificationMessage = (message: string) => { + setNotificationMessage(message); + setShowNotification(false); + setTimeout(() => setShowNotification(true), 0); + }; + + // DELETE FILE + const handleDeleteFile = async () => { try { - const response = await fetch(`/api/files/${fileName}`, { - method: "DELETE", - }); + const response = await fetch( + `http://127.0.0.1:8000/api/file?path=${encodeURIComponent(fileName)}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); if (response.ok) { - console.log(`File ${fileName} deleted successfully`); + showNotificationMessage(`File ${fileName} deleted successfully`); + refreshData(); } else { console.error("Failed to delete file"); } @@ -60,17 +85,47 @@ const ContextMenu: React.FC = ({ } }; + // DELETE DIRECTORY + const handleDeleteDirectory = async () => { + try { + const response = await fetch( + `http://127.0.0.1:8000/api/directory?path=${encodeURIComponent(fileName)}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + if (response.ok) { + showNotificationMessage(`Directory ${fileName} deleted successfully`); + refreshData(); + } else { + console.error("Failed to delete directory"); + } + } catch (error) { + console.error("Error deleting directory:", error); + } + }; + + // OPEN const handleOpen = async () => { try { const response = await fetch(`/api/files/${fileName}`, { method: "OPEN", }); if (response.ok) { - console.log(`File ${fileName} openedsuccessfully`); + console.log(`File ${fileName} opened successfully`); } else { console.error("Failed to open file"); } } catch (error) { + console.error("Error opening file:", error); + } + }; + + // RENAME +======= console.error("Error openingß file:", error); } }; @@ -82,17 +137,22 @@ const ContextMenu: React.FC = ({ const newName = prompt("Enter new file name:"); if (newName) { try { - const response = await fetch(`/api/files/${fileName}/rename`, { - method: "POST", + const response = await fetch(`http://127.0.0.1:8000/api/file`, { + method: "PATCH", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ newName }), + body: JSON.stringify({ + [fileName]: newName, + }), }); if (response.ok) { - console.log(`File ${fileName} renamed to ${newName} successfully`); + showNotificationMessage(`File ${fileName} renamed to ${newName} successfully`); + setShowNotification(true); + refreshData(); } else { - console.error("Failed to rename file"); + const errorData = await response.json(); + console.error("Failed to rename file:", errorData.details || response.statusText); } } catch (error) { console.error("Error renaming file:", error); @@ -100,6 +160,23 @@ const ContextMenu: React.FC = ({ } }; + const confirmDelete = () => { + setShowConfirmation(true); + }; + + const handleConfirmDelete = () => { + setShowConfirmation(false); + if (mime_type === "inode/directory") { + handleDeleteDirectory(); + } else { + handleDeleteFile(); + } + }; + + const handleCancelDelete = () => { + setShowConfirmation(false); + }; + return (
= ({ !isOpen ? "hidden" : "" }`} > -
    +
    • -
    • -
    • -
+ {showConfirmation && ( + + )} + + {showNotification && ( + setShowNotification(false)} + /> + )} ); }; diff --git a/client/src/components/home/Header.tsx b/client/src/components/home/Header.tsx index 1a87158..8a8d3ad 100644 --- a/client/src/components/home/Header.tsx +++ b/client/src/components/home/Header.tsx @@ -24,9 +24,9 @@ const Header = () => { > @@ -43,9 +43,9 @@ const Header = () => { > @@ -75,9 +75,9 @@ const Header = () => { > diff --git a/client/src/components/home/Notification.tsx b/client/src/components/home/Notification.tsx new file mode 100644 index 0000000..f9f2cf6 --- /dev/null +++ b/client/src/components/home/Notification.tsx @@ -0,0 +1,25 @@ +import React, { useEffect } from "react"; + +interface NotificationProps { + message: string; + onClose: () => void; +} + +const Notification: React.FC = ({ message, onClose }) => { + useEffect(() => { + const timer = setTimeout(onClose, 3000); + return () => clearTimeout(timer); + }, [onClose]); + + return ( +
+ {message} + +
+ ); +}; + +export default Notification; diff --git a/client/src/components/home/RecentFileCard.tsx b/client/src/components/home/RecentFileCard.tsx index d1ac7f7..2ed0f8d 100644 --- a/client/src/components/home/RecentFileCard.tsx +++ b/client/src/components/home/RecentFileCard.tsx @@ -4,20 +4,22 @@ import ContextMenu from "./ContextMenu"; interface RecentFileCardProps { file: { - name: string; + fileName: string; created: string; modified: string; imagepath: string; - mime: string; + mime_type: string; }; isOpen: boolean; toggleMenu: () => void; + refreshData: () => void; } const RecentFileCard: React.FC = ({ file, isOpen, toggleMenu, + refreshData }) => { const formatDate = (epoch: string) => { const date = new Date(parseInt(epoch) * 1000); @@ -30,6 +32,7 @@ const RecentFileCard: React.FC = ({ }; const removeFileExtension = (filename: string) => { + if (!filename) return ""; return filename.replace(/\.[^/.]+$/, ""); }; @@ -39,25 +42,26 @@ const RecentFileCard: React.FC = ({

- {removeFileExtension(file.name)} + {removeFileExtension(file.fileName)}

diff --git a/client/src/components/home/RecentFileGrid.tsx b/client/src/components/home/RecentFileGrid.tsx index bc52a72..22cb7d1 100644 --- a/client/src/components/home/RecentFileGrid.tsx +++ b/client/src/components/home/RecentFileGrid.tsx @@ -1,17 +1,18 @@ import React, { useState } from "react"; -import FileCard2 from "./RecentFileCard"; +import RecentFileCard from "./RecentFileCard"; interface File { files: { - name: string; + fileName: string; created: string; modified: string; imagepath: string; - mime: string; + mime_type: string; }[]; + refreshData: () => void; } -const RecentFilesGrid: React.FC = ({ files }) => { +const RecentFilesGrid: React.FC = ({ files, refreshData }) => { const [openMenuIndex, setOpenMenuIndex] = useState(null); const toggleMenu = (index: number) => { @@ -25,11 +26,12 @@ const RecentFilesGrid: React.FC = ({ files }) => { return (
{files.map((file, index) => ( - toggleMenu(index)} // Pass the toggle function + isOpen={openMenuIndex === index} + toggleMenu={() => toggleMenu(index)} + refreshData={refreshData} /> ))}
diff --git a/client/src/components/home/RecentFilesTable.tsx b/client/src/components/home/RecentFilesTable.tsx index a3519a1..4426384 100644 --- a/client/src/components/home/RecentFilesTable.tsx +++ b/client/src/components/home/RecentFilesTable.tsx @@ -3,16 +3,17 @@ import ContextMenu from "./ContextMenu"; interface RecentFilesTableProps { files: { - name: string; + fileName: string; created: string; modified: string; size: number; imagepath: string; - mime: string; + mime_type: string; }[]; + refreshData: () => void } -const RecentFilesTable: React.FC = ({ files }) => { +const RecentFilesTable: React.FC = ({ files, refreshData }) => { const [openMenuIndex, setOpenMenuIndex] = useState(null); const formatDate = (epoch: string) => { @@ -24,7 +25,9 @@ const RecentFilesTable: React.FC = ({ files }) => { }); }; - const formatFileSize = (size: number) => { + const formatFileSize = (size: number, mime: string) => { + console.log(mime); + if (mime === "inode/directory") return "-"; if (size === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; @@ -35,6 +38,7 @@ const RecentFilesTable: React.FC = ({ files }) => { const removeFileExtension = (filename: string) => { + if (!filename) return ""; return filename.replace(/\.[^/.]+$/, ""); }; @@ -63,28 +67,25 @@ const RecentFilesTable: React.FC = ({ files }) => { className="border-b border-gray-300 dark:border-gray-700" > - {file.name} - {removeFileExtension(file.name)} + + {removeFileExtension(file.fileName)} {formatDate(file.modified)} - {formatFileSize(file.size)} + {formatFileSize(file.size, file.mime_type)} - {/* Aligning the Context Menu to the right */} + toggleMenu(index)} + refreshData={refreshData} /> diff --git a/client/src/components/home/SuggestedFileCard.tsx b/client/src/components/home/SuggestedFileCard.tsx index 0155233..294bcb4 100644 --- a/client/src/components/home/SuggestedFileCard.tsx +++ b/client/src/components/home/SuggestedFileCard.tsx @@ -1,53 +1,76 @@ -import React from "react"; +import React, { useState } from "react"; import { Link } from "react-router-dom"; +import ContextMenu from "./ContextMenu"; +import { faRefresh } from "@fortawesome/free-solid-svg-icons"; -interface FileCardProps { +interface SuggestedFileCardProps { file: { - name: string; + fileName: string; created: string; modified: string; imagepath: string; - mime: string; + mime_type: string; }; + refreshData: () => void; } -const FileCard: React.FC = ({ file }) => { +const SuggestedFileCard: React.FC = ({ file, refreshData }) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const formatDate = (epoch: string) => { const date = new Date(parseInt(epoch) * 1000); return date.toLocaleDateString("en-US", { - weekday: "long", year: "numeric", month: "long", day: "numeric", }); }; - const removeFileExtension = (filename: string) => { - return filename.replace(/\.[^/.]+$/, ""); + const removeFileExtension = (fileName: string) => { + if (!fileName) return "Unnamed File"; + return fileName.replace(/\.[^/.]+$/, ""); + }; + + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); }; return ( - - -
-

- {removeFileExtension(file.name)} -

-

- {formatDate(file.modified)} -

+
+ + +
+

+ {removeFileExtension(file.fileName)} +

+

+ {formatDate(file.modified)} +

+
+ +
+
- +
); }; -export default FileCard; +export default SuggestedFileCard; diff --git a/client/src/components/home/SuggestedFilesGrid.tsx b/client/src/components/home/SuggestedFilesGrid.tsx index 661b1ea..b933669 100644 --- a/client/src/components/home/SuggestedFilesGrid.tsx +++ b/client/src/components/home/SuggestedFilesGrid.tsx @@ -1,21 +1,22 @@ import React from "react"; -import FileCard from "./SuggestedFileCard"; +import SuggestedFileCard from "./SuggestedFileCard"; interface SuggestedFilesGridProps { files: { - name: string; + fileName: string; created: string; modified: string; imagepath: string; - mime: string; + mime_type: string; }[]; + refreshData: () => void; } -const SuggestedFilesGrid: React.FC = ({ files }) => { +const SuggestedFilesGrid: React.FC = ({ files, refreshData }) => { return (
- {files.map((file, index) => ( - + {files.slice(0, 10).map((file, index) => ( + ))}
); diff --git a/client/src/components/home/sections/Home.tsx b/client/src/components/home/sections/Home.tsx index e6f91e1..64f5ab3 100644 --- a/client/src/components/home/sections/Home.tsx +++ b/client/src/components/home/sections/Home.tsx @@ -1,18 +1,19 @@ -import React, { FC, useState, useEffect } from "react"; +import React, { FC, useState, useEffect, useCallback} from "react"; import LoadingIndicator from "../../../components/home/LoadingIndicator"; import ErrorDisplay from "../../../components/home/ErrorDisplay"; import SuggestedFilesGrid from "../../../components/home/SuggestedFilesGrid"; import RecentFilesGrid from "../../../components/home/RecentFileGrid"; import RecentFilesTable from "../../../components/home/RecentFilesTable"; import ButtonGroup from "../../../components/home/ButtonGroup"; -import { getFilesFromAPI } from "../../../data/apiService"; +import axios from "axios"; interface File { - name: string; + fileName: string; created: string; modified: string; imagepath: string; - mime: string; + mime_type: string; + size: number; } interface APIResponse { @@ -25,19 +26,34 @@ const Home: FC = () => { const [error, setError] = useState(null); const [view, setView] = useState(2); - useEffect(() => { - const fetchData = async () => { - try { - const result: APIResponse = await getFilesFromAPI(); - setFiles(result.files); + const fetchData = useCallback(() => { + setLoading(true); + axios + .get("http://127.0.0.1:8000/api/directory", { + params: { + path: ".", + }, + }) + .then((response) => { + const directories = response.data.directories; + const requestedPath = "."; + if (directories && Array.isArray(directories[requestedPath])) { + setFiles(directories[requestedPath]); + } else { + setFiles([]); + setError("Unexpected response format."); + } setLoading(false); - } catch (error) { - setError("Failed to fetch data"); + }) + .catch((err) => { + setError("Failed to fetch files."); setLoading(false); - } - }; + }); + }, []); + + useEffect(() => { fetchData(); - }, []); + }, [fetchData]); if (loading) { return ( @@ -55,7 +71,7 @@ const Home: FC = () => {

For you

- +
@@ -64,7 +80,7 @@ const Home: FC = () => {
- {view === 1 ? : } + {view === 1 ? : }
); diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx index af93179..e3b76d1 100644 --- a/client/src/pages/HomePage.tsx +++ b/client/src/pages/HomePage.tsx @@ -31,7 +31,7 @@ const HomePage: FC = () => { }; return ( -
+
{/* Pass the setOpenSection function to Sidebar */}