Repository navigation
File System
The Machinjiri FileSystem component provides a single, consistent API for reading, writing, moving, copying, and managing files across multiple storage backends. It follows the adapter pattern: a
FileSystemfacade wraps a swappableFileSystemContractimplementation (the adapter), so your application code never needs to know whether it's writing to a local disk, an FTP server, Google Drive, or OneDrive.
// Identical code regardless of the underlying storage
$fs = $manager->disk('local');
$fs->write('reports/q1.pdf', $pdfContents);
$cloud = $manager->disk('google');
$cloud->write('reports/q1.pdf', $pdfContents); // same API- Uniform API — one set of methods for every backend.
- Pluggable adapters — swap storage by changing configuration, not code.
- Streams first — read and write large files without exhausting memory.
- Disk manager — define named disks (like Laravel's filesystem) and resolve them lazily.
- Cloud-ready — first-class Google Drive (v3) and OneDrive (Microsoft Graph v1.0) adapters.
- Auth helpers — pluggable token providers (Google service account, Graph client credentials).
- Resilience — optional rate limiting, caching, and structured logging.
- Path safety — cloud adapters normalize paths to prevent traversal outside the configured root.
PHP's built-in filesystem functions (file_get_contents, fopen, rename, copy) only work locally. When you need the same application logic to support local files and cloud storage, you'd normally end up with branching code (if local { ... } elseif ftp { ... }). This component collapses those branches into one interface.
| Feature | Native PHP | Machinjiri FS |
|---|---|---|
| Local files | ✅ | ✅ |
| FTP | Manual | ✅ |
| Google Drive | ❌ | ✅ |
| OneDrive | ❌ | ✅ |
| Streams for all backends | Manual | ✅ |
| Named disks (config) | ❌ | ✅ |
| Path traversal protection | Manual | ✅ (cloud) |
| Rate limiting | ❌ | ✅ |
| Caching metadata | ❌ | ✅ (cloud) |
┌────────────────────────────┐
│ FileSystemManager │ (disk resolver)
│ - reads config │
│ - caches disk instances │
└─────────────┬──────────────┘
│ returns
▼
┌────────────────────────────┐
│ FileSystem │ (facade)
│ - implements Contract │
│ - delegates to adapter │
└─────────────┬──────────────┘
│ delegates to
▼
┌────────────────────────────┐
│ FileSystemContract │ (interface)
└─────────────┬──────────────┘
│ implemented by
┌───────────────┬───────┴───────┬──────────────────┐
▼ ▼ ▼ ▼
LocalAdapter FtpAdapter CloudAdapter (your adapter)
(abstract)
│
┌──────────────┴──────────────┐
▼ ▼
GoogleDrive OneDrive
│ │
└──────────┬──────────────────┘
▼
CloudClientInterface
│
▼
HttpCloudClient
│
▼
HttpRequest (underlying client)
| Layer | Responsibility |
|---|---|
| FileSystemManager | Reads config, resolves disk names to FileSystem instances, caches them. |
| FileSystem (facade) | Implements FileSystemContract, forwards every call to the adapter. |
Adapters (LocalAdapter, FtpAdapter, CloudAdapter subclasses) |
Do the actual I/O. |
| CloudAdapter | Normalizes paths, defines primitives (fetchMetadata, uploadStream, etc.), and implements the full FileSystemContract in terms of those primitives. |
| CloudClientInterface / HttpCloudClient | Abstract HTTP transport (auth, rate limiting, logging). |
| TokenProviderInterface | Supply OAuth bearer tokens. |
use Mlangeni\Machinjiri\Core\FileSystem\FileSystemManager;
$config = [
'default' => 'local',
'disks' => [
'local' => [
'driver' => 'local',
'root' => __DIR__ . '/storage',
],
'ftp' => [
'driver' => 'ftp',
'host' => 'ftp.example.com',
'port' => 21,
'username' => 'user',
'password' => 'secret',
'root' => '/public_html',
'passive' => true,
'ssl' => false,
'timeout' => 90,
],
],
];
$manager = new FileSystemManager($config);$fs = $manager->disk('local'); // or disk() for default
$fs->write('hello.txt', 'Hello, world!');
if ($fs->exists('hello.txt')) {
echo $fs->read('hello.txt'); // "Hello, world!"
echo $fs->size('hello.txt'); // 13
}
$fs->copy('hello.txt', 'backup/hello.txt');
$fs->move('backup/hello.txt', 'archive/hello.txt');
$fs->delete('archive/hello.txt');
print_r($fs->listContents('', true));That's it. The same calls work against disk('ftp').
Configuration is a plain PHP array. Each disk requires a driver key and may supply driver-specific options.
| Key | Applies to | Description |
|---|---|---|
driver |
all |
local, ftp, or a cloud driver you register. |
root |
local, ftp, cloud | Filesystem root / remote base directory. |
host, port, username, password
|
ftp | Connection credentials. |
ssl |
ftp | Use ftp_ssl_connect (FTPS). |
passive |
ftp | Passive mode (usually true). |
timeout |
ftp | Connection timeout in seconds. |
client |
cloud | A pre-built CloudClientInterface (optional). |
root |
cloud | Prefix stripped/added to every path. |
$config = [
'default' => 'local',
'disks' => [
'local' => ['driver' => 'local', 'root' => '/var/app/storage'],
'backups' => ['driver' => 'local', 'root' => '/mnt/backups'],
'cdn' => ['driver' => 'ftp', /* ... */],
],
];disk('backups') and disk('local') return different adapters over different roots. The manager caches each by name, so repeated calls return the same instance.
All adapters implement Mlangeni\Machinjiri\Core\FileSystem\Contracts\FileSystem:
interface FileSystem
{
public function read(string $path): string;
public function readStream(string $path); // resource
public function write(string $path, string $contents, array $config = []): bool;
public function writeStream(string $path, $resource, array $config = []): bool;
public function exists(string $path): bool;
public function delete(string $path): bool;
public function move(string $source, string $destination): bool;
public function copy(string $source, string $destination): bool;
public function listContents(string $directory = '', bool $recursive = false): array;
public function size(string $path): int;
public function lastModified(string $path): int;
public function getVisibility(string $path): string;
public function setVisibility(string $path, string $visibility): bool;
}Paths are always relative to the disk root and use forward slashes: reports/2024/q1.pdf.
getVisibility / setVisibility accept 'public' and 'private'.
-
Local —
public= mode0644,private=0600. -
FTP — attempts
ftp_chmod(may be unsupported on some servers). - Google Drive / OneDrive — "public" means an anonymous sharing link exists.
Use streams for large files:
$src = fopen('/tmp/huge.zip', 'rb');
$fs->writeStream('uploads/huge.zip', $src);
fclose($src);
$handle = $fs->readStream('uploads/huge.zip');
while (!feof($handle)) {
echo fread($handle, 8192);
}
fclose($handle);For local disks, readStream returns an rb file handle. For FTP and cloud, it returns a php://temp buffer or a streamed response.
| Method | Returns | Throws on failure |
|---|---|---|
read($path) |
string |
MachinjiriException |
readStream($path) |
resource |
MachinjiriException |
write($path, $contents, $config = []) |
bool |
MachinjiriException |
writeStream($path, $resource, $config = []) |
bool |
MachinjiriException |
exists($path) |
bool |
never |
delete($path) |
bool |
MachinjiriException |
move($src, $dst) |
bool |
MachinjiriException |
copy($src, $dst) |
bool |
MachinjiriException |
listContents($dir = '', $recursive = false) |
array |
MachinjiriException (cloud) / [] (local missing dir) |
size($path) |
int |
MachinjiriException |
lastModified($path) |
int (Unix timestamp) |
MachinjiriException |
getVisibility($path) |
'public' | 'private'
|
MachinjiriException |
setVisibility($path, $visibility) |
bool |
MachinjiriException |
getAdapter() |
FileSystemContract |
— |
| Key | Values | Applies to |
|---|---|---|
visibility |
'public', 'private'
|
all |
conflict |
'replace', 'rename', 'fail'
|
cloud (Google, OneDrive) |
[
[
'type' => 'file' | 'dir',
'path' => 'relative/path/from/root',
'basename' => 'name.ext',
'size' => 1234, // files only
'lastModified' => 1700000000, // Unix timestamp
],
// ...
]Note: directory
size/lastModifiedfields are only populated by cloud adapters; local adapter returns them only for files (and directory entries only at the top level).
Wraps PHP's local filesystem.
- Root directory is created automatically if missing (mode
0755). - Parent directories are auto-created on
write/writeStream/move/copy. - If a file is read-only, the adapter makes it writable before write/move/copy/delete (adds owner-write bit).
- Uses
LOCK_EXon writes. - Extras (not on the interface):
append($path, $contents, $config = [])andappendStream($path, $resource, $config = []).
/** @var LocalAdapter $local */
$local = $manager->disk('local')->getAdapter();
$local->append('log.txt', "new line\n");Caution:
append/appendStreamare onLocalAdapteronly — not available through theFileSystemfacade or on other adapters. Access viagetAdapter().
Uses PHP's FTP extension.
- Establishes a connection in the constructor — constructing throws if the server is unreachable or login fails.
- Supports FTPS via
ssl => true. - Root is
chdir'd on connect. -
copy()is implemented asread()+write()(FTP has no native copy). -
listContents()parsesftp_rawlistoutput — may be fragile on non-standard FTP servers. -
getVisibility()always returns'public';setVisibility()attemptsftp_chmod. - Connection is closed in
__destruct().
Subclasses (GoogleDrive, OneDrive) supply primitives, and CloudAdapter provides the full interface:
| Primitive (abstract) | Purpose |
|---|---|
fetchMetadata($path): ?array |
Metadata or null if not found |
fetchContents($path): string |
Download as string |
fetchStream($path): resource |
Download as stream |
uploadContents($path, $contents, $config) |
Upload string |
uploadStream($path, $resource, $config) |
Upload stream |
removePath($path): bool |
Delete |
movePath($src, $dst): bool |
Move/rename |
copyPath($src, $dst): bool |
Copy |
listPath($path, $recursive): array |
Directory listing |
applyVisibility($path, $visibility): bool |
Public/private toggle |
It also provides:
-
Path normalization collapsing
.and.., preventing traversal. -
Optional cache (
setCache) for metadata (cacheGet/cacheSet/cacheForget). -
Optional logger (
setLogger) — logs via$logger->{level}($msg, $ctx)if the method exists. - Constants:
VISIBILITY_PUBLIC,VISIBILITY_PRIVATE,CONFLICT_REPLACE,CONFLICT_RENAME,CONFLICT_FAIL.
$items = $fs->listContents('uploads', true);
foreach ($items as $item) {
printf("%s %s (%d bytes)\n", $item['type'], $item['path'], $item['size'] ?? 0);
}$fs->write('public/logo.png', $data, ['visibility' => 'public']);The API only copies within a disk. To copy between disks, bridge with streams:
$src = $manager->disk('local')->readStream('big.zip');
$manager->disk('ftp')->writeStream('backups/big.zip', $src);
fclose($src);exists() never throws — use it to guard.
if (!$fs->exists('config/app.json')) {
$fs->write('config/app.json', '{}');
}$fs->write('report.pdf', $pdf, [
'conflict' => 'rename', // or 'replace', 'fail'
]);-
replace(default) — overwrite existing file. -
fail— throw if file exists (409). -
rename— append a counter (report (1).pdf).
Cloud adapters implement resumable chunked uploads:
-
Google Drive: 8 MiB chunks,
PUTwithContent-Range(tolerates308 Resume Incomplete). -
OneDrive: 1.6 MiB chunks (multiple of 320 KiB, Graph requirement), follows
nextExpectedRanges.
Always pass a seekable stream for best results:
$handle = fopen('/tmp/video.mp4', 'rb');
$fs->writeStream('videos/video.mp4', $handle);
fclose($handle);use Mlangeni\Machinjiri\Core\Artisans\Caching\CacheManager;
$cache = new CacheManager(/* ... */);
$adapter = $manager->disk('google')->getAdapter();
$adapter->setCache($cache); // PSR-16-shaped: get/set/deleteGoogle Drive caches metadata for 300 seconds; OneDrive invalidates per-path on write.
$logger = LoggerFactory::make(/* ... */);
$manager->disk('google')->getAdapter()->setLogger($logger);Both CloudAdapter and HttpCloudClient use the logger. HttpCloudClient logs cloud.request with method, uri, status, duration.
use Mlangeni\Machinjiri\Core\FileSystem\Cloud\Adapters\GoogleDrive;
use Mlangeni\Machinjiri\Core\FileSystem\Cloud\HttpCloudClient;
use Mlangeni\Machinjiri\Core\FileSystem\Cloud\TokenProviders\GoogleServiceAccountTokenProvider;
use Mlangeni\Machinjiri\Core\Http\HttpRequest;
$http = new HttpRequest(/* ... */);
$client = new HttpCloudClient($http);
$provider = new GoogleServiceAccountTokenProvider(
clientEmail: 'service-account@project.iam.gserviceaccount.com',
privateKey: file_get_contents('/path/to/key.pem'),
scope: 'https://www.googleapis.com/auth/drive'
);
$client->setTokenProvider($provider);
$drive = new GoogleDrive($client, root: 'app-data');
$drive->write('docs/readme.txt', 'hello');Key behaviours:
-
Path resolution walks the Drive hierarchy, memoizing IDs in
$idCachefor the request. -
Uploads:
-
uploadContentsuses multipart (uploadType=multipart). -
uploadStreamuses resumable (uploadType=resumable).
-
-
Conflict handling: emulated via delete-then-create (
replace), pre-check (fail), or name-uniquing loop (rename). -
Visibility: "public" = an
anyonereader permission exists. -
getVisibilityoverride inspects permissions directly (cached metadata doesn't include it).
use Mlangeni\Machinjiri\Core\FileSystem\Cloud\Adapters\OneDrive;
use Mlangeni\Machinjiri\Core\FileSystem\Cloud\TokenProviders\GraphClientCredentialsProvider;
$client = new HttpCloudClient($http);
$client->setTokenProvider(new GraphClientCredentialsProvider(
tenantId: getenv('AZURE_TENANT_ID'),
clientId: getenv('AZURE_CLIENT_ID'),
clientSecret: getenv('AZURE_CLIENT_SECRET'),
));
$onedrive = new OneDrive($client, driveId: 'b!abcdEFGH...', root: 'Documents');
$onedrive->write('notes.txt', 'hello');Key behaviours:
-
Small uploads (≤ 4 MB) go via simple
PUT /content. - Larger uploads create an upload session and send chunks.
-
Conflict behavior maps directly to Graph's
@microsoft.graph.conflictBehavior(replace,rename,fail). -
Copy is async — a
202 Acceptedis treated as success. -
Visibility toggles an anonymous
viewlink.
CloudAdapter only needs a CloudClientInterface (with send() and stream()). Implement it to use any HTTP transport, or extend HttpCloudClient.
Both providers implement TokenProviderInterface and cache tokens in memory, refreshing 5 minutes before expiry.
Builds a JWT (RS256), signs with openssl_sign, exchanges it at https://oauth2.googleapis.com/token for a bearer token.
new GoogleServiceAccountTokenProvider(
clientEmail: '...',
privateKey: '-----BEGIN PRIVATE KEY-----...',
scope: 'https://www.googleapis.com/auth/drive'
);Uses the OAuth2 client credentials grant against
https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
with scope https://graph.microsoft.com/.default.
new GraphClientCredentialsProvider(
tenantId: '...',
clientId: '...',
clientSecret: '...'
);Production note: these providers use raw
curl_initinstead of your application HTTP client, so they bypass retries/proxies/telemetry configured onHttpRequest. Swap them for your own provider if you need full HTTP abstraction.
$client->withRateLimit(requestsPerSecond: 10.0, burst: 20);Uses a token bucket (TokenBucket) so bursts up to burst are allowed, then sustained at requestsPerSecond.
$client->setLogger($logger); // propagates to underlying HTTP client too-
HttpCloudClientlogs each request ascloud.request. -
CloudAdapterlogs vialog($level, $message, $context)if the logger has that method.
Implement the contract directly:
use Mlangeni\Machinjiri\Core\FileSystem\Contracts\FileSystem;
class S3Adapter implements FileSystem
{
public function read(string $path): string { /* ... */ }
public function readStream(string $path) { /* ... */ }
// ... all other methods
}Then add a match arm in FileSystemManager::resolve():
$adapter = match ($driver) {
'local' => new LocalAdapter($diskConfig['root'] ?? getcwd()),
'ftp' => new FtpAdapter($diskConfig),
's3' => new S3Adapter($diskConfig), // your driver
default => throw new MachinjiriException("Unsupported driver: {$driver}", 500),
};Far less work — implement only the primitives:
class S3Cloud extends CloudAdapter
{
protected function fetchMetadata(string $path): ?array { /* ... */ }
protected function fetchContents(string $path): string { /* ... */ }
protected function fetchStream(string $path) { /* ... */ }
protected function uploadContents(string $path, string $contents, array $config = []): void { /* ... */ }
protected function uploadStream(string $path, $resource, array $config = []): void { /* ... */ }
protected function removePath(string $path): bool { /* ... */ }
protected function movePath(string $source, string $destination): bool { /* ... */ }
protected function copyPath(string $source, string $destination): bool { /* ... */ }
protected function listPath(string $path, bool $recursive): array { /* ... */ }
protected function applyVisibility(string $path, string $visibility): bool { /* ... */ }
}You inherit path normalization, caching, logging, and interface wiring.
All failures throw Mlangeni\Machinjiri\Core\Exceptions\MachinjiriException. The second constructor argument is used as an HTTP-ish status code:
| Code | Meaning in this component |
|---|---|
400 |
Invalid argument (bad visibility) |
401 |
Authentication failure (FTP login) |
404 |
File/directory not found |
409 |
Conflict (cloud fail policy) |
500 |
General runtime failure (I/O, HTTP 5xx, config) |
use Mlangeni\Machinjiri\Core\Exceptions\MachinjiriException;
try {
$fs->read('missing.txt');
} catch (MachinjiriException $e) {
echo $e->getMessage(); // "File does not exist: missing.txt"
echo $e->getCode(); // 404
}exists() is the exception-free way to probe paths.
-
Always use streams for large files.
read()/write()load the whole payload into memory. UsereadStream/writeStream. -
Guard with
exists()rather than catching404s. -
Use
writeStreamfor cloud uploads. Google Drive and OneDrive's string uploads use multipart bodies, which duplicate memory. - Close streams you open. Adapters don't close resources they didn't create.
-
Use named disks per concern (
backups,cdn,uploads) instead of one giant disk. - Set explicit visibility when writing public assets — don't rely on defaults.
-
Use
conflict => 'rename'for user-uploaded files to avoid silent overwrites. - Set a logger in production — cloud calls are expensive; observability matters.
-
Rate-limit cloud clients shared by background jobs (
withRateLimit). -
Check
getAdapter()type before calling adapter-specific methods likeappend().
Install and enable ext-ftp:
sudo apt install php-ftp # Debian/Ubuntu
# or compile with --with-ftp- Verify
host,port, and firewall rules. - Try
ssl => trueif the server requires FTPS (usually port 990 or explicit TLS). - Increase
timeout.
The root config is chdir'd right after login. Make sure the path exists relative to the login home.
- Confirm the parent hierarchy exists. Cloud adapters do not auto-create directories — you must create folder items yourself (via the provider's API or pre-provisioned folders).
- For Google Drive, ensure the service account's
scopeincludesdrive. - For OneDrive, verify
driveIdis correct.
The Location header was missing — likely an auth or permission error. Inspect the response body via logging.
copyPath returns true on 202. The destination may not be visible immediately. Poll exists() or fetchMetadata() before depending on it.
ftp_rawlist formats vary. Some servers require ftp_pasv(..., true) (already the default) or return different column counts. Consider a custom FTP adapter for non-standard servers.
Both token providers refresh 5 minutes early. If you see 401s, check:
- Clock skew on your server.
-
scopecorrectness. - That the provided client is actually passing the
Authorizationheader (it does unless you already supplied one).
The process user lacks chmod rights (common on mounted shares). Fix ownership or run with sufficient privileges.
| Method | Signature | Notes |
|---|---|---|
__construct |
(array $config = []) |
Expects default + disks. |
disk |
(?string $name = null): FileSystem |
Cached by name; throws MachinjiriException on bad config. |
Delegates every interface method 1:1 to the wrapped adapter. Additional:
| Method | Returns |
|---|---|
getAdapter() |
FileSystemContract |
See The FileSystem API.
Implements FileSystem plus:
| Method | Signature |
|---|---|
append |
(string $path, string $contents, array $config = []): bool |
appendStream |
(string $path, $resource, array $config = []): bool |
Implements FileSystem. Constructor: (array $config).
Config keys: host, port, username, password, root, ssl, passive, timeout.
Implements FileSystem. Constructor: (CloudClientInterface $client, string $root = '', ?object $cache = null, ?object $logger = null).
Setters: setCache, setLogger.
Constants: VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, CONFLICT_REPLACE, CONFLICT_RENAME, CONFLICT_FAIL.
Constructor: (CloudClientInterface $client, string $root = '').
- Multipart upload for strings, resumable for streams.
- Supports
conflictconfig. -
getVisibilityinspects permissions.
Constructor: (CloudClientInterface $client, string $driveId, string $root = '').
- Simple PUT ≤ 4 MB, upload session > 4 MB.
- Graph
conflictBehaviorforconflictconfig. -
copyis asynchronous (202).
| Method | Signature |
|---|---|
__construct |
(HttpRequest $http, array $defaultHeaders = []) |
setLogger |
(object $logger): self |
setTokenProvider |
(?TokenProviderInterface $provider): self |
withRateLimit |
(float $requestsPerSecond, int $burst = 10): self |
send |
(string $method, string $uri, array $options = []): CloudResponse |
stream |
(string $method, string $uri, array $options = []): resource |
$options keys: headers (assoc), query (assoc), body (array or string).
| Method | Returns |
|---|---|
status() |
int |
headers() |
array |
header($name) |
?string |
body() |
string |
ok() |
bool (2xx) |
json() |
array |
public function getToken(): array; // ['token' => '...', 'expires_at' => 1234567890]Machinjiri Docs
- Home
- Learn
- Build
-
Tutorials
- Routing
- Controllers and Middleware
- Views and Templates
- Database ORM
- Eloquent Database ORM
- Errors and Exceptions
- Forms and Validation
- Http
- Events and Logging
- Services and DI
- Security
- Authentication
- Caching
- File System
- Date and Time
- Debugging
- Queue and Jobs
- Task Scheduling
- Transport (Mail, SMS)
- LDAP
- Notifications
- Webhooks
- UUID Generations
- Terminal
- Helper Function
- Reference