开发者

PHP Copy Directory Function

I'm working on a Minecraft Server Dashboard, and one of it's functions is to backup and restore your world (a directory). I already have a function (see below), but, as you can probably see, it's pretty bad code. Anyone know of a better, cleaner function?

function backupOrRestoreWorld($source,$target){
    foreach(glob($target.'*.*')as$v){
        unlink($v);
    }
    if(is_dir($source)){
        @mkdir($target);
        $d=dir($source);
        while(FALSE!==($entry=$d->read())){
            if($entry=='.'||$entry=='..'){
                continue;
            }
            $Entry=$source.'/'.$entry;
  开发者_Python百科          if(is_dir($Entry)){
                backupOrRestoreWorld($Entry,$target.'/'.$entry);
                continue;
            }
            copy($Entry,$target.'/'.$entry);
        }
        $d->close();
    }
    else{
        copy($source,$target);
    }
    if($source == "server/world"){
        return "World backed up.";
    }
    else {
        return "World restored from backup.";
    }
}


I wouldn't do this in PHP. Just use system("cp -a $source $dest"). (And make sure the user can not in any way control the contents of $source and $dest or you will be hacked.)


I would create more functions out of it, each one doing a distinctive job, probably encapsulated into a class, like

  • empty_directory
  • copy_directory

You can still maintain your single function then making use of the subroutines / objects to provide a façade into your application, for example dealing with exceptions for error handling and such.

Next to that you're having not really bad code. It's a recursive function for copying the data which might stress the file-system a bit - which presumably can be neglected. If you move the decent functionality into units of it's own, you can change that over time if you run into actual problems.

But the first benefit would be to make use of exceptions in subroutines I think:

function backupOrRestoreWorld($source, $target)
{

    empty_directory($target);

    copy_directory($source, $target);

    if($source == "server/world"){
        return "World backed up.";
    }
    else {
        return "World restored from backup.";
    }
}

function empty_directory($path)
{
    $path = rtrim($path, '/');

    if (!is_dir($path))
    {
        throw new InvalidArgumentException(sprintf('Not a directory ("%s").', $path));
    }

    if (!is_writable($path))
    {
        throw new InvalidArgumentException(sprintf('Directory ("%s") is not a writeable.', $path));
    }

    $paths = glob($path.'/*.*');

    if (false === $paths)
    {
        throw new Exception(sprintf('Unable to get path list on path "%s" (glob failed).', $path));
    }

    foreach ($paths as $v)
    {
        unlink($v);
    }
}

function copy_directory($source, $target)
{

    $source = rtrim($source, '/');
    $target = rtrim($target, '/');

    if (!is_dir($source))
    {
        throw new InvalidArgumentException(sprintf('Source ("%s") is not a valid directory.', $source));
    }

    if (!is_readable($source))
    {
        throw new InvalidArgumentException(sprintf('Source ("%s") is not readable.', $source));
    }

    if (!is_dir($target))
        $r = mkdir($target);

    if (!is_dir($target))
    {
        throw new InvalidArgumentException(sprintf('Target ("%s") is not a valid directory.', $target));
    }

    if (!is_writable($target))
    {
        throw new InvalidArgumentException(sprintf('Target ("%s") is not a writeable.', $target));
    }

    $dirs = array('');

    while(count($dirs))
    {
        $dir = array_shift($dirs)
        $base = $source.'/'.$dir;
        $d = dir($base);
        if (!$d)
        {
            throw new Exception(sprintf('Unable to open directory "%s".', $base));
        }

        while(false !== ($entry = $d->read()))
        {
            // skip self and parent directories
            if (in_array($entry, array('.', '..'))
            {
                continue;
            }

            // put subdirectories on stack
            if (is_dir($base.'/'.$entry))
            {
                $dirs[] = $dir.'/'.$entry;
                continue;
            }

            // copy file
            $from = $base.'/'.$entry;
            $to = $target.'/'.$dir.'/'.$entry;
            $result = copy($from, $to);
            if (!$result)
            {
                throw new Exception(sprintf('Failed to copy file (from "%s" to "%s").', $from, $to);
            }
        }
        $d->close();
    }
}

This example is basically introducing two functions, one to empty a directory and another one to copy the contents of one directory to another. Both functions do throw exceptions now with more or less useful descriptions what happens. I tried to reveal errors early, that's by checking input parameters and by performing some additional tests.

The empty_directory function might be a bit short. I don't know for example, if there a subdirectories and those aren't empty, if unlink would work. I leave this for an exercise for you.

The copy_directory function is working non-recursive. This is done by providing a stack of directories to process. In case there is a subdirectory, the directory is put on the stack and processed after the all files of the current directory are copied. This helps to prevent switching directories too often and is normally faster. But as you can see, it's very similar to your code.

So these functions concentrate on the file-system work. As it's clear what they do and for what they are, you can concentrate inside your function on the main work, like the logic to determine in which direction the copying has been done. As the helper functions now throw exceptions, you can catch those, too. Additionally you could verify that $source and $target actually contain values that you explicitly allow. For example, you don't want to have .. or / inside of them probably, but just characters from a-z.

This will help you as well to find other causes of error, like overwriting attempts etc.:

function backupOrRestoreWorld($source, $target)
{
    $basePath = 'path/to/server';
    $world = 'world';
    $pattern = '[a-z]+';

    try
    {
        if (!preg_match("/^{$pattern}\$/", $source))
        {
            throw new InvalidArgumentException('Invalid source path.');
        }

        if (!preg_match("/^{$pattern}\$/", $target))
        {
            throw new InvalidArgumentException('Invalid target path.');
        }

        if ($source === $target)
        {
            throw new InvalidArgumentException('Can not backup or restore to itself.');
        }

        $targetPath = $basePath.'/'.$target;

        if (is_dir($targetPath))        
            empty_directory($targetPath);

        copy_directory($basePath.'/'.$source, $targetPath);

        if($source === $world)
        {
            return "World backed up.";
        }
        else 
        {
            return "World restored from backup.";
        }
   }
   catch(Exception $e)
   {
        return 'World not backed up. Error: '.$e->getMessage(); 
   }
}

In the example backupOrRestoreWorld still acts as the original function but now returns an error message and more specifically checks for error conditions in it's own logic. It's a bit borked because it converts exceptions to the return value, which are two sorts of error handling (which you might not want), but it's a compromise to interface with your existing code (the façade) while covering itself's input validation with exceptions.

Additionally, the values it works on (parameters) are specified at the top of the function instead of later on in the function's code.

Hope this helps. What's left?

  • The copy_directory function could check if a directory already exists, that it is empty. Otherwise it would not truly copy a world, but mix two worlds.
  • The empty_directory function should be properly checked if it actually does the job to empty a directory in a fail-safe manner, especially while dealing with subdirectories.
  • You can make up your mind about a general way of doing error handling inside your application so that you can more easily deal with errors inside your application.
  • You can think about creating objects to deal with storing and retrieving worlds so things can be extended easily in the long run.


.1. Using @ operator will lead you to trouble. NEVER use it. If there is a possibility of unexisting file - CHECK IT first!

if (!file_exists($target)) mkdir($target);

.2. I am quite surprised why you're using glob in the first part and don't use it in the second.

.3. Your "clean target directory code first" code won't clean subdirectories.


I used, from here: http://codestips.com/php-copy-directory-from-source-to-destination/

<?
function copy_directory( $source, $destination ) {
    if ( is_dir( $source ) ) {
        mkdir( $destination );
        $directory = dir( $source );
        while ( FALSE !== ( $readdirectory = $directory->read() ) ) {
            if ( $readdirectory == '.' || $readdirectory == '..' ) {
                continue;
            }
            $PathDir = $source . '/' . $readdirectory; 
            if ( is_dir( $PathDir ) ) {
                copy_directory( $PathDir, $destination . '/' . $readdirectory );
                continue;
            }
            copy( $PathDir, $destination . '/' . $readdirectory );
        }

        $directory->close();
    }else {
        copy( $source, $destination );
    }
}
?>

Works so long as you don't try to copy a directory inside itself and go into an infinite loop..


I have some recursive copying code that follows, but first a few thoughts.

Conversely to what some others think - I believe the file system functions are about the only place where the @ operator makes sense. It allows you to raise your own exceptions rather than having to handle the functions built in warnings. With all file system calls you should check for failure conditions.

I would not do something that was suggested to you:

if (!file_exists($target)) mkdir($target);

(which assumes that mkdir will succeed). Always check for failure (the file_exists check does not match the complete set of possibilities where mkdir would fail). (e.g broken sym links, inaccessible safe mode files).

You must always decide how you will handle exceptions, and what are the initial conditions required for your function to succeed. I treat any operation that fails with the file system as an exception which should be caught and dealt with.

Here is the recursive copying code that I use:

   /** Copy file(s) recursively from source to destination.
    *  \param from \string The source of the file(s) to copy.
    *  \param to \string The desitination to copy the file(s) into.
    *  \param mode \int Octal integer to specify the permissions.
    */
   public function copy($from, $to, $mode=0777)
   {  
      if (is_dir($from))
      {
         // Recursively copy the directory.
         $rDir = new RecursiveDirectoryIterator(
            $from, FilesystemIterator::SKIP_DOTS);
         $rIt = new RecursiveIteratorIterator(
            $rDir, RecursiveIteratorIterator::SELF_FIRST);

         // Make the directory - recursively creating the path required.
         if (!@mkdir($to, $mode, true))
         {
            throw new Exception(
               __METHOD__ .
               'Unable to make destination directory: ' . var_export($to, true));
         }

         foreach ($rIt as $file)
         {
            $src = $file->getPathname();
            $dest = $to . $rIt->getInnerIterator()->getSubPathname();

            if (is_dir($src))
            {
               if (!@mkdir($dest, $mode))
               {
                  throw new Exception(
                     __METHOD__ .
                     'From: ' . $from . ' To: ' . $to .
                     ' Copying subdirectory from:' . $src . ' to: ' . $dest);
               }
            }
            else
               if (!@copy($src, $dest))
               {
                  throw new Exception(
                     __METHOD__ .
                     'From: ' . $from . ' To: ' . $to .
                     ' Copying file from: ' . $src . ' to: ' . $dest);
               }
            }
         }
      }
      else
      {
         if (!@copy($from, $to))
         {
            throw new Exception(
               __METHOD__ .
               'Copying single file from: ' . $from . ' to: ' . $to);
         }
      }
   }


Before copy all files, you must to make "/destination" permission to be 0777

        $dst = '/destination';  // path for past
        $src = '/source';       // path for copy

        $files = glob($src.'/*.*');
        foreach($files as $file){
            $file_to_go = str_replace($src, $dst, $file);
            copy($file, $file_to_go);
        }
        $dir_array = array();
        $dir_array[] = $src;
        while ($dir_array != null) {
            $newDir = array ();
            foreach ($dir_array as $d) {

                $results = scandir($d);
                foreach ($results as $r) {
                    if ($r == '.' or $r == '..') continue;
                    if (is_file($d . '/' . $r)) {

                    } else {
                        $path = $d.'/'.$r;
                        $newDir[] = $path;
                        $new = str_replace($src, $dst, $path);
                        mkdir($new, 0777, true);
                        $files = glob($path.'/*.*');
                        foreach($files as $file) {
                            $file_to_go = str_replace($src, $dst, $file);
                            copy($file, $file_to_go);
                        }
                        continue;
                    }
                }
            }
            $dir_array = $newDir;
        }

all done. Thanks.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜