mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 15:22:59 +00:00
fix(stack-files): confirm before overwriting an existing upload target (#1204)
* fix(stack-files): confirm before overwriting an existing upload target Same-name uploads previously truncated the existing file silently. A user dragging a file with a name that matched an in-place file destroyed the original with no warning and no undo. The upload route now reads ?overwrite=0|1. When the flag is not set and the target already exists, the server returns 409 FILE_EXISTS and the original file is untouched. The frontend opens a confirm dialog and retries with overwrite=1 on the user's approval; cancel keeps the original. A new pathExists helper on FileSystemService performs the existence check through the same path-resolution barrier as the write so a malicious relPath cannot bypass the conflict check. UploadConflictError is exported so callers can distinguish the conflict case from generic upload failures without parsing error strings. * fix(stack-files): distinct DIR_EXISTS code, drop INVALID_PATH swallow in existence check
This commit is contained in:
@@ -711,6 +711,25 @@ export class FileSystemService {
|
||||
await fsPromises.writeFile(safePath, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'file' or 'directory' if the resolved path exists, null if it
|
||||
* does not. Path-resolution errors (INVALID_PATH, SYMLINK_ESCAPE) propagate
|
||||
* so callers do not silently treat a malformed path as 'available for write'.
|
||||
* Callers should validate inputs upstream before invoking this helper.
|
||||
*/
|
||||
async pathKind(stackName: string, relPath: string): Promise<'file' | 'directory' | null> {
|
||||
const safePath = await this.resolveSafeStackPath(stackName, relPath);
|
||||
try {
|
||||
const stat = await fsPromises.lstat(safePath);
|
||||
if (stat.isDirectory()) return 'directory';
|
||||
return 'file';
|
||||
} catch (err: unknown) {
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
if (e.code === 'ENOENT') return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteStackPath(stackName: string, relPath: string, recursive: boolean = false): Promise<void> {
|
||||
if (isProtectedRelPath(relPath)) throw protectedFileError(relPath);
|
||||
const safePath = await this.resolveSafeStackPath(stackName, relPath);
|
||||
|
||||
Reference in New Issue
Block a user