Skip to content

Commit f53eed4

Browse files
authored
Add folder delete and rename behavior (#355)
* Add folder delete and rename behavior * Remove unused files state * Fix lint warnings
1 parent d32b748 commit f53eed4

File tree

6 files changed

+303
-83
lines changed

6 files changed

+303
-83
lines changed

packages/api/apps/disk.mts

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RmOptions } from 'node:fs';
12
import fs from 'node:fs/promises';
23
import Path from 'node:path';
34
import { fileURLToPath } from 'node:url';
@@ -152,6 +153,19 @@ export async function createDirectory(
152153
};
153154
}
154155

156+
export function deleteDirectory(app: DBAppType, path: string) {
157+
return deleteEntry(app, path, { recursive: true, force: true });
158+
}
159+
160+
export async function renameDirectory(
161+
app: DBAppType,
162+
path: string,
163+
name: string,
164+
): Promise<DirEntryType> {
165+
const result = await rename(app, path, name);
166+
return { ...result, type: 'directory' as const, children: null };
167+
}
168+
155169
export async function loadFile(app: DBAppType, path: string): Promise<FileType> {
156170
const projectDir = Path.join(APPS_DIR, app.externalId);
157171
const filePath = Path.join(projectDir, path);
@@ -184,29 +198,32 @@ export async function createFile(
184198
}
185199

186200
export function deleteFile(app: DBAppType, path: string) {
187-
const filePath = Path.join(APPS_DIR, app.externalId, path);
188-
return fs.rm(filePath);
201+
return deleteEntry(app, path);
189202
}
190203

191204
export async function renameFile(
192205
app: DBAppType,
193206
path: string,
194207
name: string,
195208
): Promise<FileEntryType> {
209+
const result = await rename(app, path, name);
210+
return { ...result, type: 'file' as const };
211+
}
212+
213+
async function rename(app: DBAppType, path: string, name: string) {
196214
const projectDir = Path.join(APPS_DIR, app.externalId);
197215
const oldPath = Path.join(projectDir, path);
198216
const dirname = Path.dirname(oldPath);
199217
const newPath = Path.join(dirname, name);
200218
await fs.rename(oldPath, newPath);
201-
202219
const relativePath = Path.relative(projectDir, newPath);
203220
const basename = Path.basename(newPath);
221+
return { name: basename, path: relativePath };
222+
}
204223

205-
return {
206-
type: 'file' as const,
207-
name: basename,
208-
path: relativePath,
209-
};
224+
function deleteEntry(app: DBAppType, path: string, options: RmOptions = {}) {
225+
const filePath = Path.join(APPS_DIR, app.externalId, path);
226+
return fs.rm(filePath, options);
210227
}
211228

212229
// TODO: This does not scale.

packages/api/server/http.mts

+47
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {
4242
loadFile,
4343
createFile,
4444
createDirectory,
45+
renameDirectory,
46+
deleteDirectory,
4547
} from '../apps/disk.mjs';
4648
import { CreateAppSchema } from '../apps/schemas.mjs';
4749

@@ -515,6 +517,51 @@ router.post('/apps/:id/directories', cors(), async (req, res) => {
515517
}
516518
});
517519

520+
router.options('/apps/:id/directories', cors());
521+
router.delete('/apps/:id/directories', cors(), async (req, res) => {
522+
const { id } = req.params;
523+
524+
// TODO: validate and ensure path is not absolute
525+
const path = typeof req.query.path === 'string' ? req.query.path : '.';
526+
527+
try {
528+
const app = await loadApp(id);
529+
530+
if (!app) {
531+
return res.status(404).json({ error: 'App not found' });
532+
}
533+
534+
await deleteDirectory(app, path);
535+
536+
return res.json({ data: { deleted: true } });
537+
} catch (e) {
538+
return error500(res, e as Error);
539+
}
540+
});
541+
542+
router.options('/apps/:id/directories/rename', cors());
543+
router.post('/apps/:id/directories/rename', cors(), async (req, res) => {
544+
const { id } = req.params;
545+
546+
// TODO: validate and ensure path is not absolute
547+
const path = typeof req.query.path === 'string' ? req.query.path : '.';
548+
const name = req.query.name as string;
549+
550+
try {
551+
const app = await loadApp(id);
552+
553+
if (!app) {
554+
return res.status(404).json({ error: 'App not found' });
555+
}
556+
557+
const directory = await renameDirectory(app, path, name);
558+
559+
return res.json({ data: directory });
560+
} catch (e) {
561+
return error500(res, e as Error);
562+
}
563+
});
564+
518565
router.options('/apps/:id/files', cors());
519566
router.get('/apps/:id/files', cors(), async (req, res) => {
520567
const { id } = req.params;

packages/web/src/clients/http/apps.ts

+39
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,45 @@ export async function createDirectory(
103103
return response.json();
104104
}
105105

106+
export async function deleteDirectory(
107+
id: string,
108+
path: string,
109+
): Promise<{ data: { deleted: true } }> {
110+
const queryParams = new URLSearchParams({ path });
111+
112+
const response = await fetch(API_BASE_URL + `/apps/${id}/directories?${queryParams}`, {
113+
method: 'DELETE',
114+
headers: { 'content-type': 'application/json' },
115+
});
116+
117+
if (!response.ok) {
118+
console.error(response);
119+
throw new Error('Request failed');
120+
}
121+
122+
return response.json();
123+
}
124+
125+
export async function renameDirectory(
126+
id: string,
127+
path: string,
128+
name: string,
129+
): Promise<{ data: DirEntryType }> {
130+
const queryParams = new URLSearchParams({ path, name });
131+
132+
const response = await fetch(API_BASE_URL + `/apps/${id}/directories/rename?${queryParams}`, {
133+
method: 'POST',
134+
headers: { 'content-type': 'application/json' },
135+
});
136+
137+
if (!response.ok) {
138+
console.error(response);
139+
throw new Error('Request failed');
140+
}
141+
142+
return response.json();
143+
}
144+
106145
export async function loadFile(id: string, path: string): Promise<{ data: FileType }> {
107146
const queryParams = new URLSearchParams({ path });
108147

packages/web/src/components/apps/lib/file-tree.ts

+38
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,44 @@ function merge(oldChildren: FsEntryTreeType | null, newChildren: FsEntryTreeType
101101
});
102102
}
103103

104+
export function renameDirNode(
105+
tree: DirEntryType,
106+
oldNode: DirEntryType,
107+
newNode: DirEntryType,
108+
): DirEntryType {
109+
return sortTree(doRenameDirNode(tree, oldNode, newNode));
110+
}
111+
112+
function doRenameDirNode(
113+
tree: DirEntryType,
114+
oldNode: DirEntryType,
115+
newNode: DirEntryType,
116+
): DirEntryType {
117+
const children =
118+
tree.children === null
119+
? null
120+
: tree.children.map((entry) => {
121+
if (entry.type === 'directory') {
122+
return doRenameDirNode(entry, oldNode, newNode);
123+
} else {
124+
if (entry.path.startsWith(oldNode.path)) {
125+
return { ...entry, path: entry.path.replace(oldNode.path, newNode.path) };
126+
} else {
127+
return entry;
128+
}
129+
}
130+
});
131+
132+
if (tree.path === oldNode.path) {
133+
return { ...newNode, children };
134+
} else if (tree.path.startsWith(oldNode.path)) {
135+
const path = tree.path.replace(oldNode.path, newNode.path);
136+
return { ...tree, path, children };
137+
} else {
138+
return { ...tree, children };
139+
}
140+
}
141+
104142
export function updateFileNode(
105143
tree: DirEntryType,
106144
oldNode: FileEntryType,

0 commit comments

Comments
 (0)