* Get file statistics via exec. * Uses stat -L to follow symlinks (report target's type, not "symbolic link").
(filePath: string, abortSignal?: AbortSignal)
| 499 | * Uses stat -L to follow symlinks (report target's type, not "symbolic link"). |
| 500 | */ |
| 501 | async stat(filePath: string, abortSignal?: AbortSignal): Promise<FileStat> { |
| 502 | const stream = await this.exec(`stat -L -c '%s %Y %F' ${this.quoteForRemote(filePath)}`, { |
| 503 | cwd: this.getBasePath(), |
| 504 | timeout: 10, |
| 505 | abortSignal, |
| 506 | }); |
| 507 | |
| 508 | const [stdout, stderr, exitCode] = await Promise.all([ |
| 509 | streamToString(stream.stdout), |
| 510 | streamToString(stream.stderr), |
| 511 | stream.exitCode, |
| 512 | ]); |
| 513 | |
| 514 | if (exitCode !== 0) { |
| 515 | throw new RuntimeError(`Failed to stat ${filePath}: ${stderr}`, "file_io"); |
| 516 | } |
| 517 | |
| 518 | const parts = stdout.trim().split(" "); |
| 519 | if (parts.length < 3) { |
| 520 | throw new RuntimeError(`Failed to parse stat output for ${filePath}: ${stdout}`, "file_io"); |
| 521 | } |
| 522 | |
| 523 | const size = parseInt(parts[0], 10); |
| 524 | const mtime = parseInt(parts[1], 10); |
| 525 | const fileType = parts.slice(2).join(" "); |
| 526 | |
| 527 | return { |
| 528 | size, |
| 529 | modifiedTime: new Date(mtime * 1000), |
| 530 | isDirectory: fileType === "directory", |
| 531 | }; |
| 532 | } |
| 533 | |
| 534 | /** |
| 535 | * Normalize path for comparison (POSIX semantics). |