From bce9d11b3bbcc48c3ffac46f196d111489f7063e Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Mon, 29 Apr 2024 00:50:25 -0400 Subject: [PATCH] adding file logic --- .gitignore | 4 +- backend/server/dist/index.js | 48 +++++++++++++++-- backend/server/dist/terminal.js | 5 +- backend/server/dist/utils.js | 17 ++++-- backend/server/src/index.ts | 57 ++++++++++++++++++-- backend/server/src/terminal.ts | 6 ++- backend/server/src/utils.ts | 16 ++++-- backend/storage/src/index.ts | 11 +++- frontend/components/editor/index.tsx | 31 ++++++----- frontend/components/editor/sidebar/index.tsx | 12 ++++- frontend/components/editor/sidebar/new.tsx | 22 ++++++-- frontend/lib/utils.ts | 23 ++++++++ 12 files changed, 208 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index f5a1ce5..e6538d8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,6 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -wrangler.toml \ No newline at end of file +wrangler.toml + +backend/server/projects \ No newline at end of file diff --git a/backend/server/dist/index.js b/backend/server/dist/index.js index e8f567c..d35a61b 100644 --- a/backend/server/dist/index.js +++ b/backend/server/dist/index.js @@ -12,6 +12,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const express_1 = __importDefault(require("express")); const dotenv_1 = __importDefault(require("dotenv")); const http_1 = require("http"); @@ -30,6 +32,7 @@ const io = new socket_io_1.Server(httpServer, { }, }); const terminals = {}; +const dirName = path_1.default.join(__dirname, ".."); const handshakeSchema = zod_1.z.object({ userId: zod_1.z.string(), sandboxId: zod_1.z.string(), @@ -67,12 +70,21 @@ 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, utils_1.getSandboxFiles)(data.id); + sandboxFiles.fileData.forEach((file) => { + const filePath = path_1.default.join(dirName, file.id); + fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true }); + fs_1.default.writeFile(filePath, file.data, function (err) { + if (err) + throw err; + // console.log("Saved File:", file.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("get file " + file.id + ": ", file.data.slice(0, 10) + "..."); + // console.log("get file " + file.id + ": ", file.data.slice(0, 10) + "...") callback(file.data); }); // todo: send diffs + debounce for efficiency @@ -81,19 +93,47 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { if (!file) return; file.data = body; - console.log("save file " + file.id + ": ", file.data); + // console.log("save file " + file.id + ": ", file.data) + fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) { + if (err) + throw err; + }); yield (0, utils_1.saveFile)(fileId, body); })); + socket.on("createFile", (name) => __awaiter(void 0, void 0, void 0, function* () { + const id = `projects/${data.id}/${name}`; + console.log("create file", id, name); + fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) { + if (err) + throw err; + }); + sandboxFiles.files.push({ + id, + name, + type: "file", + }); + sandboxFiles.fileData.push({ + id, + data: "", + }); + yield (0, utils_1.createFile)(id); + })); 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; file.id = newName; - yield (0, utils_1.renameFile)(fileId, newName, file.data); + const parts = fileId.split("/"); + const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName; + fs_1.default.rename(path_1.default.join(dirName, fileId), path_1.default.join(dirName, newFileId), function (err) { + if (err) + throw err; + }); + yield (0, utils_1.renameFile)(fileId, newFileId, file.data); })); socket.on("createTerminal", ({ id }) => { console.log("creating terminal (" + id + ")"); - terminals[id] = new terminal_1.Pty(socket, id); + terminals[id] = new terminal_1.Pty(socket, id, `/projects/${data.id}`); }); socket.on("terminalData", ({ id, data }) => { console.log(`Received data for terminal ${id}: ${data}`); diff --git a/backend/server/dist/terminal.js b/backend/server/dist/terminal.js index b56bb6b..301f8f5 100644 --- a/backend/server/dist/terminal.js +++ b/backend/server/dist/terminal.js @@ -7,13 +7,14 @@ exports.Pty = void 0; const node_pty_1 = require("node-pty"); const os_1 = __importDefault(require("os")); class Pty { - constructor(socket, id) { + constructor(socket, id, cwd) { this.socket = socket; this.shell = os_1.default.platform() === "win32" ? "cmd.exe" : "bash"; + this.id = id; this.ptyProcess = (0, node_pty_1.spawn)(this.shell, [], { name: "xterm", cols: 100, - cwd: `/temp`, + cwd: cwd, // env: process.env as { [key: string]: string }, }); this.ptyProcess.onData((data) => { diff --git a/backend/server/dist/utils.js b/backend/server/dist/utils.js index 982bbc6..a2e19ad 100644 --- a/backend/server/dist/utils.js +++ b/backend/server/dist/utils.js @@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.saveFile = exports.renameFile = exports.getSandboxFiles = void 0; +exports.saveFile = exports.renameFile = exports.createFile = 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(); @@ -77,9 +77,18 @@ const fetchFileContent = (fileId) => __awaiter(void 0, void 0, void 0, function* 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 createFile = (fileId) => __awaiter(void 0, void 0, void 0, function* () { + const res = yield fetch(`https://storage.ishaan1013.workers.dev/api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ fileId }), + }); + return res.ok; +}); +exports.createFile = createFile; +const renameFile = (fileId, newFileId, data) => __awaiter(void 0, void 0, void 0, function* () { const res = yield fetch(`https://storage.ishaan1013.workers.dev/api/rename`, { method: "POST", headers: { diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index a707a1a..62097a5 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -1,3 +1,5 @@ +import fs from "fs" +import path from "path" import express, { Express, NextFunction, Request, Response } from "express" import dotenv from "dotenv" import { createServer } from "http" @@ -5,7 +7,7 @@ import { Server } from "socket.io" import { z } from "zod" import { User } from "./types" -import { getSandboxFiles, renameFile, saveFile } from "./utils" +import { createFile, getSandboxFiles, renameFile, saveFile } from "./utils" import { Pty } from "./terminal" dotenv.config() @@ -22,6 +24,8 @@ const io = new Server(httpServer, { const terminals: { [id: string]: Pty } = {} +const dirName = path.join(__dirname, "..") + const handshakeSchema = z.object({ userId: z.string(), sandboxId: z.string(), @@ -72,6 +76,14 @@ io.on("connection", async (socket) => { } const sandboxFiles = await getSandboxFiles(data.id) + sandboxFiles.fileData.forEach((file) => { + const filePath = path.join(dirName, file.id) + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + fs.writeFile(filePath, file.data, function (err) { + if (err) throw err + // console.log("Saved File:", file.id) + }) + }) socket.emit("loaded", sandboxFiles.files) @@ -79,7 +91,7 @@ io.on("connection", async (socket) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId) if (!file) return - console.log("get file " + file.id + ": ", file.data.slice(0, 10) + "...") + // console.log("get file " + file.id + ": ", file.data.slice(0, 10) + "...") callback(file.data) }) @@ -88,22 +100,57 @@ io.on("connection", async (socket) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId) if (!file) return file.data = body + // console.log("save file " + file.id + ": ", file.data) - console.log("save file " + file.id + ": ", file.data) + fs.writeFile(path.join(dirName, file.id), body, function (err) { + if (err) throw err + }) await saveFile(fileId, body) }) + socket.on("createFile", async (name: string) => { + const id = `projects/${data.id}/${name}` + console.log("create file", id, name) + + fs.writeFile(path.join(dirName, id), "", function (err) { + if (err) throw err + }) + + sandboxFiles.files.push({ + id, + name, + type: "file", + }) + + sandboxFiles.fileData.push({ + id, + data: "", + }) + + await createFile(id) + }) + socket.on("renameFile", async (fileId: string, newName: string) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId) if (!file) return file.id = newName - await renameFile(fileId, newName, file.data) + const parts = fileId.split("/") + const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName + + fs.rename( + path.join(dirName, fileId), + path.join(dirName, newFileId), + function (err) { + if (err) throw err + } + ) + await renameFile(fileId, newFileId, file.data) }) socket.on("createTerminal", ({ id }: { id: string }) => { console.log("creating terminal (" + id + ")") - terminals[id] = new Pty(socket, id) + terminals[id] = new Pty(socket, id, `/projects/${data.id}`) }) socket.on("terminalData", ({ id, data }: { id: string; data: string }) => { diff --git a/backend/server/src/terminal.ts b/backend/server/src/terminal.ts index 6d984ae..b118850 100644 --- a/backend/server/src/terminal.ts +++ b/backend/server/src/terminal.ts @@ -6,15 +6,17 @@ export class Pty { socket: Socket ptyProcess: IPty shell: string + id: string - constructor(socket: Socket, id: string) { + constructor(socket: Socket, id: string, cwd: string) { this.socket = socket this.shell = os.platform() === "win32" ? "cmd.exe" : "bash" + this.id = id this.ptyProcess = spawn(this.shell, [], { name: "xterm", cols: 100, - cwd: `/temp`, + cwd: cwd, // env: process.env as { [key: string]: string }, }) diff --git a/backend/server/src/utils.ts b/backend/server/src/utils.ts index 314ee0a..da8716e 100644 --- a/backend/server/src/utils.ts +++ b/backend/server/src/utils.ts @@ -87,14 +87,22 @@ const fetchFileContent = async (fileId: string): Promise => { } } +export const createFile = async (fileId: string) => { + const res = await fetch(`https://storage.ishaan1013.workers.dev/api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ fileId }), + }) + return res.ok +} + export const renameFile = async ( fileId: string, - newName: string, + newFileId: 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: { diff --git a/backend/storage/src/index.ts b/backend/storage/src/index.ts index 5054a6e..ccca6da 100644 --- a/backend/storage/src/index.ts +++ b/backend/storage/src/index.ts @@ -41,7 +41,16 @@ export default { }); } else return invalidRequest; } else if (method === 'POST') { - return new Response('Hello, world!'); + const createSchema = z.object({ + fileId: z.string(), + }); + + const body = await request.json(); + const { fileId } = createSchema.parse(body); + + await env.R2.put(fileId, ''); + + return success; } else return methodNotAllowed; } else if (path === '/api/rename' && method === 'POST') { const renameSchema = z.object({ diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index cfa39bb..a272b29 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -23,7 +23,7 @@ import { useClerk } from "@clerk/nextjs" import { TFile, TFileData, TFolder, TTab } from "./sidebar/types" import { io } from "socket.io-client" -import { processFileType } from "@/lib/utils" +import { processFileType, validateName } from "@/lib/utils" import { toast } from "sonner" import EditorTerminal from "./terminal" @@ -158,21 +158,7 @@ export default function CodeEditor({ oldName: string, type: "file" | "folder" ) => { - // Validation - if (newName === oldName) { - return false - } - - if ( - newName.includes("/") || - newName.includes("\\") || - newName.includes(" ") || - (type === "file" && !newName.includes(".")) || - (type === "folder" && newName.includes(".")) - ) { - toast.error("Invalid file name.") - return false - } + if (!validateName(newName, oldName, type)) return false // Action socket.emit("renameFile", id, newName) @@ -189,6 +175,19 @@ export default function CodeEditor({ files={files} selectFile={selectFile} handleRename={handleRename} + socket={socket} + addNew={(name, type) => { + if (type === "file") { + console.log("adding file") + setFiles((prev) => [ + ...prev, + { id: `projects/${sandboxId}/${name}`, name, type: "file" }, + ]) + } else { + console.log("adding folder") + // setFiles(prev => [...prev, { id, name, type: "folder", children: [] }]) + } + }} /> void @@ -20,6 +23,8 @@ export default function Sidebar({ oldName: string, type: "file" | "folder" ) => boolean + socket: Socket + addNew: (name: string, type: "file" | "folder") => void }) { const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null) @@ -73,8 +78,13 @@ export default function Sidebar({ )} {creatingNew !== null ? ( setCreatingNew(null)} + stopEditing={() => { + console.log("stopped editing") + setCreatingNew(null) + }} + addNew={addNew} /> ) : null} diff --git a/frontend/components/editor/sidebar/new.tsx b/frontend/components/editor/sidebar/new.tsx index 3289e00..d1c6ac5 100644 --- a/frontend/components/editor/sidebar/new.tsx +++ b/frontend/components/editor/sidebar/new.tsx @@ -1,19 +1,33 @@ "use client" +import { validateName } from "@/lib/utils" import Image from "next/image" import { useEffect, useRef } from "react" +import { Socket } from "socket.io-client" export default function New({ + socket, type, stopEditing, + addNew, }: { + socket: Socket type: "file" | "folder" stopEditing: () => void + addNew: (name: string, type: "file" | "folder") => void }) { const inputRef = useRef(null) - const createFile = () => { - console.log("Create File") + const createNew = () => { + const name = inputRef.current?.value + // console.log("Create:", name, type) + + if (name && validateName(name, "", type)) { + if (type === "file") { + socket.emit("createFile", name) + } + addNew(name, type) + } stopEditing() } @@ -37,13 +51,13 @@ export default function New({
{ e.preventDefault() - createFile() + createNew() }} > createFile()} + onBlur={() => createNew()} />
diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index 5e3b034..f7a6a5e 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -1,4 +1,5 @@ import { type ClassValue, clsx } from "clsx" +import { toast } from "sonner" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { @@ -18,3 +19,25 @@ export function processFileType(file: string) { export function decodeTerminalResponse(buffer: Buffer): string { return buffer.toString("utf-8") } + +export function validateName( + newName: string, + oldName: string, + type: "file" | "folder" +) { + if (newName === oldName || newName.length === 0) { + return false + } + + if ( + newName.includes("/") || + newName.includes("\\") || + newName.includes(" ") || + (type === "file" && !newName.includes(".")) || + (type === "folder" && newName.includes(".")) + ) { + toast.error("Invalid file name.") + return false + } + return true +}