diff --git a/README.md b/README.md index b29787d..c39260b 100755 --- a/README.md +++ b/README.md @@ -148,7 +148,35 @@ Zip::create("package.zip") ->addFromDisk("s3", "object.pdf", "Something.pdf"); ``` -In this case the S3 client from the storage disk will be used. +In this case the S3 client from the storage disk will be used. + +## Support for FTP + +You can stream files from an FTP server into your zip. There's two ways to achieve this: + +### Add files from disk + +For this you will need to [configure an FTP disk.](https://laravel.com/docs/12.x/filesystem#ftp-driver-configuration) + +```php +Zip::create('package.zip') + ->addFromDisk('ftp', 'object.pdf', 'Something.pdf'); + +\\ OR + +$disk = Storage::disk('custom_ftp_disk'); +Zip::create('package.zip') + ->addFromDisk($disk, 'object.pdf', 'Something.pdf'); +``` + +### Add files from a FTP URL + +Pass a valid [FTP URL](https://www.php.net/manual/en/wrappers.ftp.php) to add files from a custom URL: + +```php +Zip::create('package.zip') + ->add('ftp://example.com/pub/file.txt', 'object.pdf', 'Something.pdf'); +``` ## Specify your own filesizes diff --git a/composer.json b/composer.json index a361cda..0244c07 100755 --- a/composer.json +++ b/composer.json @@ -26,9 +26,13 @@ }, "require-dev": { "league/flysystem-aws-s3-v3": "^3.28", + "league/flysystem-ftp": "^3.29", "orchestra/testbench": "^8.0|^9.0|^10.0", "phpunit/phpunit": "^9.0|^10.0|^11.5.3" }, + "suggest": { + "league/flysystem-ftp": "Allows FTP support." + }, "autoload": { "psr-4": { "STS\\ZipStream\\": "src" diff --git a/src/Models/File.php b/src/Models/File.php index 7a1ee26..46c9bd3 100644 --- a/src/Models/File.php +++ b/src/Models/File.php @@ -6,9 +6,10 @@ use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; +use League\Flysystem\Ftp\FtpAdapter; use League\Flysystem\Local\LocalFilesystemAdapter; use Psr\Http\Message\StreamInterface; -use Illuminate\Support\Str; use STS\ZipStream\Contracts\FileContract; use STS\ZipStream\Exceptions\UnsupportedSourceDiskException; use STS\ZipStream\OutputStream; @@ -48,6 +49,10 @@ public static function make(string $source, ?string $zipPath = null): static return new HttpFile($source, $zipPath); } + if (Str::startsWith($source, "ftp") && filter_var($source, FILTER_VALIDATE_URL)) { + return new FtpFile($source, $zipPath); + } + if (Str::startsWith($source, "/") || preg_match('/^\w:[\/\\\\]/', $source) || file_exists($source)) { return new LocalFile($source, $zipPath); } @@ -78,30 +83,42 @@ public static function makeFromDisk($disk, string $source, ?string $zipPath = nu ); } + if($disk->getAdapter() instanceof FtpAdapter) { + return FtpFile::makeFromDisk($disk, $source, $zipPath); + } + throw new UnsupportedSourceDiskException("Unsupported disk type"); } - public static function makeWriteable(string $source): S3File|LocalFile + public static function makeWriteable(string $source): static { if (Str::startsWith($source, "s3://")) { return new S3File($source); } + if (Str::startsWith($source, "ftp") && filter_var($source, FILTER_VALIDATE_URL)) { + return new FtpFile($source); + } + return new LocalFile($source); } - public static function makeWriteableFromDisk($disk, string $source): S3File|LocalFile + public static function makeWriteableFromDisk($disk, string $source): static { - if(!$disk instanceof FilesystemAdapter) { + if (!$disk instanceof FilesystemAdapter) { $disk = Storage::disk($disk); } - if($disk instanceof AwsS3V3Adapter) { + if ($disk instanceof AwsS3V3Adapter) { return S3File::make( "s3://" . Arr::get($disk->getConfig(), "bucket") . "/" . $disk->path($source) )->setS3Client($disk->getClient()); } + if ($disk->getAdapter() instanceof FtpAdapter) { + return FtpFile::makeFromDisk($disk, $source); + } + return new LocalFile( $disk->path($source) ); diff --git a/src/Models/FtpFile.php b/src/Models/FtpFile.php new file mode 100644 index 0000000..1fefb2c --- /dev/null +++ b/src/Models/FtpFile.php @@ -0,0 +1,63 @@ +getConfig(); + + $protocol = !empty($config['ssl']) ? 'ftps://' : 'ftp://'; + $host = $config['host'] ?? ''; + + $user = $config['username'] ?? null; + $pass = $config['password'] ?? ''; + $userInfo = $user ? "$user:$pass@" : ''; + + $root = $config['root'] ?? ''; + $root = $root ? Str::start($root, '/') : ''; + + return new static($protocol . $userInfo . $host . $root .'/'. $source, $zipPath); + } + + public function canPredictZipDataSize(): bool + { + return true; + } + + public function calculateFilesize(): int + { + return stat($this->source)['size']; + } + + protected function buildReadableStream(): StreamInterface + { + return Utils::streamFor($this->getStream('rb')); + } + + protected function buildWritableStream(): OutputStream + { + return new OutputStream($this->getStream('wb')); + } + + /** + * @return resource + */ + protected function getStream(string $mode) + { + return fopen($this->source, $mode, context: stream_context_create([ + 'ftp' => [ + 'overwrite' => true + ] + ])); + } +} diff --git a/tests/FileTest.php b/tests/FileTest.php index 384c7a8..4262903 100644 --- a/tests/FileTest.php +++ b/tests/FileTest.php @@ -2,6 +2,7 @@ use Orchestra\Testbench\TestCase; use STS\ZipStream\Models\File; +use STS\ZipStream\Models\FtpFile; use STS\ZipStream\Models\HttpFile; use STS\ZipStream\Models\LocalFile; use STS\ZipStream\Models\S3File; @@ -20,6 +21,7 @@ protected function getPackageProviders($app) public function testMake() { $this->assertInstanceOf(S3File::class, File::make('s3://bucket/key')); + $this->assertInstanceOf(FtpFile::class, File::make('ftp://foo.com/bar.txt')); $this->assertInstanceOf(LocalFile::class, File::make('/dev/null')); $this->assertInstanceOf(LocalFile::class, File::make('/tmp/foobar')); $this->assertInstanceOf(LocalFile::class, File::make('C:/foo/bar')); @@ -29,6 +31,7 @@ public function testMake() public function testMakeWriteable() { + $this->assertInstanceOf(FtpFile::class, File::makeWriteable('ftp://foo.com/bar.zip')); $this->assertInstanceOf(S3File::class, File::makeWriteable('s3://bucket/key')); $this->assertInstanceOf(LocalFile::class, File::makeWriteable('/tmp/foobar')); $this->assertInstanceOf(LocalFile::class, File::makeWriteable("C:/")); @@ -115,4 +118,22 @@ public function testFromS3Disk() $this->assertInstanceOf(S3File::class, $file); $this->assertEquals('s3://my-test-bucket/my-prefix/test.txt', $file->getSource()); } + + public function testFromFtpDisk() + { + $options = [ + 'driver' => 'ftp', + 'host' => 'ftp_server', + ]; + + config(['filesystems.disks.ftp' => $options]); + config(['filesystems.disks.ftps' => $options + ['ssl' => true]]); + + $file1 = File::makeFromDisk('ftp', 'file1.txt'); + $file2 = File::makeFromDisk('ftps', 'file2.txt'); + + $this->assertContainsOnlyInstancesOf(FtpFile::class, [$file1, $file2]); + $this->assertEquals('ftp://ftp_server/file1.txt', $file1->getSource()); + $this->assertEquals('ftps://ftp_server/file2.txt', $file2->getSource()); + } }