diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c8ffc3..6f0a2e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Latest ## Fixes * [#367](https://github.com/cleverage/process-bundle/issues/367) `ProcessLauncherTask` (`process_launcher`) with `json_buffering`: when the sub-process writes nothing into its JSON buffer (no `end_point`, or an output that is not an array), the task outputs nothing again, as before 5.5, instead of the path of an empty file, and the empty buffer is removed. The buffer of a failed sub-process, and those of the still running sub-processes stopped because of it, are removed too (one file was left in the temporary directory for each launch). Update documentation, add tests. +* [#368](https://github.com/cleverage/process-bundle/issues/368) `YamlWriterTask` (`yaml_writer`): the parent directory is created for a local path only (no scheme, or `file://`), not for a stream wrapper (`compress.zlib://`, `s3://`...), which failed, or `php://stdout`, which created a `php:` directory. Update documentation, add tests. v5.5 ----- diff --git a/docs/reference/tasks/yaml_writer_task.md b/docs/reference/tasks/yaml_writer_task.md index c911428c..7a604a98 100644 --- a/docs/reference/tasks/yaml_writer_task.md +++ b/docs/reference/tasks/yaml_writer_task.md @@ -24,7 +24,7 @@ Options | Code | Type | Required | Default | Description | |-------------|----------|:--------:|---------|------------------------------------------------------------------------| -| `file_path` | `string` | **X** | | Path of the file to write to. Its parent directory is created if needed | +| `file_path` | `string` | **X** | | Path of the file to write to. Its parent directory is created if needed (local path only) | | `inline` | `int` | | `4` | Level at which the dumper switches to inline YAML (see `Yaml::dump()`) | Examples @@ -44,4 +44,6 @@ Notes * This task is not blocking: when receiving several inputs, each one overwrites the file. Aggregate the data first (e.g. with [AggregateIterableTask](aggregate_iterable_task.md)) if needed. +* The parent directory is created only for a local path (no scheme, or `file://`): a stream wrapper path + (`compress.zlib://`, `s3://`, `php://stdout`...) is written as is, its directory is never created. * When the file cannot be written (e.g. not writable directory), the task fails with a `RuntimeException`. diff --git a/src/Task/File/Yaml/YamlWriterTask.php b/src/Task/File/Yaml/YamlWriterTask.php index 92d1751d..ba87cdb4 100644 --- a/src/Task/File/Yaml/YamlWriterTask.php +++ b/src/Task/File/Yaml/YamlWriterTask.php @@ -30,10 +30,13 @@ public function execute(ProcessState $state): void { $options = $this->getOptions($state); $filePath = $options['file_path']; - // Same as the other writers: the parent directory is created if needed - $dirname = \dirname($filePath); - if (!@mkdir($dirname, 0o755, true) && !is_dir($dirname)) { - throw new RuntimeException(\sprintf('Directory "%s" was not created', $dirname)); + // Same as the other writers: the parent directory is created if needed, for a local path only (not for a stream + // wrapper such as compress.zlib://, s3:// or php://stdout) + if (!preg_match('#^[a-z][a-z0-9+.-]*://#i', $filePath) || str_starts_with(strtolower($filePath), 'file://')) { + $dirname = \dirname($filePath); + if (!@mkdir($dirname, 0o755, true) && !is_dir($dirname)) { + throw new RuntimeException(\sprintf('Directory "%s" was not created', $dirname)); + } } // The PHP warning is replaced by the exception below, also outside of the debug mode if (false === @file_put_contents($filePath, Yaml::dump($state->getInput(), $options['inline']))) { diff --git a/tests/Task/File/Yaml/YamlWriterTaskTest.php b/tests/Task/File/Yaml/YamlWriterTaskTest.php index c0599882..5d46d433 100644 --- a/tests/Task/File/Yaml/YamlWriterTaskTest.php +++ b/tests/Task/File/Yaml/YamlWriterTaskTest.php @@ -109,6 +109,46 @@ public function testTheParentDirectoryIsCreated(): void self::assertSame(self::DATA, Yaml::parseFile($filePath)); } + public function testTheParentDirectoryOfAFileUrlIsCreated(): void + { + $filePath = $this->tmpDir.'/url/file.yaml'; + $this->execute(['file_path' => 'file://'.$filePath], self::DATA); + + self::assertSame(self::DATA, Yaml::parseFile($filePath)); + } + + public function testAStreamWrapperPathIsWritten(): void + { + $filePath = 'compress.zlib://'.$this->tmpDir.'/file.yaml.gz'; + $state = $this->execute(['file_path' => $filePath], self::DATA); + + self::assertSame($filePath, $state->getOutput()); + self::assertSame(self::DATA, Yaml::parse((string) file_get_contents($filePath))); + self::assertSame("\x1f\x8b", substr((string) file_get_contents($this->tmpDir.'/file.yaml.gz'), 0, 2)); + } + + public function testAStreamWrapperPathDoesNotCreateADirectory(): void + { + $cwd = getcwd(); + self::assertNotFalse($cwd); + chdir($this->tmpDir); + try { + $this->execute(['file_path' => 'php://memory'], self::DATA); + ob_start(); + try { + $this->execute(['file_path' => 'php://output'], self::DATA); + } finally { + $output = ob_get_clean(); + } + } finally { + chdir($cwd); + } + + self::assertSame(self::DATA, Yaml::parse((string) $output)); + + self::assertSame([], glob($this->tmpDir.'/*')); + } + public function testAWriteFailureThrows(): void { // A directory cannot be written as a file