diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 89a33c7..4f45cbe 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -13,6 +13,7 @@ import { createFile, deleteFile, generateCode, + getFolder, getProjectSize, getSandboxFiles, renameFile, @@ -130,6 +131,11 @@ io.on("connection", async (socket) => { callback(file.data) }) + socket.on("getFolder", async (folderId: string, callback) => { + const files = await getFolder(folderId) + callback(files) + }) + // todo: send diffs + debounce for efficiency socket.on("saveFile", async (fileId: string, body: string) => { try { @@ -264,6 +270,33 @@ io.on("connection", async (socket) => { } }) + socket.on("renameFolder", async (folderId: string, newName: string) => { + // todo + }) + + socket.on("deleteFolder", async (folderId: string, callback) => { + const files = await getFolder(folderId) + + console.log("deleting folder", folderId, files) + + await Promise.all(files.map(async (file) => { + fs.unlink(path.join(dirName, file), function (err) { + if (err) throw err + }) + + sandboxFiles.fileData = sandboxFiles.fileData.filter( + (f) => f.id !== file + ) + + await deleteFile(file) + })) + + const newFiles = await getSandboxFiles(data.sandboxId) + + callback(newFiles.files) + + }) + socket.on("createTerminal", (id: string, callback) => { console.log("creating terminal", id) if (terminals[id] || Object.keys(terminals).length >= 4) { @@ -339,6 +372,7 @@ io.on("connection", async (socket) => { instructions: string, callback ) => { + // Log code generation credit in DB const fetchPromise = fetch(`https://database.ishaan1013.workers.dev/api/sandbox/generate`, { method: "POST", headers: { @@ -349,6 +383,7 @@ io.on("connection", async (socket) => { }), }) + // Generate code from cloudflare workers AI const generateCodePromise = generateCode({ fileName, code, diff --git a/backend/server/src/ratelimit.ts b/backend/server/src/ratelimit.ts index 8b99ef7..a199e03 100644 --- a/backend/server/src/ratelimit.ts +++ b/backend/server/src/ratelimit.ts @@ -1,23 +1,28 @@ import { RateLimiterMemory } from "rate-limiter-flexible" export const saveFileRL = new RateLimiterMemory({ - points: 3, + points: 2, duration: 1, }) export const MAX_BODY_SIZE = 5 * 1024 * 1024 export const createFileRL = new RateLimiterMemory({ - points: 3, - duration: 1, + points: 1, + duration: 2, }) export const renameFileRL = new RateLimiterMemory({ - points: 3, - duration: 1, + points: 1, + duration: 2, }) export const deleteFileRL = new RateLimiterMemory({ - points: 3, - duration: 1, + points: 1, + duration: 2, }) + +export const deleteFolderRL = new RateLimiterMemory({ + points: 1, + duration: 2, +}) \ No newline at end of file diff --git a/backend/server/src/utils.ts b/backend/server/src/utils.ts index 73a9aba..1e758a2 100644 --- a/backend/server/src/utils.ts +++ b/backend/server/src/utils.ts @@ -10,16 +10,25 @@ import { } from "./types" export const getSandboxFiles = async (id: string) => { - const sandboxRes = await fetch( + const res = await fetch( `https://storage.ishaan1013.workers.dev/api?sandboxId=${id}` ) - const sandboxData: R2Files = await sandboxRes.json() + const data: R2Files = await res.json() - const paths = sandboxData.objects.map((obj) => obj.key) + const paths = data.objects.map((obj) => obj.key) const processedFiles = await processFiles(paths, id) return processedFiles } +export const getFolder = async (folderId: string) => { + const res = await fetch( + `https://storage.ishaan1013.workers.dev/api?folderId=${folderId}` + ) + const data: R2Files = await res.json() + + return data.objects.map((obj) => obj.key) +} + const processFiles = async (paths: string[], id: string) => { const root: TFolder = { id: "/", type: "folder", name: "/", children: [] } const fileData: TFileData[] = [] diff --git a/backend/storage/src/index.ts b/backend/storage/src/index.ts index 5050be1..9d8953a 100644 --- a/backend/storage/src/index.ts +++ b/backend/storage/src/index.ts @@ -36,11 +36,15 @@ export default { if (method === 'GET') { const params = url.searchParams; const sandboxId = params.get('sandboxId'); + const folderId = params.get('folderId'); const fileId = params.get('fileId'); if (sandboxId) { const res = await env.R2.list({ prefix: `projects/${sandboxId}` }); return new Response(JSON.stringify(res), { status: 200 }); + } else if (folderId) { + const res = await env.R2.list({ prefix: folderId }); + return new Response(JSON.stringify(res), { status: 200 }); } else if (fileId) { const obj = await env.R2.get(fileId); if (obj === null) { diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 41ec0b0..3ff0949 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -58,6 +58,7 @@ export default function CodeEditor({ const [tabs, setTabs] = useState([]); const [activeFileId, setActiveFileId] = useState(""); const [activeFileContent, setActiveFileContent] = useState(""); + const [deletingFolderId, setDeletingFolderId] = useState(""); // Editor state const [editorLanguage, setEditorLanguage] = useState("plaintext"); @@ -435,14 +436,16 @@ export default function CodeEditor({ }; // Close tab and remove from tabs - const closeTab = (tab: TFile) => { + const closeTab = (id: string) => { const numTabs = tabs.length; - const index = tabs.findIndex((t) => t.id === tab.id); + const index = tabs.findIndex((t) => t.id === id); + + console.log("closing tab", id, index); if (index === -1) return; const nextId = - activeFileId === tab.id + activeFileId === id ? numTabs === 1 ? null : index < numTabs - 1 @@ -450,7 +453,7 @@ export default function CodeEditor({ : tabs[index - 1].id : activeFileId; - setTabs((prev) => prev.filter((t) => t.id !== tab.id)); + setTabs((prev) => prev.filter((t) => t.id !== id)); if (!nextId) { setActiveFileId(""); @@ -462,6 +465,36 @@ export default function CodeEditor({ } }; + const closeTabs = (ids: string[]) => { + const numTabs = tabs.length; + + if (numTabs === 0) return; + + const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id)); + + const indexes = allIndexes.filter((index) => index !== -1); + if (indexes.length === 0) return; + + console.log("closing tabs", ids, indexes); + + const activeIndex = tabs.findIndex((t) => t.id === activeFileId); + + const newTabs = tabs.filter((t) => !ids.includes(t.id)); + setTabs(newTabs); + + if (indexes.length === numTabs) { + setActiveFileId(""); + } else { + const nextTab = + newTabs.length > activeIndex + ? newTabs[activeIndex] + : newTabs[newTabs.length - 1]; + if (nextTab) { + selectFile(nextTab); + } + } + }; + const handleRename = ( id: string, newName: string, @@ -486,13 +519,25 @@ export default function CodeEditor({ socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { setFiles(response); }); - closeTab(file); + closeTab(file.id); }; const handleDeleteFolder = (folder: TFolder) => { - // socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { - // setFiles(response) - // }) + setDeletingFolderId(folder.id); + console.log("deleting folder", folder.id); + + socket.emit("getFolder", folder.id, (response: string[]) => + closeTabs(response) + ); + + socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { + setFiles(response); + setDeletingFolderId(""); + }); + + setTimeout(() => { + setDeletingFolderId(""); + }, 3000); }; // On disabled access for shared users, show un-interactable loading placeholder + info modal @@ -581,6 +626,7 @@ export default function CodeEditor({ // setFiles(prev => [...prev, { id, name, type: "folder", children: [] }]) } }} + deletingFolderId={deletingFolderId} // AI Copilot Toggle ai={ai} setAi={setAi} @@ -604,7 +650,7 @@ export default function CodeEditor({ onClick={(e) => { selectFile(tab); }} - onClose={() => closeTab(tab)} + onClose={() => closeTab(tab.id)} > {tab.name} diff --git a/frontend/components/editor/sidebar/file.tsx b/frontend/components/editor/sidebar/file.tsx index 6b00b86..14bc0da 100644 --- a/frontend/components/editor/sidebar/file.tsx +++ b/frontend/components/editor/sidebar/file.tsx @@ -20,6 +20,7 @@ export default function SidebarFile({ handleRename, handleDeleteFile, movingId, + deletingFolderId, }: { data: TFile; selectFile: (file: TTab) => void; @@ -31,8 +32,11 @@ export default function SidebarFile({ ) => boolean; handleDeleteFile: (file: TFile) => void; movingId: string; + deletingFolderId: string; }) { const isMoving = movingId === data.id; + const isDeleting = + deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId); const ref = useRef(null); // for draggable const [dragging, setDragging] = useState(false); @@ -40,7 +44,11 @@ export default function SidebarFile({ const inputRef = useRef(null); const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`); const [editing, setEditing] = useState(false); - const [pendingDelete, setPendingDelete] = useState(false); + const [pendingDelete, setPendingDelete] = useState(isDeleting); + + useEffect(() => { + setPendingDelete(isDeleting); + }, [isDeleting]); useEffect(() => { const el = ref.current; @@ -104,8 +112,9 @@ export default function SidebarFile({ ) : pendingDelete ? ( <> - -
Deleting...
+
+ Deleting... +
) : (
void; @@ -33,10 +36,14 @@ export default function SidebarFolder({ handleDeleteFile: (file: TFile) => void; handleDeleteFolder: (folder: TFolder) => void; movingId: string; + deletingFolderId: string; }) { const ref = useRef(null); // drop target const [isDraggedOver, setIsDraggedOver] = useState(false); + const isDeleting = + deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId); + useEffect(() => { const el = ref.current; @@ -58,9 +65,10 @@ export default function SidebarFolder({ // return !file; // }, + // no dropping while awaiting move canDrop: () => { return !movingId; - }, // no dropping while awaiting move + }, }); }, []); @@ -69,28 +77,20 @@ export default function SidebarFolder({ ? getIconForOpenFolder(data.name) : getIconForFolder(data.name); - const [editing, setEditing] = useState(false); const inputRef = useRef(null); + // const [editing, setEditing] = useState(false); - useEffect(() => { - if (editing) { - inputRef.current?.focus(); - } - }, [editing]); - - // return ( - //
- //
- // ) + // useEffect(() => { + // if (editing) { + // inputRef.current?.focus(); + // } + // }, [editing]); return ( setIsOpen((prev) => !prev)} className={`${ isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm" @@ -103,13 +103,26 @@ export default function SidebarFolder({ height={18} className="mr-2" /> - { - e.preventDefault(); - setEditing(false); - }} - > - +
+ Deleting... +
+ + ) : ( + { + // e.preventDefault(); + // setEditing(false); + // }} + > + + {/* { setEditing(false); }} - /> - + /> */} + + )}
{ - setEditing(true); - }} + disabled + // onClick={() => { + // setEditing(true); + // }} > Rename { - console.log("delete"); - // setPendingDelete(true) - // handleDeleteFile(data) + handleDeleteFolder(data); }} > @@ -160,6 +173,7 @@ export default function SidebarFolder({ handleRename={handleRename} handleDeleteFile={handleDeleteFile} movingId={movingId} + deletingFolderId={deletingFolderId} /> ) : ( ) )} diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx index e95b0db..909f3b5 100644 --- a/frontend/components/editor/sidebar/index.tsx +++ b/frontend/components/editor/sidebar/index.tsx @@ -26,6 +26,7 @@ export default function Sidebar({ addNew, ai, setAi, + deletingFolderId, }: { sandboxData: Sandbox; files: (TFile | TFolder)[]; @@ -43,6 +44,7 @@ export default function Sidebar({ addNew: (name: string, type: "file" | "folder") => void; ai: boolean; setAi: React.Dispatch>; + deletingFolderId: string; }) { const ref = useRef(null); // drop target @@ -147,6 +149,7 @@ export default function Sidebar({ handleRename={handleRename} handleDeleteFile={handleDeleteFile} movingId={movingId} + deletingFolderId={deletingFolderId} /> ) : ( ) )}