118 lines
4.8 KiB
TypeScript
118 lines
4.8 KiB
TypeScript
|
|
import { Router } from 'express';
|
||
|
|
import { requireAuth } from '../../auth-connector/source/server/auth';
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
const router = Router();
|
||
|
|
router.use( requireAuth );
|
||
|
|
|
||
|
|
interface FileNode
|
||
|
|
{
|
||
|
|
name: string;
|
||
|
|
path: string;
|
||
|
|
type: 'file' | 'directory';
|
||
|
|
children?: FileNode[];
|
||
|
|
}
|
||
|
|
|
||
|
|
function safeResolve( root: string, filePath: string ): string | null
|
||
|
|
{
|
||
|
|
const resolvedRoot = path.resolve( root );
|
||
|
|
const full = path.resolve( path.join( resolvedRoot, filePath ) );
|
||
|
|
if ( !full.startsWith( resolvedRoot + path.sep ) && full !== resolvedRoot ) return null;
|
||
|
|
return full;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildTree( absDir: string, rootDir: string ): FileNode[]
|
||
|
|
{
|
||
|
|
return fs.readdirSync( absDir ).map( name =>
|
||
|
|
{
|
||
|
|
const abs = path.join( absDir, name );
|
||
|
|
const rel = path.relative( rootDir, abs ).replace( /\\/g, '/' );
|
||
|
|
if ( fs.statSync( abs ).isDirectory() )
|
||
|
|
{
|
||
|
|
return { name, path: rel, type: 'directory' as const, children: buildTree( abs, rootDir ) };
|
||
|
|
}
|
||
|
|
return { name, path: rel, type: 'file' as const };
|
||
|
|
} );
|
||
|
|
}
|
||
|
|
|
||
|
|
router.get( '/tree', ( req, res ) =>
|
||
|
|
{
|
||
|
|
const root = req.query.root as string;
|
||
|
|
if ( !root ) { res.status( 400 ).json( { error: 'root required' } ); return; }
|
||
|
|
const resolvedRoot = path.resolve( root );
|
||
|
|
if ( !fs.existsSync( resolvedRoot ) ) { res.status( 404 ).json( { error: 'Directory not found' } ); return; }
|
||
|
|
res.json( buildTree( resolvedRoot, resolvedRoot ) );
|
||
|
|
} );
|
||
|
|
|
||
|
|
router.get( '/read', ( req, res ) =>
|
||
|
|
{
|
||
|
|
const root = req.query.root as string;
|
||
|
|
const filePath = req.query.path as string;
|
||
|
|
if ( !root || !filePath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; }
|
||
|
|
const full = safeResolve( root, filePath );
|
||
|
|
if ( !full || !fs.existsSync( full ) ) { res.status( 404 ).json( { error: 'Not found' } ); return; }
|
||
|
|
res.type( 'text/plain' ).send( fs.readFileSync( full, 'utf8' ) );
|
||
|
|
} );
|
||
|
|
|
||
|
|
router.put( '/write', ( req, res ) =>
|
||
|
|
{
|
||
|
|
const root = req.query.root as string;
|
||
|
|
const filePath = req.query.path as string;
|
||
|
|
if ( !root || !filePath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; }
|
||
|
|
if ( typeof req.body !== 'string' ) { res.status( 400 ).json( { error: 'Content must be text' } ); return; }
|
||
|
|
const full = safeResolve( root, filePath );
|
||
|
|
if ( !full ) { res.status( 403 ).json( { error: 'Invalid path' } ); return; }
|
||
|
|
fs.mkdirSync( path.dirname( full ), { recursive: true } );
|
||
|
|
fs.writeFileSync( full, req.body, 'utf8' );
|
||
|
|
res.json( { ok: true } );
|
||
|
|
} );
|
||
|
|
|
||
|
|
router.post( '/create-file', ( req, res ) =>
|
||
|
|
{
|
||
|
|
const { root, path: filePath } = req.body as { root: string; path: string };
|
||
|
|
if ( !root || !filePath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; }
|
||
|
|
const full = safeResolve( root, filePath );
|
||
|
|
if ( !full || fs.existsSync( full ) ) { res.status( 409 ).json( { error: 'Already exists or invalid path' } ); return; }
|
||
|
|
fs.mkdirSync( path.dirname( full ), { recursive: true } );
|
||
|
|
fs.writeFileSync( full, '', 'utf8' );
|
||
|
|
res.json( { ok: true } );
|
||
|
|
} );
|
||
|
|
|
||
|
|
router.post( '/create-directory', ( req, res ) =>
|
||
|
|
{
|
||
|
|
const { root, path: dirPath } = req.body as { root: string; path: string };
|
||
|
|
if ( !root || !dirPath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; }
|
||
|
|
const full = safeResolve( root, dirPath );
|
||
|
|
if ( !full || fs.existsSync( full ) ) { res.status( 409 ).json( { error: 'Already exists or invalid path' } ); return; }
|
||
|
|
fs.mkdirSync( full, { recursive: true } );
|
||
|
|
res.json( { ok: true } );
|
||
|
|
} );
|
||
|
|
|
||
|
|
router.post( '/rename', ( req, res ) =>
|
||
|
|
{
|
||
|
|
const { root, path: oldPath, newName } = req.body as { root: string; path: string; newName: string };
|
||
|
|
if ( !root || !oldPath || !newName ) { res.status( 400 ).json( { error: 'root, path, and newName required' } ); return; }
|
||
|
|
if ( newName.includes( '/' ) || newName.includes( '\\' ) ) { res.status( 400 ).json( { error: 'Invalid name' } ); return; }
|
||
|
|
const full = safeResolve( root, oldPath );
|
||
|
|
if ( !full || !fs.existsSync( full ) ) { res.status( 404 ).json( { error: 'Not found' } ); return; }
|
||
|
|
const resolvedRoot = path.resolve( root );
|
||
|
|
const newFull = path.join( path.dirname( full ), newName );
|
||
|
|
if ( !newFull.startsWith( resolvedRoot + path.sep ) ) { res.status( 403 ).json( { error: 'Invalid path' } ); return; }
|
||
|
|
if ( fs.existsSync( newFull ) ) { res.status( 409 ).json( { error: 'Already exists' } ); return; }
|
||
|
|
fs.renameSync( full, newFull );
|
||
|
|
res.json( { ok: true } );
|
||
|
|
} );
|
||
|
|
|
||
|
|
router.post( '/delete', ( req, res ) =>
|
||
|
|
{
|
||
|
|
const { root, path: targetPath } = req.body as { root: string; path: string };
|
||
|
|
if ( !root || !targetPath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; }
|
||
|
|
const full = safeResolve( root, targetPath );
|
||
|
|
if ( !full || !fs.existsSync( full ) ) { res.status( 404 ).json( { error: 'Not found' } ); return; }
|
||
|
|
fs.rmSync( full, { recursive: true, force: true } );
|
||
|
|
res.json( { ok: true } );
|
||
|
|
} );
|
||
|
|
|
||
|
|
export default router;
|