import express, { type Request, type Response } from 'express' import fs from 'fs/promises' import path from 'path' import { asyncHandler } from '../../utils/asyncHandler.js' import { successResponse } from '../../utils/response.js' import { resolveNotebookPath } from '../../utils/pathSafety.js' import { restoreFile, restoreFolder } from './recycleBinService.js' import { NotFoundError, BadRequestError, ValidationError, AlreadyExistsError, } from '../../../shared/errors/index.js' const router = express.Router() router.get( '/', asyncHandler(async (req: Request, res: Response) => { const { fullPath: rbDir } = resolveNotebookPath('RB') try { await fs.access(rbDir) } catch { successResponse(res, { groups: [] }) return } const entries = await fs.readdir(rbDir, { withFileTypes: true }) const items: { name: string; originalName: string; type: 'file' | 'dir'; deletedDate: string; path: string }[] = [] for (const entry of entries) { const match = entry.name.match(/^(\d{8})_(.+)$/) if (!match) continue const [, dateStr, originalName] = match if (entry.isDirectory()) { items.push({ name: entry.name, originalName, type: 'dir', deletedDate: dateStr, path: `RB/${entry.name}`, }) } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { items.push({ name: entry.name, originalName, type: 'file', deletedDate: dateStr, path: `RB/${entry.name}`, }) } } const groupedMap = new Map() for (const item of items) { const existing = groupedMap.get(item.deletedDate) || [] existing.push(item) groupedMap.set(item.deletedDate, existing) } const groups = Array.from(groupedMap.entries()) .map(([date, items]) => ({ date, items: items.sort((a, b) => a.originalName.localeCompare(b.originalName)), })) .sort((a, b) => b.date.localeCompare(a.date)) successResponse(res, { groups }) }), ) router.post( '/restore', asyncHandler(async (req: Request, res: Response) => { const { path: relPath, type } = req.body as { path?: string; type?: 'file' | 'dir' } if (!relPath || !type) { throw new ValidationError('Path and type are required') } const { fullPath: itemPath } = resolveNotebookPath(relPath) try { await fs.access(itemPath) } catch { throw new NotFoundError('Item not found in recycle bin') } const match = path.basename(itemPath).match(/^(\d{8})_(.+)$/) if (!match) { throw new BadRequestError('Invalid recycle bin item name') } const [, dateStr, originalName] = match const year = dateStr.substring(0, 4) const month = dateStr.substring(4, 6) const day = dateStr.substring(6, 8) const { fullPath: markdownsDir } = resolveNotebookPath('markdowns') await fs.mkdir(markdownsDir, { recursive: true }) const destPath = path.join(markdownsDir, originalName) const existing = await fs.stat(destPath).catch(() => null) if (existing) { throw new AlreadyExistsError('A file or folder with this name already exists') } if (type === 'dir') { await restoreFolder(itemPath, destPath, dateStr, year, month, day) } else { await restoreFile(itemPath, destPath, dateStr, year, month, day) } successResponse(res, null) }), ) router.delete( '/permanent', asyncHandler(async (req: Request, res: Response) => { const { path: relPath, type } = req.body as { path?: string; type?: 'file' | 'dir' } if (!relPath || !type) { throw new ValidationError('Path and type are required') } const { fullPath: itemPath } = resolveNotebookPath(relPath) try { await fs.access(itemPath) } catch { throw new NotFoundError('Item not found in recycle bin') } if (type === 'dir') { await fs.rm(itemPath, { recursive: true, force: true }) } else { await fs.unlink(itemPath) } successResponse(res, null) }), ) router.delete( '/empty', asyncHandler(async (req: Request, res: Response) => { const { fullPath: rbDir } = resolveNotebookPath('RB') try { await fs.access(rbDir) } catch { successResponse(res, null) return } const entries = await fs.readdir(rbDir, { withFileTypes: true }) for (const entry of entries) { const entryPath = path.join(rbDir, entry.name) if (entry.isDirectory()) { await fs.rm(entryPath, { recursive: true, force: true }) } else { await fs.unlink(entryPath) } } successResponse(res, null) }), ) export default router