Developers
Filesystem
Hubzero\Filesystem\Filesystem
is the file API. It holds an adapter, delegates every operation to it, and
adds a virus scan, path normalisation, and a macro mechanism. Because the
adapter is chosen at boot, the same calls work whether the hub writes to
local disk or over FTP.
Use it rather than fopen(), file_get_contents() and unlink() for two
reasons. A hub configured to write over FTP — because the web server does
not own the document root — has no working unlink(), and PHP's own
functions will fail there in ways that only appear on that hub. And the
virus scan is here, not in your controller: everything a member uploads has
to pass isSafe(), and code that writes the file itself skips it.
What the facade resolves to
public function register()
{
$this->app['filesystem'] = function($app)
{
if ($app['config']->get('ftp_enable'))
{
$adapter = new Ftp(array(
'host' => $app['config']->get('ftp_host'),
'port' => $app['config']->get('ftp_port'),
'username' => $app['config']->get('ftp_user'),
'password' => $app['config']->get('ftp_pass'),
'root' => $app['config']->get('ftp_root'),
));
}
else
{
$adapter = new Local($app['config']->get('virus_scanner', "clamscan -i --no-summary --block-encrypted"));
}
$filesystem = new Filesystem($adapter);
$filesystem->addMacro(new EmptyDirectory)
->addMacro(new Directories)
->addMacro(new Files)
->addMacro(new DirectoryTree);
return $filesystem;
};
}
With ftp_enable set in the global configuration the adapter is
Ftp,
configured from the ftp_* values; otherwise it is
Local,
constructed with the shell command used for virus scanning. A third
adapter, None, exists for tests. Four macros are registered on the way
out; see below.
Reading and writing
use Filesystem;
if (!Filesystem::exists($path))
{
throw new Exception(Lang::txt('File not found.'));
}
$contents = Filesystem::read($path);
| Method | What it does |
|---|---|
exists($path) |
Whether the path exists |
read($path) |
File contents as a string; throws FileNotFoundException if absent |
write($path, $contents) |
Write, replacing what is there |
prepend($path, $data) / append($path, $data) |
Add to the start or end |
delete($path) |
Remove a file |
copy($path, $target) / rename($path, $target) |
Copy or rename; move() is an alias for rename() |
upload($path, $target) |
Move an uploaded temporary file into place |
copy() and rename() call assertPresent() on the source first, so a
missing source raises
FileNotFoundException
rather than returning false.
delete() asserts the same way, which is the one that surprises people:
deleting a file that is already gone throws rather than doing nothing.
Cleaning up after a failed upload, or removing an attachment a previous
run already removed, is a stack trace on the member's screen unless you
test exists() first.
| Missing source | What happens |
|---|---|
read(), delete(), copy(), rename(), move() |
FileNotFoundException |
write(), exists(), isFile() |
false |
Check exists() first, as the example above does, and neither bites.
Inspecting
| Method | Returns |
|---|---|
name($path) |
The filename without its extension |
extension($path) |
The extension, without the dot |
type($path) |
file or dir |
size($path) |
Size in bytes |
mimetype($path) |
The detected MIME type |
lastModified($path) |
Modification time |
isFile($path) / isDirectory($path) / isWritable($path) |
Booleans |
isSafe($path) |
Runs the configured virus scanner over the file |
find($paths, $file) |
Full path to $file in the first of $paths that has it, or false |
isSafe() is the one to remember. Everything a member uploads goes through
it, and a failure means the file is deleted rather than kept:
if (!Filesystem::isSafe($path . DS . $file['name']))
{
Filesystem::delete($path . DS . $file['name']);
throw new Exception(Lang::txt('COM_BOOKINGS_FILE_FAILED_SCAN'));
}
Scan after the file is in place and delete it if the scan fails, in that
order. isSafe() needs a path on disk, so there is no way to check before
writing.
Directories
| Method | What it does |
|---|---|
makeDirectory($path, $mode = 0755, $recursive = true, $force = false) |
Create a directory |
deleteDirectory($path, $preserve = false) |
Remove it, optionally keeping the directory itself |
copyDirectory($path, $target, $options = null) |
Copy a tree |
setPermissions($path, $filemode = '0644', $foldermode = '0755') |
Chmod a tree |
listContents($path, $filter = '.', $recursive = false, $full = false, $exclude = [...]) |
Entries as arrays with type and path |
Cleaning names
Never build a path out of an uploaded filename without normalising it
first. The name arrives from the browser and can be anything —
../../config/app.php, a name with a null byte, a name that is 300
characters of Unicode:
| Method | What it does |
|---|---|
clean($file) |
Strip a filename down to safe characters |
cleanPath($path) |
Normalise separators and collapse .. |
cleanDirectory($directory) |
The same, for a directory name |
Macros
A macro adds one method to the filesystem object. It implements
MacroInterface
— usually by extending Hubzero\Filesystem\Macro\Base, which supplies
setFilesystem() — and provides getMethod() and handle():
class Files extends Base
{
/**
* Get the method name.
*
* @return string
*/
public function getMethod()
{
return 'files';
}
/**
* List all files in the directory.
*
* @param string $path The path of the folder to read.
* @param string $filter A filter for file names.
* @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth.
* @param boolean $full True to return the full path to the file.
* @param array $exclude Array with names of files which should not be shown in the result.
* @return array
*/
public function handle($path, $filter = '.', $recursive = false, $full = false, $exclude = array('.svn', '.git', 'CVS', '.DS_Store', '__MACOSX'))
{
$result = array();
$contents = $this->filesystem->listContents($path, $filter, $recursive, $full, $exclude);
foreach ($contents as $object)
{
if ($object['type'] === 'file')
{
$result[] = $object['path'];
}
}
return $result;
}
getMethod() names the call; handle() receives whatever arguments the
call was given, with $this->filesystem already set. Register it and call
it:
$filesystem = App::get('filesystem');
$filesystem->addMacro(new Example);
$content = $filesystem->examplify($path);
addMacro() refuses a macro without a handle() method, hasMacro($name)
tests for one, and calling a name that is neither a real method nor a
registered macro raises BadMethodCallException.
Four macros are registered at boot and are therefore always available on
the Filesystem facade:
| Call | Returns |
|---|---|
files($path, $filter, $recursive, $full, $exclude) |
Paths of the files in a directory |
directories($path, $filter, $recursive, $full, $exclude) |
Paths of the sub-directories |
emptyDirectory($path) |
Deletes everything inside a directory, keeping the directory |
directoryTree($path, $filter, $maxLevel, $level, $parent) |
A nested array describing the tree |
$num_files = count(Filesystem::files(PATH_APP . DS . $folder));
Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.