Skip to content

Commit e00fde8

Browse files
committed
feature: format filename
1 parent b0f752d commit e00fde8

5 files changed

Lines changed: 87 additions & 22 deletions

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"sonata-project/exporter": "^2.10",
2020
"illuminate/container": ">=6",
2121
"illuminate/support": ">=6",
22-
"symfony/http-foundation": ">=3"
22+
"symfony/http-foundation": ">=3",
23+
"symfony/filesystem": ">=3"
2324
},
2425
"require-dev": {
2526
"friendsofphp/php-cs-fixer": "^3.0",

src/DataExporter/Handler.php

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use InvalidArgumentException;
88
use Kriss\DataExporter\Exceptions\FileAlreadyExistException;
99
use Sonata\Exporter\Writer\WriterInterface;
10+
use Symfony\Component\Filesystem\Path;
1011
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
1112
use Symfony\Component\HttpFoundation\StreamedResponse;
1213

@@ -84,11 +85,11 @@ public function browserDownload(string $downloadName): StreamedResponse
8485
});
8586

8687
if ($downloadName) {
87-
$name = basename($this->makeFilename($downloadName));
88+
$name = $this->makeFilename($downloadName, false);
8889
$response->headers->set('Content-Disposition', $response->headers->makeDisposition(
8990
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
9091
$name,
91-
Str::ascii($name)
92+
str_replace('%', $this->getConfig()['filenameInvalidCharsReplace'], Str::ascii($name))
9293
));
9394
}
9495

@@ -115,27 +116,55 @@ public function clean(): void
115116
/**
116117
* 生成的文件名
117118
* @param string $filename
119+
* @param bool $maybeHasPath filename 中是否可能包含路径
118120
* @return string
119121
*/
120-
public function makeFilename(string $filename): string
122+
public function makeFilename(string $filename, bool $maybeHasPath = true): string
121123
{
122124
if (strpos($filename, 'php://') !== false) {
123125
return $filename;
124126
}
125-
$extension = $this->getWriterConfig()['extension'];
126-
if (pathinfo($filename, PATHINFO_EXTENSION) !== $extension) {
127-
$filename .= '.' . $extension;
127+
128+
$dirname = '';
129+
$fileBasename = $filename;
130+
if ($maybeHasPath) {
131+
// 格式化文件路径
132+
$filename = Path::canonicalize($filename);
133+
// 截取 / 之后的作为文件名
134+
if (($pos = strrpos($filename, '/')) !== false) {
135+
$dirname = substr($filename, 0, $pos + 1); // 保留 /
136+
$fileBasename = substr($filename, $pos + 1);
137+
}
128138
}
129139

130-
return $filename;
140+
$configExtension = $this->getWriterConfig()['extension'];
141+
if (($pos = strpos($fileBasename, '.' . $configExtension . '?')) !== false) {
142+
// 兼容同扩展名情况下,存在 ?x=1 的情况,比如:abc.csv?x=1,此时直接将 ? 后的删除
143+
$fileBasename = substr($fileBasename, 0, $pos);
144+
}
145+
146+
// 去除特殊符号
147+
$invalidChars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
148+
$fileBasename = str_replace($invalidChars, array_fill(0, count($invalidChars) - 1, $this->getConfig()['filenameInvalidCharsReplace']), $fileBasename); // 替换文件系统不支持的特殊符号
149+
150+
// 补全 ext
151+
if (pathinfo($fileBasename, PATHINFO_EXTENSION) !== $configExtension) {
152+
$fileBasename .= '.' . $configExtension;
153+
}
154+
155+
return $dirname . $fileBasename;
131156
}
132157

133158
/**
134-
* @return array{writer: array, deleteFirstIfExist: bool}
159+
* @return array{writer: array, deleteFirstIfExist: bool, filenameInvalidCharsReplace: string, filenameMaker: ?callable}
135160
*/
136161
private function getConfig(): array
137162
{
138-
return $this->container->get(static::CONTAINER_DATA_EXPORT_CONFIG_KEY);
163+
return array_merge([
164+
'writer' => [],
165+
'deleteFirstIfExist' => true,
166+
'filenameInvalidCharsReplace' => '_',
167+
], $this->container->get(static::CONTAINER_DATA_EXPORT_CONFIG_KEY));
139168
}
140169

141170
/**

tests/Feature/Handler.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,39 @@
2525
$name = 'test';
2626
$builder = DataExporter::csv([['a']]);
2727
$realName = $builder->makeFilename($name);
28-
2928
expect($realName)->toBe('test.csv');
29+
30+
$name = __DIR__ . '/../test';
31+
$builder = DataExporter::csv([['a']]);
32+
$realName = $builder->makeFilename($name);
33+
expect($realName)->toBe(str_replace('\\', '/', dirname(__DIR__)) . '/test.csv');
34+
35+
// windows
36+
$name = 'G:\\\\abc\\test';
37+
$builder = DataExporter::csv([['a']]);
38+
$realName = $builder->makeFilename($name);
39+
expect($realName)->toBe('G://abc/test.csv');
40+
41+
// linux
42+
$name = '/abc/test';
43+
$builder = DataExporter::csv([['a']]);
44+
$realName = $builder->makeFilename($name);
45+
expect($realName)->toBe('/abc/test.csv');
46+
});
47+
48+
it('Handler makeFile can solve invalid character', function () {
49+
$name = 'abc /\\:*"<>|.csv?x=1';
50+
$builder = DataExporter::csv([['a']]);
51+
52+
$realName = $builder->makeFilename($name);
53+
expect($realName)->toBe('abc /_____.csv'); // / 被认为是路径分隔符,因此留下了
54+
55+
$realName = $builder->makeFilename($name, false);
56+
expect($realName)->toBe('abc _______.csv'); // 设置不含 path 后,/ 不会被认为是路径分隔符
57+
58+
$name = 'abc /\\:*"<>|?x=1';
59+
$builder = DataExporter::csv([['a']]);
60+
61+
$realName = $builder->makeFilename($name, false);
62+
expect($realName)->toBe('abc ________x=1.csv'); // 因为不存在 .csv 后缀在 name 中,因此 ? 会被保留替换
3063
});

tests/Feature/WriterTest.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use Kriss\DataExporter\DataExporter;
44
use PhpOffice\PhpSpreadsheet\IOFactory;
5+
use Symfony\Component\Filesystem\Path;
56

67
beforeEach(function () {
78
$this->source = [
@@ -16,79 +17,79 @@
1617
it('Writer csv', function () {
1718
$filename = DataExporter::csv($this->source)->saveAs($this->filename);
1819

19-
expect($this->filename . '.csv')->toBe($filename);
20+
expect(Path::canonicalize($this->filename . '.csv'))->toBe($filename);
2021
$factory = IOFactory::load($filename);
2122
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
2223
});
2324

2425
it('Writer xlsx', function () {
2526
$filename = DataExporter::xlsx($this->source)->saveAs($this->filename);
2627

27-
expect($this->filename . '.xlsx')->toBe($filename);
28+
expect(Path::canonicalize($this->filename . '.xlsx'))->toBe($filename);
2829
$factory = IOFactory::load($filename);
2930
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
3031
});
3132

3233
it('Writer xls', function () {
3334
$filename = DataExporter::xls($this->source)->saveAs($this->filename);
3435

35-
expect($this->filename . '.xls')->toBe($filename);
36+
expect(Path::canonicalize($this->filename . '.xls'))->toBe($filename);
3637
$factory = IOFactory::load($filename);
3738
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
3839
});
3940

4041
it('Writer csvSpout', function () {
4142
$filename = DataExporter::csvSpout($this->source)->saveAs($this->filename);
4243

43-
expect($this->filename . '.csv')->toBe($filename);
44+
expect(Path::canonicalize($this->filename . '.csv'))->toBe($filename);
4445
$factory = IOFactory::load($filename);
4546
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
4647
});
4748

4849
it('Writer xlsxSpout', function () {
4950
$filename = DataExporter::xlsxSpout($this->source)->saveAs($this->filename);
5051

51-
expect($this->filename . '.xlsx')->toBe($filename);
52+
expect(Path::canonicalize($this->filename . '.xlsx'))->toBe($filename);
5253
$factory = IOFactory::load($filename);
5354
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
5455
});
5556

5657
it('Writer odsSpout', function () {
5758
$filename = DataExporter::odsSpout($this->source)->saveAs($this->filename);
5859

59-
expect($this->filename . '.ods')->toBe($filename);
60+
expect(Path::canonicalize($this->filename . '.ods'))->toBe($filename);
6061
$factory = IOFactory::load($filename);
6162
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
6263
});
6364

6465
it('Writer csvSpreadsheet', function () {
6566
$filename = DataExporter::csvSpreadsheet($this->source)->saveAs($this->filename);
6667

67-
expect($this->filename . '.csv')->toBe($filename);
68+
expect(Path::canonicalize($this->filename . '.csv'))->toBe($filename);
6869
$factory = IOFactory::load($filename);
6970
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
7071
});
7172

7273
it('Writer xlsSpreadsheet', function () {
7374
$filename = DataExporter::xlsSpreadsheet($this->source)->saveAs($this->filename);
7475

75-
expect($this->filename . '.xls')->toBe($filename);
76+
expect(Path::canonicalize($this->filename . '.xls'))->toBe($filename);
7677
$factory = IOFactory::load($filename);
7778
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
7879
});
7980

8081
it('Writer xlsxSpreadsheet', function () {
8182
$filename = DataExporter::xlsxSpreadsheet($this->source)->saveAs($this->filename);
8283

83-
expect($this->filename . '.xlsx')->toBe($filename);
84+
expect(Path::canonicalize($this->filename . '.xlsx'))->toBe($filename);
8485
$factory = IOFactory::load($filename);
8586
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
8687
});
8788

8889
it('Writer odsSpreadsheet', function () {
8990
$filename = DataExporter::odsSpreadsheet($this->source)->saveAs($this->filename);
9091

91-
expect($this->filename . '.ods')->toBe($filename);
92+
expect(Path::canonicalize($this->filename . '.ods'))->toBe($filename);
9293
$factory = IOFactory::load($filename);
9394
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
9495
});

tests/Unit/DataExporterTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Kriss\DataExporter\DataExporter;
4+
use Symfony\Component\Filesystem\Path;
45

56
it('DataExporter __callStatic result type correct', function () {
67
$exporter = DataExporter::xlsx([['a', 'b']]);
@@ -11,5 +12,5 @@
1112
$savePath = __DIR__ . '/../tmp/test';
1213
$savePath = DataExporter::xlsx([['a', 'b']])->saveAs($savePath);
1314

14-
expect($savePath)->toBe(__DIR__ . '/../tmp/test.xlsx');
15+
expect($savePath)->toBe(Path::canonicalize(__DIR__ . '/../tmp/test.xlsx'));
1516
});

0 commit comments

Comments
 (0)