File Manager

Overview

The FileManager class handles everything file-related: validating and saving uploads, resizing images, converting to WebP, reading and writing files, copying, moving, deleting, listing directories, and streaming downloads. It is a non-static utility — access it via $app->fileManager.

  • Class: JiFramework\Core\Utilities\FileManager
  • Access: $app->fileManager (lazy-loaded instance)
  • Upload defaults: any parameter left null falls back to the matching Config value — Config::$uploadDirectory, Config::$maxFileSize, Config::$allowedImageTypes, Config::$imageMaxDimension
  • Image support: JPEG, PNG, GIF, WebP (requires PHP GD extension)
// Upload and resize an image in one call
$result = $app->fileManager->uploadFile($_FILES['photo'], null, null, null, 800);

if ($result['success']) {
    $path = $result['data']['savedPath'];
    echo "Saved: " . $result['data']['uniqueName'];
}

// Convert an existing image to WebP
$webp = $app->fileManager->convertToWebp($path, null, 85);

// Read / write files
$app->fileManager->writeFile('storage/data.json', json_encode($payload));
$json = $app->fileManager->readFile('storage/data.json');

getUploadInfo()

getUploadInfo(array $file): array

Extract and validate information from a single $_FILES entry. Verifies that the file was genuinely uploaded via HTTP POST (guards against injection attacks) and computes a SHA-256 hash of the file contents for integrity checks.

  • $file(array) A single entry from $_FILES, e.g. $_FILES['photo'].

Returns: array with the following keys:

  • name(string) Sanitized filename (safe for use on any OS).
  • originalName(string) Original filename as submitted by the browser.
  • extension(string) Lowercase file extension, e.g. 'jpg'.
  • size(int) File size in bytes.
  • humanSize(string) Human-readable size, e.g. '1.20 MB'.
  • tmpName(string) Temporary file path on the server.
  • typeProvided(string) MIME type as reported by the browser (untrusted).
  • actualType(string) Real MIME type detected by PHP (trusted).
  • modificationTime(int) Unix timestamp of the temp file.
  • hash(string) SHA-256 hex digest of the file contents.

Throws: Exception if the file was not uploaded via HTTP POST.

$info = $app->fileManager->getUploadInfo($_FILES['avatar']);

echo $info['originalName'];  // 'My Photo.jpg'
echo $info['name'];          // 'My_Photo.jpg' (sanitized)
echo $info['actualType'];    // 'image/jpeg'
echo $info['humanSize'];     // '245.60 KB'
echo $info['hash'];          // 'a3f9d1...'

getMultipleUploadInfo()

getMultipleUploadInfo(array $files): array

Extract and validate information from a multi-file $_FILES entry (an input with multiple or a name like files[]). Calls getUploadInfo() for each file internally.

  • $files(array) Multi-file entry from $_FILES, e.g. $_FILES['photos'].

Returns: array — indexed array where each element is an info array identical to the return value of getUploadInfo().

Throws: Exception if any file was not uploaded via HTTP POST.

// HTML: <input type="file" name="photos[]" multiple>

$infos = $app->fileManager->getMultipleUploadInfo($_FILES['photos']);

foreach ($infos as $info) {
    echo $info['name'] . ' — ' . $info['humanSize'] . "
";
}

uploadFile()

uploadFile(array $file, ?string $destination = null, ?int $maxSize = null, ?array $allowedTypes = null, ?int $maxDim = null): array

Validate and save a single uploaded file. Any parameter left null falls back to its matching Config value. The saved filename is a cryptographically random hex string — the original name is never used for the path.

  • $file(array) Single entry from $_FILES.
  • $destination(string|null) Directory to save the file. Default: Config::$uploadDirectory.
  • $maxSize(int|null) Maximum file size in bytes. Default: Config::$maxFileSize.
  • $allowedTypes(array|null) Allowed MIME types. Default: Config::$allowedImageTypes.
  • $maxDim(int|null) Maximum image dimension in pixels. When set and the file is an image, resizeImage() is called automatically after saving. Default: null (no resize).

Returns: array

  • On success: ['success' => true, 'data' => array] where data contains all keys from getUploadInfo() plus savedPath and uniqueName.
  • On failure: ['success' => false, 'error' => string]
// Use all Config defaults
$result = $app->fileManager->uploadFile($_FILES['photo']);

// Custom destination and limits, auto-resize to max 800px
$result = $app->fileManager->uploadFile(
    $_FILES['photo'],
    'storage/avatars/',
    2 * 1024 * 1024,                         // 2 MB limit
    ['image/jpeg', 'image/png', 'image/webp'],
    800                                      // resize to max 800px
);

if ($result['success']) {
    $data = $result['data'];
    echo $data['savedPath'];   // 'storage/avatars/3f8a2c1b4d9e0f7a.jpg'
    echo $data['uniqueName']; // '3f8a2c1b4d9e0f7a.jpg'
    echo $data['humanSize'];  // '1.20 MB'
} else {
    echo $result['error']; // 'File type "image/gif" is not allowed.'
}

uploadMultipleFiles()

uploadMultipleFiles(array $files, ?string $destination = null, ?int $maxSize = null, ?array $allowedTypes = null, ?int $maxDim = null): array

Validate and save multiple uploaded files in one call. Accepts the same parameters as uploadFile(). Each file is processed independently — a failure on one file does not stop the others.

  • $files(array) Multi-file entry from $_FILES.
  • $destination(string|null) Directory to save all files. Default: Config::$uploadDirectory.
  • $maxSize(int|null) Max file size per file. Default: Config::$maxFileSize.
  • $allowedTypes(array|null) Allowed MIME types. Default: Config::$allowedImageTypes.
  • $maxDim(int|null) Max image dimension for auto-resize. Default: null.

Returns: array — indexed array of result arrays, one per file, each identical in structure to the return value of uploadFile().

// HTML: <input type="file" name="gallery[]" multiple>

$results = $app->fileManager->uploadMultipleFiles(
    $_FILES['gallery'],
    'storage/gallery/',
    5 * 1024 * 1024,
    ['image/jpeg', 'image/png', 'image/webp'],
    1200
);

foreach ($results as $i => $result) {
    if ($result['success']) {
        echo "File $i saved: " . $result['data']['uniqueName'] . "
";
    } else {
        echo "File $i failed: " . $result['error'] . "
";
    }
}

resizeImage()

resizeImage(string $photoPath, ?int $maxDim = null, ?string $savePath = null): bool

Resize an image so that neither its width nor height exceeds $maxDim, maintaining the original aspect ratio. Supports JPEG, PNG, GIF, and WebP. Preserves alpha transparency for PNG, GIF, and WebP. Images already within the dimension limit are copied unchanged (never upscaled).

  • $photoPath(string) Path to the source image.
  • $maxDim(int|null) Maximum width or height in pixels. Default: Config::$imageMaxDimension.
  • $savePath(string|null) Output path. Default: overwrites the source file in-place.

Returns: booltrue on success, false if the image type is unsupported.

Throws: Exception if the image cannot be read or the output cannot be written.

// Resize in-place (overwrites original)
$app->fileManager->resizeImage('storage/uploads/photo.jpg', 800);

// Resize to new path (original preserved)
$app->fileManager->resizeImage(
    'storage/uploads/original.jpg',
    400,
    'storage/uploads/thumbnail.jpg'
);

// No upscale — a 300x200 image with maxDim=800 is left unchanged
$app->fileManager->resizeImage('storage/small.jpg', 800); // returns true, no change

convertToWebp()

convertToWebp(string $imagePath, ?string $outputPath = null, int $quality = 80): string|false

Convert an image to WebP format. Supports JPEG, PNG, GIF, and WebP sources. Alpha transparency in PNG and GIF is preserved in the output. The source file is never modified.

  • $imagePath(string) Path to the source image.
  • $outputPath(string|null) Output file path. Default: same directory and base name as the source, with a .webp extension.
  • $quality(int) WebP compression quality, 0–100. Default: 80. Higher = better quality, larger file.

Returns: string|false — the path to the created WebP file on success, false if the source type is unsupported.

Throws: Exception if the source file does not exist or cannot be read.

// Default output path: photo.jpg → photo.webp (same directory)
$webp = $app->fileManager->convertToWebp('storage/uploads/photo.jpg');
// 'storage/uploads/photo.webp'

// Custom output path and quality
$webp = $app->fileManager->convertToWebp(
    'storage/uploads/photo.jpg',
    'storage/webp/photo.webp',
    90
);

// Upload then immediately convert to WebP
$result = $app->fileManager->uploadFile($_FILES['image'], 'storage/uploads/');
if ($result['success']) {
    $webp = $app->fileManager->convertToWebp($result['data']['savedPath']);
}

readFile()

readFile(string $path): string

Read a file's entire contents into a string.

  • $path(string) Path to the file.

Returns: string — the file contents.

Throws: Exception if the file does not exist, is not readable, or cannot be read.

$contents = $app->fileManager->readFile('storage/config.json');
$data     = json_decode($contents, true);

// Read a template file
$html = $app->fileManager->readFile('resources/email/welcome.html');
$html = str_replace('{{name}}', $user['name'], $html);

writeFile()

writeFile(string $path, string $content, bool $append = false): bool

Write (or append) content to a file. Creates any missing parent directories automatically. Uses an exclusive lock to prevent concurrent write corruption.

  • $path(string) Destination file path.
  • $content(string) Content to write.
  • $append(bool) Append to the file instead of overwriting. Default: false.

Returns: booltrue on success.

Throws: Exception if the directory cannot be created.

// Write (overwrite)
$app->fileManager->writeFile(
    'storage/cache/settings.json',
    json_encode($settings, JSON_PRETTY_PRINT)
);

// Append a log entry
$app->fileManager->writeFile(
    'storage/logs/access.log',
    date('Y-m-d H:i:s') . " — " . $_SERVER['REMOTE_ADDR'] . "
",
    true
);

copyFile()

copyFile(string $source, string $destination): bool

Copy a file to a new location. Creates any missing parent directories at the destination automatically. The source file is left unchanged.

  • $source(string) Source file path.
  • $destination(string) Destination file path (including filename).

Returns: booltrue on success.

Throws: Exception if the source file does not exist or the destination directory cannot be created.

// Backup a file before modifying it
$app->fileManager->copyFile(
    'storage/data.json',
    'storage/backups/data_' . date('Ymd_His') . '.json'
);

// Copy an uploaded avatar to a public-facing directory
$app->fileManager->copyFile(
    $result['data']['savedPath'],
    'public/avatars/' . $userId . '.jpg'
);

moveFile()

moveFile(string $source, string $destination): bool

Move (rename) a file to a new location. Creates any missing parent directories at the destination automatically. The source file no longer exists after a successful move.

  • $source(string) Source file path.
  • $destination(string) Destination file path (including filename).

Returns: booltrue on success.

Throws: Exception if the source does not exist.

// Move a processed file to a different folder
$app->fileManager->moveFile(
    'storage/queue/report.pdf',
    'storage/processed/report.pdf'
);

// Rename a file in-place
$app->fileManager->moveFile(
    'storage/uploads/tmp_name.jpg',
    'storage/uploads/' . $userId . '_avatar.jpg'
);

deleteFile()

deleteFile(string $filePath): bool

Delete a file. Returns true if the file no longer exists after the call — including when the file was already gone before it was called (idempotent). Returns false only if the file exists but cannot be deleted.

  • $filePath(string) Path to the file to delete.

Returns: booltrue if the file is gone, false on failure.

// Delete an old avatar when a user uploads a new one
if ($user['avatar']) {
    $app->fileManager->deleteFile('storage/avatars/' . $user['avatar']);
}

// Safe to call even if the file may not exist — always returns true when gone
$app->fileManager->deleteFile('storage/tmp/export.csv');

ensureDirectoryExists()

ensureDirectoryExists(string $path): string

Ensure a directory exists and is writable. Creates the directory and any missing parent directories if needed. Safe to call concurrently — uses an atomic mkdir pattern that avoids race condition failures when two requests try to create the same directory simultaneously.

  • $path(string) Directory path to ensure.

Returns: string — the directory path (for fluent use).

Throws: Exception if the directory cannot be created or cannot be made writable.

// Ensure a per-user upload directory exists before saving
$userDir = $app->fileManager->ensureDirectoryExists(
    'storage/uploads/users/' . $userId
);

// Fluent use — create and immediately use the path
$path = $app->fileManager->ensureDirectoryExists('storage/exports')
       . '/report_' . date('Ymd') . '.csv';

listFiles()

listFiles(string $directory, bool $recursive = false, string $extension = ''): array

List files in a directory, sorted alphabetically. Optionally includes subdirectories and filters by file extension.

  • $directory(string) Directory path to scan.
  • $recursive(bool) Include files in subdirectories. Default: false.
  • $extension(string) Filter by file extension, e.g. 'jpg' or '.jpg'. Empty string = all files. Default: ''.

Returns: array — sorted array of absolute file paths. Returns an empty array if the directory does not exist.

// All files in a directory (non-recursive)
$files = $app->fileManager->listFiles('storage/uploads');

// All files recursively
$all = $app->fileManager->listFiles('storage', true);

// Only JPEG files across the whole storage tree
$jpegs = $app->fileManager->listFiles('storage', true, 'jpg');

foreach ($jpegs as $path) {
    echo basename($path) . "
";
}

cleanDirectory()

cleanDirectory(string $path): bool

Delete all files inside a directory without removing the directory itself. Subdirectories inside $path are left untouched.

  • $path(string) Directory to clean.

Returns: booltrue if every file was deleted successfully, false if any deletion failed or the directory does not exist.

// Clear a temporary export directory
$app->fileManager->cleanDirectory('storage/tmp');

// Clear a cache folder and confirm
if ($app->fileManager->cleanDirectory('storage/cache/html')) {
    $app->logger->info('HTML cache cleared.');
}

getFileInfo()

getFileInfo(string $path): array

Get detailed information about an existing file on disk. Unlike getUploadInfo(), this method works on any file — not just uploaded ones.

  • $path(string) Path to the file.

Returns: array with the following keys:

  • name(string) Filename (basename).
  • path(string) Absolute resolved path.
  • extension(string) Lowercase file extension.
  • size(int) File size in bytes.
  • humanSize(string) Human-readable size, e.g. '2.40 MB'.
  • mimeType(string) Detected MIME type.
  • modificationTime(int) Unix timestamp of last modification.
  • hash(string) SHA-256 hex digest of the file contents.

Throws: Exception if the file does not exist or is not readable.

$info = $app->fileManager->getFileInfo('storage/exports/report.pdf');

echo $info['name'];          // 'report.pdf'
echo $info['humanSize'];     // '1.85 MB'
echo $info['mimeType'];      // 'application/pdf'
echo $info['modificationTime']; // 1718457000

// Verify file integrity using stored hash
if ($info['hash'] !== $storedHash) {
    $app->logger->warning('File integrity check failed: ' . $info['name']);
}

getMimeType()

getMimeType(string $path): string

Get the actual MIME type of a file on disk by inspecting its contents, not its extension. Uses PHP's finfo extension for reliable detection.

  • $path(string) Path to the file.

Returns: string — MIME type string, e.g. 'image/jpeg', 'application/pdf'.

Throws: Exception if the file does not exist.

echo $app->fileManager->getMimeType('storage/uploads/file.jpg');
// 'image/jpeg'

echo $app->fileManager->getMimeType('storage/exports/data.csv');
// 'text/plain' (CSV is detected as plain text)

// Serve a file with the correct Content-Type
$mime = $app->fileManager->getMimeType($filePath);
header('Content-Type: ' . $mime);
readfile($filePath);

humanFileSize()

humanFileSize(int $bytes, int $decimals = 2): string

Convert a raw byte count to a human-readable size string (B, KB, MB, GB, TB, PB).

  • $bytes(int) File size in bytes.
  • $decimals(int) Number of decimal places for KB and above. Default: 2.

Returns: string — e.g. '820 B', '1.50 MB', '2.34 GB'.

echo $app->fileManager->humanFileSize(512);        // '512 B'
echo $app->fileManager->humanFileSize(1536);       // '1.50 KB'
echo $app->fileManager->humanFileSize(1048576);    // '1 MB'
echo $app->fileManager->humanFileSize(2684354560); // '2.50 GB'

// Display upload limit to users
$limit = $app->fileManager->humanFileSize(Config::$maxFileSize);
echo "Maximum upload size: $limit";

generateSafeFilename()

generateSafeFilename(string $name): string

Sanitize a filename by removing path traversal components and characters that are unsafe on common operating systems. Converts spaces to underscores, strips non-alphanumeric characters (except -, _, .), collapses consecutive dots, and strips leading dots and dashes. Returns 'file' if the result would be empty.

  • $name(string) The original filename to sanitize (may include a path or extension).

Returns: string — a safe filename usable on any OS.

echo $app->fileManager->generateSafeFilename('My Photo.jpg');
// 'My_Photo.jpg'

echo $app->fileManager->generateSafeFilename('../../etc/passwd');
// 'passwd'

echo $app->fileManager->generateSafeFilename('report (final).pdf');
// 'report_final.pdf'

echo $app->fileManager->generateSafeFilename('..hidden');
// 'hidden'

Note: uploadFile() calls this automatically — the sanitized name is stored in data['name'] and the original in data['originalName']. Call this method directly when you need to sanitize a filename outside of the upload flow.

downloadFile()

downloadFile(string $filePath, ?string $filename = null): void

Stream a file to the browser as a forced download. Sets the correct Content-Type, Content-Disposition, and Content-Length headers, flushes any previously buffered output, then streams the file with readfile() and exits. Works for any file type — images, PDFs, CSVs, ZIPs, etc.

  • $filePath(string) Absolute path to the file to serve.
  • $filename(string|null) Filename shown in the browser's save dialog. Default: basename($filePath).

Returns: void — always exits after streaming.

Throws: Exception if the file does not exist or cannot be read.

// Download a file using its original name
$app->fileManager->downloadFile('storage/exports/report.pdf');

// Download with a custom browser-facing filename
$app->fileManager->downloadFile(
    'storage/exports/rpt_' . $id . '.csv',
    'monthly_report_' . date('Y-m') . '.csv'
);

// Serve a stored upload as a download
$row = $app->db->table('documents')->find($docId);
$app->fileManager->downloadFile(
    Config::$uploadDirectory . $row['file_path'],
    $row['original_name']
);

Examples

Full avatar upload flow with WebP conversion

// pages/profile/upload-avatar.php
$result = $app->fileManager->uploadFile(
    $_FILES['avatar'],
    'storage/avatars/',
    2 * 1024 * 1024,                          // 2 MB limit
    ['image/jpeg', 'image/png', 'image/webp'],
    400                                       // resize to max 400px
);

if (!$result['success']) {
    $app->abort(422, $result['error']);
}

$savedPath = $result['data']['savedPath'];

// Convert to WebP for modern browsers
$webpPath = $app->fileManager->convertToWebp($savedPath, null, 85);

// Delete the original (keep only WebP)
if ($webpPath !== false) {
    $app->fileManager->deleteFile($savedPath);
    $savedPath = $webpPath;
}

// Delete old avatar before saving the new one
if ($user['avatar']) {
    $app->fileManager->deleteFile('storage/avatars/' . $user['avatar']);
}

$app->db->table('users')
    ->where('id', $userId)
    ->update(['avatar' => basename($savedPath)]);

Gallery upload with per-file results

$results = $app->fileManager->uploadMultipleFiles(
    $_FILES['gallery'],
    'storage/gallery/',
    10 * 1024 * 1024,
    ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
    1200
);

$saved  = [];
$errors = [];

foreach ($results as $i => $result) {
    if ($result['success']) {
        $saved[] = $result['data']['uniqueName'];
    } else {
        $errors[] = "File $i: " . $result['error'];
    }
}

$app->json(200, [
    'uploaded' => $saved,
    'errors'   => $errors,
]);

Cached JSON data file

$cachePath = 'storage/cache/products.json';
$fm        = $app->fileManager;

// Read from cache if it exists and is fresh (less than 1 hour old)
if (file_exists($cachePath) && (time() - filemtime($cachePath)) < 3600) {
    $products = json_decode($fm->readFile($cachePath), true);
} else {
    $products = $app->db->table('products')->where('active', 1)->get();
    $fm->writeFile($cachePath, json_encode($products));
}

Backup before overwrite

$source = 'storage/data/settings.json';
$backup = 'storage/backups/settings_' . date('Ymd_His') . '.json';

$app->fileManager->copyFile($source, $backup);
$app->fileManager->writeFile($source, json_encode($newSettings, JSON_PRETTY_PRINT));

$app->logger->info('Settings updated. Backup saved to ' . $backup);

Serve a secure file download

// pages/documents/download.php
$doc = $app->db->table('documents')
    ->where('id', $_GET['id'] ?? 0)
    ->where('user_id', $app->auth->getUserId())
    ->first();

if (!$doc) {
    $app->abort(404, 'Document not found.');
}

$app->fileManager->downloadFile(
    Config::$uploadDirectory . 'documents/' . $doc['file_path'],
    $doc['original_name']
);