@@ -34,10 +34,12 @@ import {
3434import {
3535 arePathsEqual ,
3636 isEmptyDir ,
37+ isPathWithin ,
3738 isWordPressDirectory ,
3839 recursiveCopyDirectory ,
3940} from '@studio/common/lib/fs-utils' ;
4041import { generateNumberedName , generateSiteName } from '@studio/common/lib/generate-site-name' ;
42+ import { importIpcEventSchema } from '@studio/common/lib/import-export-events' ;
4143import { isErrnoException } from '@studio/common/lib/is-errno-exception' ;
4244import { getAuthenticationUrl } from '@studio/common/lib/oauth' ;
4345import { decodePassword } from '@studio/common/lib/passwords' ;
@@ -185,9 +187,7 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
185187 const cliRunner = createCliRunner ( { cliBinary, nodeBinary } ) ;
186188 const execute = cliRunner . executeCliCommand ;
187189
188- // --- Server-Sent Events: one stream carries every run's output ------------
189- // The web connector expects the same envelope on both channels: agent run
190- // events on `agent`, session-placement updates on `placement`.
190+ // --- Server-Sent Events: one stream carries live UI updates ----------------
191191 const sseClients = new Set < Response > ( ) ;
192192 function sseSend ( message : { channel : string ; payload : unknown } ) : void {
193193 if ( sseClients . size === 0 ) {
@@ -526,7 +526,15 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
526526
527527 api . post ( '/paths/compare' , ( req : Request , res : Response ) => {
528528 const { path1, path2 } = req . body as { path1 ?: string ; path2 ?: string } ;
529- res . json ( { equal : ! ! path1 && ! ! path2 && arePathsEqual ( path1 , path2 ) } ) ;
529+ // Confine both operands to `sitesRoot`: nothing outside it can be a site, and
530+ // this keeps untrusted input from reaching statSync (in arePathsEqual).
531+ const equal =
532+ ! ! path1 &&
533+ ! ! path2 &&
534+ isPathWithin ( sitesRoot , path1 ) &&
535+ isPathWithin ( sitesRoot , path2 ) &&
536+ arePathsEqual ( path1 , path2 ) ;
537+ res . json ( { equal } ) ;
530538 } ) ;
531539
532540 api . post (
@@ -837,9 +845,8 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
837845 } )
838846 ) ;
839847
840- // Import a backup archive into an already-created site — the same CLI `import`
841- // the desktop forks. The archive path is normally an upload from `/uploads`,
842- // which is cleaned up afterwards.
848+ // Import a backup into an already-created site. Uploaded files are cleaned up
849+ // after each attempt, so a retry must upload the selected File again.
843850 api . post (
844851 '/sites/:id/import' ,
845852 asyncHandler ( async ( req : Request , res : Response ) => {
@@ -855,8 +862,18 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
855862 }
856863 try {
857864 await new Promise < void > ( ( resolve , reject ) => {
858- const [ emitter ] = execute ( [ 'import' , '--path' , site . path , archivePath ] , {
859- output : 'capture' ,
865+ const [ emitter ] = execute (
866+ [ 'import' , '--path' , site . path , archivePath , '--start-server' ] ,
867+ { output : 'capture' }
868+ ) ;
869+ emitter . on ( 'data' , ( { data } ) => {
870+ const parsed = importIpcEventSchema . safeParse ( data ) ;
871+ if ( parsed . success ) {
872+ sseSend ( {
873+ channel : 'import' ,
874+ payload : { siteId : site . id , event : parsed . data . event } ,
875+ } ) ;
876+ }
860877 } ) ;
861878 emitter . on ( 'success' , ( ) => resolve ( ) ) ;
862879 emitter . on ( 'failure' , ( { error } ) => reject ( error ) ) ;
@@ -867,12 +884,14 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
867884 // direct child of the OS temp dir. Resolve first so `..` in the
868885 // supplied path can't escape the temp root and delete elsewhere.
869886 const uploadDir = path . dirname ( path . resolve ( archivePath ) ) ;
870- if ( path . dirname ( uploadDir ) === path . resolve ( os . tmpdir ( ) ) ) {
887+ if (
888+ path . dirname ( uploadDir ) === path . resolve ( os . tmpdir ( ) ) &&
889+ path . basename ( uploadDir ) . startsWith ( 'studio-upload-' )
890+ ) {
871891 rm ( uploadDir , { recursive : true , force : true } , ( ) => undefined ) ;
872892 }
873893 }
874- const imported = ( await listSites ( execute ) ) . find ( ( s ) => s . id === req . params . id ) ;
875- res . json ( toSiteDetails ( imported ?? site ) ) ;
894+ res . status ( 204 ) . end ( ) ;
876895 } )
877896 ) ;
878897
0 commit comments