adding file logic

This commit is contained in:
Ishaan Dey 2024-04-29 00:50:25 -04:00
parent 3b9aa900c8
commit bce9d11b3b
12 changed files with 208 additions and 44 deletions

4
.gitignore vendored
View File

@ -36,4 +36,6 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
wrangler.toml wrangler.toml
backend/server/projects

View File

@ -12,6 +12,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); 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 express_1 = __importDefault(require("express"));
const dotenv_1 = __importDefault(require("dotenv")); const dotenv_1 = __importDefault(require("dotenv"));
const http_1 = require("http"); const http_1 = require("http");
@ -30,6 +32,7 @@ const io = new socket_io_1.Server(httpServer, {
}, },
}); });
const terminals = {}; const terminals = {};
const dirName = path_1.default.join(__dirname, "..");
const handshakeSchema = zod_1.z.object({ const handshakeSchema = zod_1.z.object({
userId: zod_1.z.string(), userId: zod_1.z.string(),
sandboxId: 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* () { io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
const data = socket.data; const data = socket.data;
const sandboxFiles = yield (0, utils_1.getSandboxFiles)(data.id); 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.emit("loaded", sandboxFiles.files);
socket.on("getFile", (fileId, callback) => { socket.on("getFile", (fileId, callback) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId); const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) if (!file)
return; 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); callback(file.data);
}); });
// todo: send diffs + debounce for efficiency // todo: send diffs + debounce for efficiency
@ -81,19 +93,47 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
if (!file) if (!file)
return; return;
file.data = body; 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); 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* () { socket.on("renameFile", (fileId, newName) => __awaiter(void 0, void 0, void 0, function* () {
const file = sandboxFiles.fileData.find((f) => f.id === fileId); const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) if (!file)
return; return;
file.id = newName; 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 }) => { socket.on("createTerminal", ({ id }) => {
console.log("creating terminal (" + 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 }) => { socket.on("terminalData", ({ id, data }) => {
console.log(`Received data for terminal ${id}: ${data}`); console.log(`Received data for terminal ${id}: ${data}`);

View File

@ -7,13 +7,14 @@ exports.Pty = void 0;
const node_pty_1 = require("node-pty"); const node_pty_1 = require("node-pty");
const os_1 = __importDefault(require("os")); const os_1 = __importDefault(require("os"));
class Pty { class Pty {
constructor(socket, id) { constructor(socket, id, cwd) {
this.socket = socket; this.socket = socket;
this.shell = os_1.default.platform() === "win32" ? "cmd.exe" : "bash"; this.shell = os_1.default.platform() === "win32" ? "cmd.exe" : "bash";
this.id = id;
this.ptyProcess = (0, node_pty_1.spawn)(this.shell, [], { this.ptyProcess = (0, node_pty_1.spawn)(this.shell, [], {
name: "xterm", name: "xterm",
cols: 100, cols: 100,
cwd: `/temp`, cwd: cwd,
// env: process.env as { [key: string]: string }, // env: process.env as { [key: string]: string },
}); });
this.ptyProcess.onData((data) => { this.ptyProcess.onData((data) => {

View File

@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}); });
}; };
Object.defineProperty(exports, "__esModule", { value: true }); 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 getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () {
const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`); const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`);
const sandboxData = yield sandboxRes.json(); const sandboxData = yield sandboxRes.json();
@ -77,9 +77,18 @@ const fetchFileContent = (fileId) => __awaiter(void 0, void 0, void 0, function*
return ""; return "";
} }
}); });
const renameFile = (fileId, newName, data) => __awaiter(void 0, void 0, void 0, function* () { const createFile = (fileId) => __awaiter(void 0, void 0, void 0, function* () {
const parts = fileId.split("/"); const res = yield fetch(`https://storage.ishaan1013.workers.dev/api`, {
const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName; 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`, { const res = yield fetch(`https://storage.ishaan1013.workers.dev/api/rename`, {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -1,3 +1,5 @@
import fs from "fs"
import path from "path"
import express, { Express, NextFunction, Request, Response } from "express" import express, { Express, NextFunction, Request, Response } from "express"
import dotenv from "dotenv" import dotenv from "dotenv"
import { createServer } from "http" import { createServer } from "http"
@ -5,7 +7,7 @@ import { Server } from "socket.io"
import { z } from "zod" import { z } from "zod"
import { User } from "./types" import { User } from "./types"
import { getSandboxFiles, renameFile, saveFile } from "./utils" import { createFile, getSandboxFiles, renameFile, saveFile } from "./utils"
import { Pty } from "./terminal" import { Pty } from "./terminal"
dotenv.config() dotenv.config()
@ -22,6 +24,8 @@ const io = new Server(httpServer, {
const terminals: { [id: string]: Pty } = {} const terminals: { [id: string]: Pty } = {}
const dirName = path.join(__dirname, "..")
const handshakeSchema = z.object({ const handshakeSchema = z.object({
userId: z.string(), userId: z.string(),
sandboxId: z.string(), sandboxId: z.string(),
@ -72,6 +76,14 @@ io.on("connection", async (socket) => {
} }
const sandboxFiles = await getSandboxFiles(data.id) 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) socket.emit("loaded", sandboxFiles.files)
@ -79,7 +91,7 @@ io.on("connection", async (socket) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId) const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return 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) callback(file.data)
}) })
@ -88,22 +100,57 @@ io.on("connection", async (socket) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId) const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return if (!file) return
file.data = body 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) 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) => { socket.on("renameFile", async (fileId: string, newName: string) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId) const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return if (!file) return
file.id = newName 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 }) => { socket.on("createTerminal", ({ id }: { id: string }) => {
console.log("creating terminal (" + id + ")") 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 }) => { socket.on("terminalData", ({ id, data }: { id: string; data: string }) => {

View File

@ -6,15 +6,17 @@ export class Pty {
socket: Socket socket: Socket
ptyProcess: IPty ptyProcess: IPty
shell: string shell: string
id: string
constructor(socket: Socket, id: string) { constructor(socket: Socket, id: string, cwd: string) {
this.socket = socket this.socket = socket
this.shell = os.platform() === "win32" ? "cmd.exe" : "bash" this.shell = os.platform() === "win32" ? "cmd.exe" : "bash"
this.id = id
this.ptyProcess = spawn(this.shell, [], { this.ptyProcess = spawn(this.shell, [], {
name: "xterm", name: "xterm",
cols: 100, cols: 100,
cwd: `/temp`, cwd: cwd,
// env: process.env as { [key: string]: string }, // env: process.env as { [key: string]: string },
}) })

View File

@ -87,14 +87,22 @@ const fetchFileContent = async (fileId: string): Promise<string> => {
} }
} }
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 ( export const renameFile = async (
fileId: string, fileId: string,
newName: string, newFileId: string,
data: 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`, { const res = await fetch(`https://storage.ishaan1013.workers.dev/api/rename`, {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -41,7 +41,16 @@ export default {
}); });
} else return invalidRequest; } else return invalidRequest;
} else if (method === 'POST') { } 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 return methodNotAllowed;
} else if (path === '/api/rename' && method === 'POST') { } else if (path === '/api/rename' && method === 'POST') {
const renameSchema = z.object({ const renameSchema = z.object({

View File

@ -23,7 +23,7 @@ import { useClerk } from "@clerk/nextjs"
import { TFile, TFileData, TFolder, TTab } from "./sidebar/types" import { TFile, TFileData, TFolder, TTab } from "./sidebar/types"
import { io } from "socket.io-client" import { io } from "socket.io-client"
import { processFileType } from "@/lib/utils" import { processFileType, validateName } from "@/lib/utils"
import { toast } from "sonner" import { toast } from "sonner"
import EditorTerminal from "./terminal" import EditorTerminal from "./terminal"
@ -158,21 +158,7 @@ export default function CodeEditor({
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => { ) => {
// Validation if (!validateName(newName, oldName, type)) return false
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
}
// Action // Action
socket.emit("renameFile", id, newName) socket.emit("renameFile", id, newName)
@ -189,6 +175,19 @@ export default function CodeEditor({
files={files} files={files}
selectFile={selectFile} selectFile={selectFile}
handleRename={handleRename} 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: [] }])
}
}}
/> />
<ResizablePanelGroup direction="horizontal"> <ResizablePanelGroup direction="horizontal">
<ResizablePanel <ResizablePanel

View File

@ -6,11 +6,14 @@ import SidebarFolder from "./folder"
import { TFile, TFolder, TTab } from "./types" import { TFile, TFolder, TTab } from "./types"
import { useState } from "react" import { useState } from "react"
import New from "./new" import New from "./new"
import { Socket } from "socket.io-client"
export default function Sidebar({ export default function Sidebar({
files, files,
selectFile, selectFile,
handleRename, handleRename,
socket,
addNew,
}: { }: {
files: (TFile | TFolder)[] files: (TFile | TFolder)[]
selectFile: (tab: TTab) => void selectFile: (tab: TTab) => void
@ -20,6 +23,8 @@ export default function Sidebar({
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean ) => boolean
socket: Socket
addNew: (name: string, type: "file" | "folder") => void
}) { }) {
const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null) const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null)
@ -73,8 +78,13 @@ export default function Sidebar({
)} )}
{creatingNew !== null ? ( {creatingNew !== null ? (
<New <New
socket={socket}
type={creatingNew} type={creatingNew}
stopEditing={() => setCreatingNew(null)} stopEditing={() => {
console.log("stopped editing")
setCreatingNew(null)
}}
addNew={addNew}
/> />
) : null} ) : null}
</> </>

View File

@ -1,19 +1,33 @@
"use client" "use client"
import { validateName } from "@/lib/utils"
import Image from "next/image" import Image from "next/image"
import { useEffect, useRef } from "react" import { useEffect, useRef } from "react"
import { Socket } from "socket.io-client"
export default function New({ export default function New({
socket,
type, type,
stopEditing, stopEditing,
addNew,
}: { }: {
socket: Socket
type: "file" | "folder" type: "file" | "folder"
stopEditing: () => void stopEditing: () => void
addNew: (name: string, type: "file" | "folder") => void
}) { }) {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const createFile = () => { const createNew = () => {
console.log("Create File") 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() stopEditing()
} }
@ -37,13 +51,13 @@ export default function New({
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
createFile() createNew()
}} }}
> >
<input <input
ref={inputRef} ref={inputRef}
className={`bg-transparent transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-sm w-full`} className={`bg-transparent transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-sm w-full`}
onBlur={() => createFile()} onBlur={() => createNew()}
/> />
</form> </form>
</div> </div>

View File

@ -1,4 +1,5 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx"
import { toast } from "sonner"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
@ -18,3 +19,25 @@ export function processFileType(file: string) {
export function decodeTerminalResponse(buffer: Buffer): string { export function decodeTerminalResponse(buffer: Buffer): string {
return buffer.toString("utf-8") 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
}