FTP через сокеты - вариант реализации

Alexiy II

Guest
FTP через сокеты - вариант реализации

Возникла задача написания скрипта, качающего файлы по FTP. FTP Functions на хостинге отключены. Поэтому, остаются только сокеты.

Через поиск нашёл несколько тем, но ясного представления о проблеме так и не сложилось. Пришлось разбираться самому, используя примеры из этих топиков и rfc959.

Получилось творение на 300 строк - поддерживается докачка и ведутся достаточно подробные логи.

С разрешения модераторов готов поделиться, ответить на вопросы, задать свои :) а также выслушать замечания и предложения.
 

Alexandre

PHPПенсионер
[offtop] автор молодец, а провайдер лох, раз 21 исходящий открыт[offtop]
 

ys

отодвинутый новичок
Исходящее все должно быть закрыто.
Да и входящее тоже.

Открыто только то, что реально нужно.
 

Vital_N

Новичок
22 - это sftp - это конечно хорошо, но есть трудности когда ходишь в инет через проксю, т.е. обычно наружу это дело закрывают, потому что ssh прокси не каждому суждено поставить и нарулить, а пускать напрямую минуя прокси мало кто желает -
ys
а 21 (ftp) - по твоему не нужен вообще?
 

ys

отодвинутый новичок
Vital_N

> а 21 (ftp) - по твоему не нужен вообще?

Мне - нет.
хотя я ftp использую, но over ssh :)
 

Alexiy II

Guest
Ладно, вот код. Только не ругайте сильно :)

PHP:
function ftp_get_file($url)
{
  function ftp_send($f, $query)
  {
    fputs($f, $query);   //Отправляем запрос
    $ans = fgets($f);   //Читаем ответ сервера
    return $ans;
  }

  function ftp_send_pasv($f, $host, &$answers)
  {
    $pasv = ftp_send($f, "PASV\r\n");
    $answers .= $pasv;

    /* Определяем номер порта, на котором нас ждёт сервер после команды PASV */
    $res = array();
    if (preg_match('/^227.*\((\d{1,3}\,){4}(\d{1,3})\,(\d{1,3})\).*/', $pasv, $res) != 0)
      $port_num = $res[2] * 256 + $res[3];
    else
      return FALSE;
    /*************************************************************************/

    $errno2 = 0;
    $errstr2 = '';
    if ($f2 = @fsockopen($host, $port_num, $errno2, $errstr2, 15))   //Открываем сокет для передачи данных
      return $f2;
    else
      return FALSE;
  }

  function write_logs($logs)
  {
    $logs_fname = 'logs.htm';

    $fl = fopen($logs_fname, 'a');
    clearstatcache();
    if (filesize($logs_fname) == 0) $logs = str_replace("\r\n<br><b>", '<b>', $logs);
    fputs($fl, $logs);
    fclose($fl);
  }

  function logs_abort()
  {
    return date('d.m.Y H:i:s').' - <font color="#ff0000"><b>download aborted</b></font><br>'."\r\n";
  }

  $MAX_TRIES = 5;   //Максимальное число попыток

  $logs = "\r\n<br><b>{URL}</b><br>\r\n";
  $logs_end = "--------------------------------------------------------------------------<br>\r\n";

  $logs = str_replace('{URL}', $url, $logs);

  $url_ar = @parse_url($url);
  if (empty($url_ar['port']))
    if ($url_ar['scheme'] == 'ftp')   //Только ftp
      $url_ar['port'] = 21;
    else
    {
      $logs .= date('d.m.Y H:i:s').' - bad URL: only FTP supported'."<br>\r\n".logs_abort().$logs_end;
      write_logs($logs);
      return FALSE;
    }

  if ($url_ar['path'] == '')
  {
    $logs .= date('d.m.Y H:i:s').' - bad URL: no file name'."<br>\r\n".logs_abort().$logs_end;
    write_logs($logs);
    return FALSE;
  }

  $fname = basename($url);
  $size = 0;    //Размер скачанного файла
  $old_size = 0;
  $cont = '';   //Содержание скачанного файла
  $remote_size = 0;   //Размер удаленного файла
  $tries = 0;   //Число попыток
  $time_b = time();   //Начало закачки
  while ($tries <= $MAX_TRIES)
  {
    $tries++;
    if ($tries > $MAX_TRIES)  //Превысили максимальное число попыток
    {
      $logs .= date('d.m.Y H:i:s').' - max ('.$MAX_TRIES.') tries reached<br>'."\r\n".logs_abort();
      break;
    }

    $errno = 0;
    $errstr = '';
    $f = @fsockopen($url_ar['host'], $url_ar['port'], $errno, $errstr, 15);  //Открываем сокет для передачи команд
    if (!$f)
    {
      $logs .= date('d.m.Y H:i:s').' - could not connect to '.$url_ar['host']."<br>\r\n".logs_abort();
      break;
    }
    $logs .= date('d.m.Y H:i:s').' - connected to '.$url_ar['host']."<br>\r\n";

    $answers = '';   //Для отладки

    $user_send = empty($url_ar['user'])?'anonymous':$url_ar['user'];
    $pass_send = empty($url_ar['pass'])?'guest':$url_ar['pass'];

    $answers .= fgets($f);   //220 Welcome to *** FTP service.
    $answers .= ftp_send($f, "USER ".$user_send."\r\n");
    $pass = ftp_send($f, "PASS ".$pass_send."\r\n");
    $answers .= $pass;

    $pass = substr($pass, 0, 4);   //Извлекаем код сообщения
    if ($pass == '230-')   //Расширенный ответ сервера
    {
      do
      {
        $tmp = fgets($f);
        $answers .= $tmp;
        $tmp = substr($tmp, 0, 4);
      }
      while ($tmp != '230 ');
    }
    elseif ($pass == '530 ')   //530 Incorrect login или Maximum number of allowed clients are already connected.
    {
      $answers .= ftp_send($f, "QUIT\r\n");
      fclose($f);
      $logs .= date('d.m.Y H:i:s').' - incorrect login or max number of connected clients reached'."<br>\r\n";
      write_logs($logs);
      $logs = '';
      sleep(10);
      continue;
    }
    $rest = ftp_send($f, "REST 100\r\n");   //Проверка возможности докачки
    $answers .= $rest;
    $rest = substr($rest, 0, 3);   //Извлекаем код сообщения
    if ($rest != '350')
    {
      $answers .= ftp_send($f, "QUIT\r\n");
      fclose($f);
      $logs .= date('d.m.Y H:i:s').' - server doesn\'t support partial download'."<br>\r\n".logs_abort();
      break;
    }
    $answers .= ftp_send($f, "REST 0\r\n");   //Устанавливаем маркер обратно в 0
    $answers .= ftp_send($f, "TYPE A\r\n");   //200 Switching to ASCII mode.

    $logs .= date('d.m.Y H:i:s').' - getting '.$url_ar['path']."<br>\r\n";

    if (empty($remote_size))
    {
      $f2 = ftp_send_pasv($f, $url_ar['host'], $answers);   //Открываем канал для передачи данных - для определения размера файла
      if (!$f2)
      {
        $answers .= ftp_send($f, "QUIT\r\n");
        fclose($f);
        $logs .= date('d.m.Y H:i:s').' - could not establish data stream connection'."<br>\r\n".logs_abort();
        break;
      }

      $answers .= ftp_send($f, "LIST ".$url_ar['path']."\r\n");   //150 Here comes the directory listing.
      $file_info = fgets($f2);  //Информация о файле
      fclose($f2);
      $answers .= $file_info;
      $answers .= fgets($f);   //226 Directory send OK.

      /* Определяем размер файла */
      $res = array();
      preg_match('/^[rwx\-]{10}[ ]+\w+ (\w+[ ]+){2}([\d]+).*/', $file_info, $res);
      $remote_size = $res[2];

      if (empty($remote_size))   //Если не получилось через LIST, пробуем через SIZE
      {
        $size_req = ftp_send($f, "SIZE ".$url_ar['path']."\r\n");
        $answers .= $size_req;
        $res = array();
        preg_match('/^213 (\d+).*/', $size_req, $res);
        $remote_size = $res[1];
      }
      /***************************/

      if (empty($remote_size))
      {
        $answers .= ftp_send($f, "QUIT\r\n");
        fclose($f);
        $logs .= date('d.m.Y H:i:s').' - file size: unknown'."<br>\r\n".logs_abort();
        break;
      }
    }

    if (empty($size))
      if (file_exists($fname))       //Если файл уже существует, то докачиваем его
      {
        $size = filesize($fname);    //Размер уже скачанной части
        if ($size >= $remote_size)   //Если файл уже скачан
        {
          $logs .= date('d.m.Y H:i:s').' - already downloaded'."<br>\r\n".logs_abort().$logs_end;
          write_logs($logs);
          return FALSE;
        }

        $lf = fopen($fname, 'r');
        $cont = fread($lf, $size);   //Содержание уже скачанной части
        fclose($lf);

        $old_size = $size;
      }

    $logs .= date('d.m.Y H:i:s').' - file size: '.sprintf('%.0f', $remote_size / 1024).' KB<br>'."\r\n".
             date('d.m.Y H:i:s').' - download started'."<br>\r\n";
    write_logs($logs);
    $logs = '';

    $answers .= ftp_send($f, "TYPE I\r\n");   //200 Switching to Binary mode.

    $f2 = ftp_send_pasv($f, $url_ar['host'], $answers);
    if (!$f2)
    {
      $answers .= ftp_send($f, "QUIT\r\n");
      fclose($f);
      $logs .= date('d.m.Y H:i:s').' - could not establish data stream connection'."<br>\r\n".logs_abort();
      break;
    }

    $answers .= ftp_send($f, "REST ".$size."\r\n");   //Устанавливаем маркер на позицию, равную размеру уже скачанного файла
    $retr = ftp_send($f, "RETR ".$url_ar['path']."\r\n");
    $answers .= $retr;
    $retr = substr($retr, 0, 3);   //Извлекаем код сообщения
    if ($retr != '150')
    {
      $answers .= ftp_send($f, "ABOR\r\n");
      $answers .= ftp_send($f, "QUIT\r\n");
      fclose($f2); fclose($f);
      $logs .= date('d.m.Y H:i:s').' - could not retreive part of file'."<br>\r\n".logs_abort();
      break;
    }

    while (!feof($f2))   //Читаем содержимое файла
      $cont .= fgets($f2, 2048);
    fclose($f2);

    $size = strlen($cont);
    if ($size >= $remote_size)   //Файл успешно скачан
    {
      $answers .= fgets($f);   //226 File send OK.
      $answers .= ftp_send($f, "QUIT\r\n");
      fclose($f);
      break;
    }

    fclose($f);
    $logs .= date('d.m.Y H:i:s').' - server disconnected'."<br>\r\n";
  }
  $time_e = time();  //Конец закачки

  if ($size <= $old_size)   //Если ничего скачать не удалось
  {
    $logs .= $logs_end;
    write_logs($logs);
    return FALSE;
  }

  $dtime = $time_e - $time_b;
  $dtime = ($dtime == 0)?1:$dtime;   //Чтобы не делить на ноль

  if ($fout = @fopen($fname, 'w'))
  {
    $speed = sprintf('%.2f', ($size - $old_size) / 1024 / $dtime);   //Приблизительная скорость закачки
    fwrite($fout, $cont);
    fclose($fout);

    if ($size >= $remote_size)
      $logs .= date('d.m.Y H:i:s').' - <font color="#0000ff"><b>downloaded at '.$speed.' KB/s in '.$tries.' tries</b></font><br>'."\r\n";
    else
    {
      $degr = sprintf('%.0f', $size / $remote_size * 100).'%';
      $d_size = sprintf('%.0f', $size / 1024).' KB';
      $logs .= date('d.m.Y H:i:s').' - <font color="#ff0000"><b>downloaded '.$degr.' ('.$d_size.') at '.$speed.' KB/s</b></font><br>'."\r\n";
    }
  }
  else   //Не удалось записать в выходной файл
    $logs .= date('d.m.Y H:i:s').' - <font color="#ff0000"><b>could not write to output file</b></font><br>'."\r\n";

  echo '<pre>', $answers, '</pre>';

  $logs .= $logs_end;
  write_logs($logs);
  return TRUE;
}
-~{}~ 02.02.05 08:07:

Пример использования:

PHP:
set_time_limit(3600);
ftp_get_file('ftp://...');
 
Сверху