Files
XCDesktop/api/modules/recycle-bin/routes.ts
2026-03-08 01:34:54 +08:00

176 lines
4.7 KiB
TypeScript

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<string, typeof items>()
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