From 676f88a7ce0d32fac102c4756df0773acedce35d Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Sat, 27 Apr 2024 14:23:09 -0400 Subject: [PATCH] working file renaming --- backend/server/dist/getSandboxFiles.js | 3 +- backend/server/dist/index.js | 13 ++- backend/server/dist/utils.js | 92 +++++++++++++++++++ backend/server/src/index.ts | 11 ++- .../src/{getSandboxFiles.ts => utils.ts} | 22 ++++- backend/storage/src/index.ts | 14 +++ frontend/components/editor/index.tsx | 34 ++++++- frontend/components/editor/sidebar/file.tsx | 27 +++++- frontend/components/editor/sidebar/folder.tsx | 44 ++++++++- frontend/components/editor/sidebar/index.tsx | 14 ++- 10 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 backend/server/dist/utils.js rename backend/server/src/{getSandboxFiles.ts => utils.ts} (77%) diff --git a/backend/server/dist/getSandboxFiles.js b/backend/server/dist/getSandboxFiles.js index f55aa7b..dbc978c 100644 --- a/backend/server/dist/getSandboxFiles.js +++ b/backend/server/dist/getSandboxFiles.js @@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.getSandboxFiles = void 0; const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () { const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`); const sandboxData = yield sandboxRes.json(); @@ -17,6 +18,7 @@ const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () { // console.log("processedFiles.fileData:", processedFiles.fileData) return processedFiles; }); +exports.getSandboxFiles = getSandboxFiles; const processFiles = (paths, id) => __awaiter(void 0, void 0, void 0, function* () { const root = { id: "/", type: "folder", name: "/", children: [] }; const fileData = []; @@ -75,4 +77,3 @@ const fetchFileContent = (fileId) => __awaiter(void 0, void 0, void 0, function* return ""; } }); -exports.default = getSandboxFiles; diff --git a/backend/server/dist/index.js b/backend/server/dist/index.js index 8818939..6950ecc 100644 --- a/backend/server/dist/index.js +++ b/backend/server/dist/index.js @@ -17,7 +17,7 @@ const dotenv_1 = __importDefault(require("dotenv")); const http_1 = require("http"); const socket_io_1 = require("socket.io"); const zod_1 = require("zod"); -const getSandboxFiles_1 = __importDefault(require("./getSandboxFiles")); +const utils_1 = require("./utils"); dotenv_1.default.config(); const app = (0, express_1.default)(); const port = process.env.PORT || 4000; @@ -64,15 +64,22 @@ io.use((socket, next) => __awaiter(void 0, void 0, void 0, function* () { })); io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { const data = socket.data; - const sandboxFiles = yield (0, getSandboxFiles_1.default)(data.id); + const sandboxFiles = yield (0, utils_1.getSandboxFiles)(data.id); socket.emit("loaded", sandboxFiles.files); socket.on("getFile", (fileId, callback) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId); if (!file) return; - // console.log("file " + file.id + ": ", file.data) + console.log("file " + file.id + ": ", file.data); callback(file.data); }); + socket.on("renameFile", (fileId, newName) => __awaiter(void 0, void 0, void 0, function* () { + const file = sandboxFiles.fileData.find((f) => f.id === fileId); + if (!file) + return; + yield (0, utils_1.renameFile)(fileId, newName, file.data); + file.id = newName; + })); })); httpServer.listen(port, () => { console.log(`Server running on port ${port}`); diff --git a/backend/server/dist/utils.js b/backend/server/dist/utils.js new file mode 100644 index 0000000..3566509 --- /dev/null +++ b/backend/server/dist/utils.js @@ -0,0 +1,92 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.renameFile = exports.getSandboxFiles = void 0; +const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () { + const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`); + const sandboxData = yield sandboxRes.json(); + const paths = sandboxData.objects.map((obj) => obj.key); + const processedFiles = yield processFiles(paths, id); + // console.log("processedFiles.fileData:", processedFiles.fileData) + return processedFiles; +}); +exports.getSandboxFiles = getSandboxFiles; +const processFiles = (paths, id) => __awaiter(void 0, void 0, void 0, function* () { + const root = { id: "/", type: "folder", name: "/", children: [] }; + const fileData = []; + paths.forEach((path) => { + const allParts = path.split("/"); + if (allParts[1] !== id) { + console.log("invalid path!!!!"); + return; + } + const parts = allParts.slice(2); + let current = root; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const isFile = i === parts.length - 1 && part.includes("."); + const existing = current.children.find((child) => child.name === part); + if (existing) { + if (!isFile) { + current = existing; + } + } + else { + if (isFile) { + const file = { id: path, type: "file", name: part }; + current.children.push(file); + fileData.push({ id: path, data: "" }); + } + else { + const folder = { + id: path, // issue todo: for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css + type: "folder", + name: part, + children: [], + }; + current.children.push(folder); + current = folder; + } + } + } + }); + yield Promise.all(fileData.map((file) => __awaiter(void 0, void 0, void 0, function* () { + const data = yield fetchFileContent(file.id); + file.data = data; + }))); + return { + files: root.children, + fileData, + }; +}); +const fetchFileContent = (fileId) => __awaiter(void 0, void 0, void 0, function* () { + try { + const fileRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?fileId=${fileId}`); + return yield fileRes.text(); + } + catch (error) { + console.error("ERROR fetching file:", error); + return ""; + } +}); +const renameFile = (fileId, newName, data) => __awaiter(void 0, void 0, void 0, function* () { + const parts = fileId.split("/"); + const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName; + const res = yield fetch(`https://storage.ishaan1013.workers.dev/api/rename`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ fileId, newFileId, data }), + }); + return res.ok; +}); +exports.renameFile = renameFile; diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index df30fd1..6d1b703 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -5,7 +5,7 @@ import { Server } from "socket.io" import { z } from "zod" import { User } from "./types" -import getSandboxFiles from "./getSandboxFiles" +import { getSandboxFiles, renameFile } from "./utils" dotenv.config() @@ -75,9 +75,16 @@ io.on("connection", async (socket) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId) if (!file) return - // console.log("file " + file.id + ": ", file.data) + console.log("file " + file.id + ": ", file.data) callback(file.data) }) + socket.on("renameFile", async (fileId: string, newName: string) => { + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return + await renameFile(fileId, newName, file.data) + + file.id = newName + }) }) httpServer.listen(port, () => { diff --git a/backend/server/src/getSandboxFiles.ts b/backend/server/src/utils.ts similarity index 77% rename from backend/server/src/getSandboxFiles.ts rename to backend/server/src/utils.ts index b3c3857..4cdd679 100644 --- a/backend/server/src/getSandboxFiles.ts +++ b/backend/server/src/utils.ts @@ -8,7 +8,7 @@ import { User, } from "./types" -const getSandboxFiles = async (id: string) => { +export const getSandboxFiles = async (id: string) => { const sandboxRes = await fetch( `https://storage.ishaan1013.workers.dev/api?sandboxId=${id}` ) @@ -50,7 +50,7 @@ const processFiles = async (paths: string[], id: string) => { fileData.push({ id: path, data: "" }) } else { const folder: TFolder = { - id: path, + id: path, // issue todo: for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css type: "folder", name: part, children: [], @@ -87,4 +87,20 @@ const fetchFileContent = async (fileId: string): Promise => { } } -export default getSandboxFiles +export const renameFile = async ( + fileId: string, + newName: string, + data: string +) => { + const parts = fileId.split("/") + const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName + + const res = await fetch(`https://storage.ishaan1013.workers.dev/api/rename`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ fileId, newFileId, data }), + }) + return res.ok +} diff --git a/backend/storage/src/index.ts b/backend/storage/src/index.ts index 99847f7..af5fbe9 100644 --- a/backend/storage/src/index.ts +++ b/backend/storage/src/index.ts @@ -43,6 +43,20 @@ export default { } else if (method === 'POST') { return new Response('Hello, world!'); } else return methodNotAllowed; + } else if (path === '/api/rename' && method === 'POST') { + const renameSchema = z.object({ + fileId: z.string(), + newFileId: z.string(), + data: z.string(), + }); + + const body = await request.json(); + const { fileId, newFileId, data } = renameSchema.parse(body); + + await env.R2.delete(fileId); + await env.R2.put(newFileId, data); + + return success; } else if (path === '/api/init' && method === 'POST') { const initSchema = z.object({ sandboxId: z.string(), diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index ce18f1b..a327aa8 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -86,6 +86,7 @@ export default function CodeEditor({ setTabs((prev) => { const exists = prev.find((t) => t.id === tab.id) if (exists) { + // console.log("exists") setActiveId(exists.id) return prev } @@ -116,16 +117,45 @@ export default function CodeEditor({ setTabs((prev) => prev.filter((t) => t.id !== tab.id)) } - const handleFileNameChange = (id: string, newName: string) => { + // Note: add renaming validation: + // In general: must not contain / or \ or whitespace, not empty, no duplicates + // Files: must contain dot + // Folders: must not contain dot + + const handleRename = ( + id: string, + newName: string, + oldName: string, + type: "file" | "folder" + ) => { + // Validation + if ( + newName === oldName || + newName.includes("/") || + newName.includes("\\") || + newName.includes(" ") || + (type === "file" && !newName.includes(".")) || + (type === "folder" && newName.includes(".")) + ) { + return false + } + + // Action socket.emit("renameFile", id, newName) setTabs((prev) => prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) ) + + return true } return ( <> - + void + handleRename: ( + id: string, + newName: string, + oldName: string, + type: "file" | "folder" + ) => boolean }) { const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`) const [editing, setEditing] = useState(false) @@ -22,6 +29,19 @@ export default function SidebarFile({ } }, [editing]) + const renameFile = () => { + const renamed = handleRename( + data.id, + inputRef.current?.value ?? data.name, + data.name, + "file" + ) + if (!renamed && inputRef.current) { + inputRef.current.value = data.name + } + setEditing(false) + } + return ( diff --git a/frontend/components/editor/sidebar/folder.tsx b/frontend/components/editor/sidebar/folder.tsx index a275acd..93b8a8e 100644 --- a/frontend/components/editor/sidebar/folder.tsx +++ b/frontend/components/editor/sidebar/folder.tsx @@ -1,7 +1,7 @@ "use client" import Image from "next/image" -import { useState } from "react" +import { useEffect, useRef, useState } from "react" import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js" import { TFile, TFolder, TTab } from "./types" import SidebarFile from "./file" @@ -9,19 +9,38 @@ import SidebarFile from "./file" export default function SidebarFolder({ data, selectFile, + handleRename, }: { data: TFolder selectFile: (file: TTab) => void + handleRename: ( + id: string, + newName: string, + oldName: string, + type: "file" | "folder" + ) => boolean }) { const [isOpen, setIsOpen] = useState(false) const folder = isOpen ? getIconForOpenFolder(data.name) : getIconForFolder(data.name) + const [editing, setEditing] = useState(false) + const inputRef = useRef(null) + + useEffect(() => { + if (editing) { + inputRef.current?.focus() + } + }, [editing]) + return ( <>
setIsOpen((prev) => !prev)} + onDoubleClick={() => { + setEditing(true) + }} className="w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary rounded-sm cursor-pointer" > - {data.name} +
{ + e.preventDefault() + console.log("file renamed") + setEditing(false) + }} + > + { + console.log("file renamed") + setEditing(false) + }} + /> +
{isOpen ? (
@@ -43,12 +81,14 @@ export default function SidebarFolder({ key={child.id} data={child} selectFile={selectFile} + handleRename={handleRename} /> ) : ( ) )} diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx index c72c343..eb90e7c 100644 --- a/frontend/components/editor/sidebar/index.tsx +++ b/frontend/components/editor/sidebar/index.tsx @@ -5,17 +5,19 @@ import SidebarFile from "./file" import SidebarFolder from "./folder" import { TFile, TFolder, TTab } from "./types" -// Note: add renaming validation: -// In general: must not contain / or \ or whitespace, not empty, no duplicates -// Files: must contain dot -// Folders: must not contain dot - export default function Sidebar({ files, selectFile, + handleRename, }: { files: (TFile | TFolder)[] selectFile: (tab: TTab) => void + handleRename: ( + id: string, + newName: string, + oldName: string, + type: "file" | "folder" + ) => boolean }) { return (
@@ -45,12 +47,14 @@ export default function Sidebar({ key={child.id} data={child} selectFile={selectFile} + handleRename={handleRename} /> ) : ( ) )