Skip to content

Commit 4eab6df

Browse files
committed
Implement FTP support
1 parent e0c1242 commit 4eab6df

File tree

5 files changed

+144
-6
lines changed

5 files changed

+144
-6
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,35 @@ Zip::create("package.zip")
148148
->addFromDisk("s3", "object.pdf", "Something.pdf");
149149
```
150150

151-
In this case the S3 client from the storage disk will be used.
151+
In this case the S3 client from the storage disk will be used.
152+
153+
## Support for FTP
154+
155+
You can stream files from an FTP server into your zip. There's two ways to achieve this:
156+
157+
### Add files from disk
158+
159+
For this you will need to [configure an FTP disk.](https://laravel.com/docs/12.x/filesystem#ftp-driver-configuration)
160+
161+
```php
162+
Zip::create('package.zip')
163+
->addFromDisk('ftp', 'object.pdf', 'Something.pdf');
164+
165+
\\ OR
166+
167+
$disk = Storage::disk('custom_ftp_disk');
168+
Zip::create('package.zip')
169+
->addFromDisk($disk, 'object.pdf', 'Something.pdf');
170+
```
171+
172+
### Add files from a FTP URL
173+
174+
Pass a valid [FTP URL](https://www.php.net/manual/en/wrappers.ftp.php) to add files from a custom URL:
175+
176+
```php
177+
Zip::create('package.zip')
178+
->add('ftp://example.com/pub/file.txt', 'object.pdf', 'Something.pdf');
179+
```
152180

153181
## Specify your own filesizes
154182

composer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@
2626
},
2727
"require-dev": {
2828
"league/flysystem-aws-s3-v3": "^3.28",
29+
"league/flysystem-ftp": "^3.29",
2930
"orchestra/testbench": "^8.0|^9.0|^10.0",
3031
"phpunit/phpunit": "^9.0|^10.0|^11.5.3"
3132
},
33+
"suggest": {
34+
"league/flysystem-ftp": "Allows FTP support."
35+
},
3236
"autoload": {
3337
"psr-4": {
3438
"STS\\ZipStream\\": "src"

src/Models/File.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
use Illuminate\Filesystem\FilesystemAdapter;
77
use Illuminate\Support\Arr;
88
use Illuminate\Support\Facades\Storage;
9+
use Illuminate\Support\Str;
10+
use League\Flysystem\Ftp\FtpAdapter;
911
use League\Flysystem\Local\LocalFilesystemAdapter;
1012
use Psr\Http\Message\StreamInterface;
11-
use Illuminate\Support\Str;
1213
use STS\ZipStream\Contracts\FileContract;
1314
use STS\ZipStream\Exceptions\UnsupportedSourceDiskException;
1415
use STS\ZipStream\OutputStream;
@@ -48,6 +49,10 @@ public static function make(string $source, ?string $zipPath = null): static
4849
return new HttpFile($source, $zipPath);
4950
}
5051

52+
if (Str::startsWith($source, "ftp") && filter_var($source, FILTER_VALIDATE_URL)) {
53+
return new FtpFile($source, $zipPath);
54+
}
55+
5156
if (Str::startsWith($source, "/") || preg_match('/^\w:[\/\\\\]/', $source) || file_exists($source)) {
5257
return new LocalFile($source, $zipPath);
5358
}
@@ -78,30 +83,42 @@ public static function makeFromDisk($disk, string $source, ?string $zipPath = nu
7883
);
7984
}
8085

86+
if($disk->getAdapter() instanceof FtpAdapter) {
87+
return FtpFile::makeFromDisk($disk, $source, $zipPath);
88+
}
89+
8190
throw new UnsupportedSourceDiskException("Unsupported disk type");
8291
}
8392

84-
public static function makeWriteable(string $source): S3File|LocalFile
93+
public static function makeWriteable(string $source): static
8594
{
8695
if (Str::startsWith($source, "s3://")) {
8796
return new S3File($source);
8897
}
8998

99+
if (Str::startsWith($source, "ftp") && filter_var($source, FILTER_VALIDATE_URL)) {
100+
return new FtpFile($source);
101+
}
102+
90103
return new LocalFile($source);
91104
}
92105

93-
public static function makeWriteableFromDisk($disk, string $source): S3File|LocalFile
106+
public static function makeWriteableFromDisk($disk, string $source): static
94107
{
95-
if(!$disk instanceof FilesystemAdapter) {
108+
if (!$disk instanceof FilesystemAdapter) {
96109
$disk = Storage::disk($disk);
97110
}
98111

99-
if($disk instanceof AwsS3V3Adapter) {
112+
if ($disk instanceof AwsS3V3Adapter) {
100113
return S3File::make(
101114
"s3://" . Arr::get($disk->getConfig(), "bucket") . "/" . $disk->path($source)
102115
)->setS3Client($disk->getClient());
103116
}
104117

118+
if ($disk->getAdapter() instanceof FtpAdapter) {
119+
return FtpFile::makeFromDisk($disk, $source);
120+
}
121+
105122
return new LocalFile(
106123
$disk->path($source)
107124
);

src/Models/FtpFile.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace STS\ZipStream\Models;
4+
5+
use GuzzleHttp\Psr7\Utils;
6+
use Illuminate\Filesystem\FilesystemAdapter;
7+
use Illuminate\Support\Str;
8+
use Psr\Http\Message\StreamInterface;
9+
use STS\ZipStream\OutputStream;
10+
11+
class FtpFile extends File
12+
{
13+
protected FilesystemAdapter $disk;
14+
15+
public static function makeFromDisk($disk, string $source, ?string $zipPath = null): static
16+
{
17+
$config = $disk->getConfig();
18+
19+
$protocol = !empty($config['ssl']) ? 'ftps://' : 'ftp://';
20+
$host = $config['host'] ?? '';
21+
22+
$user = $config['username'] ?? null;
23+
$pass = $config['password'] ?? '';
24+
$userInfo = $user ? "$user:$pass@" : '';
25+
26+
$root = $config['root'] ?? '';
27+
$root = $root ? Str::start($root, '/') : '';
28+
29+
return new static($protocol . $userInfo . $host . $root .'/'. $source, $zipPath);
30+
}
31+
32+
public function canPredictZipDataSize(): bool
33+
{
34+
return true;
35+
}
36+
37+
public function calculateFilesize(): int
38+
{
39+
return stat($this->source)['size'];
40+
}
41+
42+
protected function buildReadableStream(): StreamInterface
43+
{
44+
return Utils::streamFor($this->getStream('rb'));
45+
}
46+
47+
protected function buildWritableStream(): OutputStream
48+
{
49+
return new OutputStream($this->getStream('wb'));
50+
}
51+
52+
/**
53+
* @return resource
54+
*/
55+
protected function getStream(string $mode)
56+
{
57+
return fopen($this->source, $mode, context: stream_context_create([
58+
'ftp' => [
59+
'overwrite' => true
60+
]
61+
]));
62+
}
63+
}

tests/FileTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<?php
22

3+
use Illuminate\Support\Facades\Storage;
34
use Orchestra\Testbench\TestCase;
45
use STS\ZipStream\Models\File;
6+
use STS\ZipStream\Models\FtpFile;
57
use STS\ZipStream\Models\HttpFile;
68
use STS\ZipStream\Models\LocalFile;
79
use STS\ZipStream\Models\S3File;
@@ -19,7 +21,10 @@ protected function getPackageProviders($app)
1921

2022
public function testMake()
2123
{
24+
Storage::fake('ftp');
25+
2226
$this->assertInstanceOf(S3File::class, File::make('s3://bucket/key'));
27+
$this->assertInstanceOf(FtpFile::class, File::make('ftp://foo.com/bar.txt'));
2328
$this->assertInstanceOf(LocalFile::class, File::make('/dev/null'));
2429
$this->assertInstanceOf(LocalFile::class, File::make('/tmp/foobar'));
2530
$this->assertInstanceOf(LocalFile::class, File::make('C:/foo/bar'));
@@ -29,6 +34,9 @@ public function testMake()
2934

3035
public function testMakeWriteable()
3136
{
37+
Storage::fake('ftp');
38+
39+
$this->assertInstanceOf(FtpFile::class, File::makeWriteable('ftp://foo.com/bar.zip'));
3240
$this->assertInstanceOf(S3File::class, File::makeWriteable('s3://bucket/key'));
3341
$this->assertInstanceOf(LocalFile::class, File::makeWriteable('/tmp/foobar'));
3442
$this->assertInstanceOf(LocalFile::class, File::makeWriteable("C:/"));
@@ -115,4 +123,22 @@ public function testFromS3Disk()
115123
$this->assertInstanceOf(S3File::class, $file);
116124
$this->assertEquals('s3://my-test-bucket/my-prefix/test.txt', $file->getSource());
117125
}
126+
127+
public function testFromFtpDisk()
128+
{
129+
$options = [
130+
'driver' => 'ftp',
131+
'host' => 'ftpserver',
132+
];
133+
134+
config(['filesystems.disks.ftp' => $options]);
135+
config(['filesystems.disks.ftps' => $options + ['ssl' => true]]);
136+
137+
$file1 = File::makeFromDisk('ftp', 'file1.txt');
138+
$file2 = File::makeFromDisk('ftps', 'file2.txt');
139+
140+
$this->assertContainsOnlyInstancesOf(FtpFile::class, [$file1, $file2]);
141+
$this->assertEquals('ftp://ftpserver/file1.txt', $file1->getSource());
142+
$this->assertEquals('ftps://ftpserver/file2.txt', $file2->getSource());
143+
}
118144
}

0 commit comments

Comments
 (0)