Skip to content

Commit 642ad20

Browse files
committed
✨ Adds a FTP client
1 parent 17bf74d commit 642ad20

File tree

10 files changed

+277
-1
lines changed

10 files changed

+277
-1
lines changed

.github/workflows/tests.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
push:
7+
branches: [ main ]
8+
9+
jobs:
10+
tests:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup PHP
17+
uses: shivammathur/setup-php@v2
18+
with:
19+
php-version: 8.3
20+
21+
- name: Cache Composer packages
22+
id: composer-cache
23+
uses: actions/cache@v3
24+
with:
25+
path: vendor
26+
key: ${{ runner.os }}-php-8.3-${{ hashFiles('**/composer.lock') }}
27+
restore-keys: |
28+
${{ runner.os }}-php-8.3-
29+
30+
- name: Install dependencies
31+
run: composer install --prefer-dist --no-progress
32+
33+
- name: Run PHP CS Fixer (dry-run)
34+
run: vendor/bin/php-cs-fixer fix ./ --rules=@PSR12 --dry-run --diff
35+
36+
- name: Run PHPStan
37+
run: vendor/bin/phpstan analyse component --memory-limit=4G

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ test/coverage
1010
.DS_Store
1111
.nova/*
1212
.php-cs-fixer.cache
13+
docker/

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,20 @@ $clientS3Bucket->set('foo', 'bar');
131131
$bar = $clientS3Bucket->get('foo');
132132
$clientS3Bucket->delete('foo');
133133
```
134+
135+
### FTP Client
136+
137+
```php
138+
use Phant\Client\Service\Ftp as FtpClient;
139+
140+
$ftpClient = new FtpClient(
141+
host: 'example.com',
142+
username: 'user',
143+
password: 'pass',
144+
//port: 21 (default)
145+
//passiveMode: false (default)
146+
);
147+
148+
$files = $ftpClient->listFiles('/path/to/directory');
149+
$downloadedFile = $ftpClient->download('/path/to/remote/file.txt', '/local/directory');
150+
```

component/Port/Ftp.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phant\Client\Port;
6+
7+
interface Ftp
8+
{
9+
public function listFiles(string $path): array;
10+
11+
/**
12+
* @param string $remotePath The path of the file on the FTP server
13+
* @param string|null $localDirectory The local directory to save the downloaded file. If null, a default temp directory will be used.
14+
* @return string Local file path of the downloaded file
15+
*
16+
* @throws \RuntimeException
17+
*/
18+
public function download(string $remotePath, ?string $localDirectory = null): string;
19+
20+
public function delete(string $remotePath): bool;
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Phant\Client\Port\Ftp\Exception;
4+
5+
class FtpClientException extends FtpException
6+
{
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Phant\Client\Port\Ftp\Exception;
4+
5+
class FtpConnectionException extends FtpException
6+
{
7+
public function __construct(string $host)
8+
{
9+
parent::__construct("Failed to connect to FTP server: {$host}");
10+
}
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Phant\Client\Port\Ftp\Exception;
4+
5+
abstract class FtpException extends \Exception
6+
{
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Phant\Client\Port\Ftp\Exception;
4+
5+
class FtpLoginException extends FtpException
6+
{
7+
public function __construct(string $host)
8+
{
9+
parent::__construct("Failed to log in to FTP server: {$host}");
10+
}
11+
}

component/Service/Ftp.php

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phant\Client\Service;
6+
7+
use FTP\Connection;
8+
use Phant\Client\Port\Ftp\Exception\FtpClientException;
9+
use Phant\Client\Port\Ftp\Exception\FtpConnectionException;
10+
use Phant\Client\Port\Ftp\Exception\FtpLoginException;
11+
12+
/**
13+
* FTP Service for interacting with FTP servers.
14+
*/
15+
class Ftp implements \Phant\Client\Port\Ftp
16+
{
17+
private Connection $connection;
18+
19+
public function __construct(
20+
string $host,
21+
string $username,
22+
string $password,
23+
int $port = 21,
24+
bool $passiveMode = false
25+
) {
26+
$this->connection = $this->connect(
27+
host: $host,
28+
username: $username,
29+
password: $password,
30+
port: $port
31+
);
32+
33+
ftp_pasv($this->connection, $passiveMode);
34+
}
35+
36+
private function connect(string $host, string $username, string $password, int $port): Connection
37+
{
38+
$ftp = ftp_connect($host, $port);
39+
if (!$ftp) {
40+
throw new FtpConnectionException($host);
41+
}
42+
43+
$login = ftp_login($ftp, $username, $password);
44+
if (!$login) {
45+
throw new FtpLoginException($host);
46+
}
47+
48+
return $ftp;
49+
}
50+
51+
/**
52+
* Retrieve a list of files in the specified directory.
53+
* @param string $path
54+
* @return array|bool
55+
* @throws FtpClientException
56+
*/
57+
public function listFiles(string $path): array
58+
{
59+
$list = ftp_nlist($this->connection, $path);
60+
if ($list === false) {
61+
throw new FtpClientException(error_get_last()['message'] ?? 'Unknown error during FTP listing');
62+
}
63+
64+
$files = array_filter($list, fn ($resource) => !is_dir($resource));
65+
66+
return $files;
67+
}
68+
69+
/**
70+
* Retrieve a list of folders in the specified directory.
71+
* @param string $path
72+
* @return array|bool
73+
* @throws FtpClientException
74+
*/
75+
public function listFolders(string $path): array
76+
{
77+
$list = ftp_nlist($this->connection, $path);
78+
if ($list === false) {
79+
throw new FtpClientException(error_get_last()['message'] ?? 'Unknown error during FTP listing');
80+
}
81+
82+
$folders = array_filter($list, fn ($resource) => is_dir($resource));
83+
84+
return $folders;
85+
}
86+
87+
/**
88+
* Retrieve a list of all resources in the specified directory.
89+
* @param string $path
90+
* @return array|bool
91+
* @throws FtpClientException
92+
*/
93+
public function listAll(string $path): array
94+
{
95+
$list = ftp_nlist($this->connection, $path);
96+
if ($list === false) {
97+
throw new FtpClientException(error_get_last()['message'] ?? 'Unknown error during FTP listing');
98+
}
99+
100+
if (false === $list) {
101+
return [];
102+
}
103+
104+
return $list;
105+
}
106+
107+
/**
108+
* Download a file from the FTP server. You can specify a local directory to save the file. If no directory is provided, the file will be saved in the system's temporary directory.
109+
* @param string $remotePath
110+
* @param ?string $localDirectory
111+
* @return string
112+
* @throws FtpClientException
113+
*/
114+
public function download(string $remoteFilePath, ?string $localDirectory = null): string
115+
{
116+
if ($localDirectory) {
117+
$localFilePath = rtrim($localDirectory, '/') . '/' . basename($remoteFilePath);
118+
} else {
119+
$localFilePath = sys_get_temp_dir() . '/' . basename($remoteFilePath);
120+
}
121+
122+
if (!is_dir(dirname($localFilePath))) {
123+
throw new FtpClientException("Local directory does not exist: " . dirname($localFilePath));
124+
}
125+
126+
if (!ftp_get($this->connection, $localFilePath, $remoteFilePath, FTP_BINARY)) {
127+
throw new FtpClientException("Failed to download file from FTP: {$remoteFilePath}. Error: " . (error_get_last()['message'] ?? 'Unknown error'));
128+
}
129+
130+
return $localFilePath;
131+
}
132+
133+
/**
134+
* Delete a file from the FTP server.
135+
* @param string $path
136+
* @return bool
137+
* @throws FtpClientException
138+
*/
139+
public function delete(string $path): bool
140+
{
141+
if (!ftp_delete($this->connection, $path)) {
142+
throw new FtpClientException("Failed to delete file from FTP: {$path}");
143+
}
144+
145+
return true;
146+
}
147+
148+
public function __destruct()
149+
{
150+
ftp_close($this->connection);
151+
}
152+
153+
public function lala()
154+
{
155+
$ftpClient = new self(
156+
host: 'example.com',
157+
username: 'user',
158+
password: 'pass'
159+
);
160+
161+
$files = $ftpClient->listFiles('/path/to/directory');
162+
$downloadedFile = $ftpClient->download('/path/to/remote/file.txt', '/local/directory');
163+
}
164+
}

component/Service/MySQL.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ protected function getPdo(
120120
if ($this->pdo) {
121121
return $this->pdo;
122122
}
123-
123+
124124
$this->pdo = new PDO(
125125
'mysql:host=' . $this->host . ';dbname=' . $this->dbname . ';port=' . $this->port . ';charset=' . $this->charset,
126126
$this->user,

0 commit comments

Comments
 (0)