메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

FTP 서버용 RSS 피드

한빛미디어

|

2006-04-06

|

by HANBIT

13,386

제공: 한빛미디어 네트워크 기사

원문: http://www.xml.com/pub/a/2006/03/22/rss-feeds-for-ftp-servers.html
저자: Mark Woodman, 한동훈 역

RSS를 위한 응용프로그램들은 뉴스 항목을 배포하는 방법을 확장시켰다. RSS는 상품 배송을 추적하는 것에서 부터 자동차 판매상의 재고에 이르기까지 거의 모든 것에 사용되고 있다. 이들은 RSS의 가장 중요한 속성을 반영하고 있다. 즉, 내가 관심을 갖고 있는 것에 어떤 일이 발생했을 때, 그것을 확인하기 위해 직접 확인하러 가는 것이 아니라 그것을 나한테 알려줄 수 있게 해줄 수 있다는 속성 말이다. 이런 인식을 바탕으로, 여기서는 FTP 서버에 어떤 파일이 새로 추가되고, 변경되는지 추적하는 PHP 스크립트를 작성하는 방법을 설명할 것이다.

PHP, FTP, 그리고 그대(Thee)

PHP의 웹과 관련된 기능들을 강조하면서도 PHP의 FTP 관련 명령들은 간과되어왔다. 좋은 소식은 표준 PHP4에서 FTP 관련 명령들이 포함되었다는 것이며, 더 이상 외부 라이브러리를 사용하지 않아도 된다는 점이다.

그러나, 설치된 PHP에 FTP 함수들을 사용할 수 있게 되어있는지 확인하는 것이 중요하다. FTP 함수들을 사용할 수 있는지 확인하려면 다음과 같이 간단한 파일에 phpinfo()를 사용하면 된다.

   phpinfo();  
?>

웹 브라우저에서 위 스크립트를 보면, 필요한 거의 모든 설정 정보를 제공하는 PHP 인포 페이지를 볼 수 있을 것이다. 항목들을 차례대로 찾아보면 FTP 섹션에서 "FTP support"가 "enabled"로 된 것을 볼 수 있다. 화면은 다음과 같을 것이다.

그림1
그림1. PHP 인포에서 FTP 활성화를 보여주는 부분

(FTP 함수를 사용할 수 없으면, FTP 함수를 활성화하거나 다른 서버에서 여기서 소개할 스크립트를 실행할 수 있게  해야 한다.
알아야 할 것은 매우 많지만, PHP 매뉴얼의 FTP 함수를 유용하게 참고할 수 있다. 이 설명서를 진행하는 동안 가까이에 두고 참고하는 것도 좋다.(만약, 불면증이 있다면 RFC 959의 FTP 스펙을 읽어보는 것도 좋다)

코드를 알라(Know the Code)

먼저 PHP 스크립트의 파일 이름은 ftp_monitor.php로 만들자. 스크립트는 하나하나씩 단계별로 볼 것이며, 참고를 위해 전체 소스 코드를 다운로드 받을 수 있다.

exploreFtpServer() 함수

먼저, exploreFtpServer() 함수 안에 캡슐화되어 있는 FTP 함수들에 대해서 살펴보자. 이 함수는 FTP 호스트이름, 사용자이름, 비밀번호, 초기 경로를 인자로 사용하며, FTP 서버를 탐험하고, 디렉터리를 재귀로 탐색하고, 파일 이름과 해당 파일의 시간을 연관 배열로 반환한다.

함수 서명(signature)을 선언했으면, PHP의 ftp_connect()를 사용해서 FTP 서버에 연결을 시도한다. 연결이 생성되면 PHP의 FTP 관련 함수들에서 지속적으로 사용되는 연결 ID를 변수 $cid에 저장한다.

function exploreFtpServer($host, $user, $pass, $path)
{
   // Connect
   $cid = ftp_connect($host) or die("Couldn"t connect to server");

코드를 단순하게 하기 위해, 여기서는 FTP 연결이 실패하면 바로 스크립트 실행을 중지한다. 스크립트를 사용해보면, 보다 견고한 오류 처리를 추가하고 싶을 것이다.

연결이 되면, PHP 함수 ftp_login()을 사용해서 서버에 인증을 시도해야 한다. PHP에서 대부분의 FTP 함수들은 첫번째 인자로 연결ID($cid)를 사용한다. ftp_login()에서는 추가로 사용자 이름과 비밀번호를 인자로 사용한다.

로그인이 성공하면 패시브 모드(Passive Mode)로 사용할 것을 FTP 서버에 알려주기 위해 ftp_pasv()를 사용한다. 패시브 모드를 사용하면 이 스크립트를 방화벽이 있는 곳에서도 실행할 수 있다.

역주: FTP는 액티브 모드(Active Mode)와 패시브 모드(Passive Mode)로 나뉜다. FTP의 기본 설정은 액티브 모드이며, 이 모드는 21번 포트로 명령을 주고 받고, 데이터는 20번 포트로 주고 받는다. 방화벽이나 공유기를 사용하는 환경에서 액티브 모드를 사용하면 FTP 서버에는 연결이 되지만 데이터를 받아오지 못해 파일 목록이나 다운로드를 할 수 없게 된다.

이와 달리, 패시브 모드(Passive Mode)는 21번 포트로 명령을 주고받는 것은 같고, 데이터를 주고 받는 포트를 1024번 이상의 포트를 할당해서 사용하는 것을 의미한다. 이 모드를 사용하면 방화벽, 공유기를 사용하는 환경에서도 정상적으로 FTP를 이용할 수 있기 때문에 대부분의 FTP 응용프로그램들은 패시브 모드를 기본 설정으로 사용하고 있다.

연결이 모두 설정되었으면 지정된 경로에서 시작하는 FTP 서버 디렉터리를 재귀적으로 탐색할 수 있다. 탐색 작업에는 뒤에서 작성할 scanDirectory() 함수를 사용할 것이다.

마지막으로, 인증의 동작 여부에 따라서, ftp_close() 함수로 서버에 대한 연결을 닫아야 한다. 이 예에서는 인증이 실패하면 die()를 사용해서 스크립트를 중단시키지만, 다른 방법으로 인증 실패에 대한 처리 방법을 선택할 수도 있다.

   // 로그인
   if (ftp_login($cid, $user, $pass))
   {
      // 패시브 모드
      ftp_pasv($cid, true);

      // 디렉터리 구조를 재귀적으로 탐색
      $fileList = scanDirectory($cid, $path);

      // 연결 끊기
      ftp_close($cid);
   }
   else
   {
      // 연결 끊기
      ftp_close($cid);

      die("Couldn"t authenticate.");
   }

이제, $fileList 변수 안에 데이터가 연관 배열 형태로 들어있다. 이 배열에서 키는 파일 이름이며, 값은 각 파일의 시간(timestamp)이다. 이 배열은 시간에 따라 정렬되는 것이 가장 유용하기 때문에 정렬 결과를 배열로 반환하는 arsort()를 사용한다.

   // 새로운 것이 먼저 오도록 시간으로 정렬
   arsort($fileList);

   // Return the result
   return $fileList;
}

scanDirectory() 함수

exploreFtpServer()에서 호출하는 scanDirectory() 함수를 작성해보자. 이 함수는 파일과 하위 디렉터리 목록을 얻고, 파일은 리스트에 추가하고, 하위 디렉터리 목록은 재귀적으로 탐색한다. 전달할 매개변수는 FTP 연결 ID($cid)와 시작 디렉터리 위치($dir)이다. 함수를 재귀적으로 호출하는 동안 파일 목록을 유지하기 위해 정적 배열 $fileList를 선언한다.

FTP에서 주어진 디렉터리의 내용을 가져오기 위해 ftp_nlist() 함수를 사용한다. 불행히도, 이 함수는 완벽하게 동작하지 않는다. 테스트해 본 대부분의 FTP 서버에서 파일과 디렉터리 이름 목록을 반환하지만 WU-FTPD와 같은 일부에서는 파일 이름의 목록만 반환한다. 이런 서버에서는 오직 초기 디렉터리만 모니터링할 수 있으며, 하위 디렉터리는 모니터링할 수 없다.

ftp_nlist() 대신, 타입에 관계없이 모든 디렉터리의 내용을 반환하는 ftp_rawlist()를 사용하는 것이다. 불행히도, ftp_rawlist()가 반환하는 데이터의 형식은 표준화되어 있지 않은 것처럼 보이며, 모든 것을 해석할 수 있는 "만국통용 파서(universal parser)"를 작성하는 것은 지리한 작업이다.(무슨 의미인지 알고 싶다면 ftp_rawlist()에 사용자들이 남긴 코멘트를 보기 바란다) 따라서, 여기서는 설명서를 위해 불완전하지만 단순한 ftp_nlist()를 사용할 것이다.

function scanDirectory($cid, $dir)
{
   // 결과를 모으기 위한 정적 변수
   static $fileList=array();

   // 디렉터리 내용을 목록으로 가져오기
   $contents = ftp_nlist($cid, $dir);

$contents 변수는 파일과 디렉터리 이름으로 된 간단한 배열이 된다. 서버에 따라, 이들 항목은 파일의 경로를 포함할 수도 있고, 아닐 수도 있다.("foo.txt"를 얻을 수도 있고, "/i/pty/the/foo.txt"를 얻을 수도 있다)

scanDirectory()의 다음 부분은 각 이름에 대해 코드를 반복하면서 해당 이름이 파일인지, 디렉터리인지 구별하기 위해 ftp_size()를 사용한다.(이는 간단한 팁인데, 디렉터리인 경우 크기로 -1을 반환한다) 해당 항목이 파일이면 경로를 일관되게 유지하기 위해 앞에 슬래시(/)를 붙이고, 파일의 수정일을 알기 위해 ftp_mdtm()을 사용한다. 다음에 $fileList 연관 배열에 키로 파일이름을 추가하고, 시간은 값으로 사용한다.
    
   // 디렉터리 내용에 대한 반복 코드
   if($contents!=null)
   {
      foreach ($contents as $item)
      {
         // 해당 항목이 파일인가?
         if (ftp_size($cid, $item)>=0)
         {
             // 파일 앞에 슬래시(/)가 없으면 추가한다.
            if($item[0]!="/") $item = "/" . $item;

            // 결과에 파일을 추가하고, 시간을 수정한다.
            $fileList[$item] = ftp_mdtm($cid, $item);
         }

이제, ftp_nlist()가 반환한 항목이 디렉터리인 경우에 대한 처리가 필요하다. 물론, 같은 디렉터리나 부모 디렉터리에 대한 별칭(alias)는 무시할 것이다. 무시된 것을 제외하고 디렉터리 이름이 있으면 scanDirectory()를 사용해서 하위 디렉터리들을 재귀적으로 탐색한다.(실제로는 전체 경로와 상대 경로를 사용하는 서버들 사이의 차이점들을 다루기 위한 추가 로직이 필요하다) 파일과 디렉터리를 모두 처리했으면, 발견된 모든 파일을 포함한 $fileList를 갖게 된다. 코드는 다음과 같다.

         else
         // 항목이 디렉터리인 경우
         {
            // 현재 위치와 상위 위치에 대한 별칭은 제외
            if($item!="." && $item!=".." && $item!="/")
            {
               // 전체 경로명을 사용하는 서버
               if($item==strstr($item, $dir))
               {
                  scanDirectory($cid, $item);
               }
               else
               {
                  // 상대 경로를 사용하는 서버
                  if($dir=="/")
                  {
                     scanDirectory($cid, $dir . $item);
                  }
                  else
                  {
                     scanDirectory($cid, $dir . "/" . $item);
                  }
               }
            }
         }
      }
   }

   // 결과를 반환한다
   return $fileList;
}

generateRssFeed() 함수

PHP 자습서 같은 느낌으로 시작하지 않았나요? 좋은 소식: 어려운 부분은 이제 끝났습니다. 이제 남은 것은 실제 RSS 피드를 생성하는 함수를 작성하는 것 뿐입니다.

generateRssFeed() 함수는 동작하는데 필요한 모든 매개변수만 전달하면, 다른 PHP 스크립트에서도 직접 호출할 수 있다.
  • $host:  FTP 서버의 호스트이름. 예: "ftp.foo.com".
  • $user: FTP 사용자 이름. 예: "anonymous".
  • $pass: FTP 사용자 비밀번호. 예: "guest".
  • $path: 서버에서의 시작 디렉터리 위치. 예: "/pub/crawl"
  • $itemCount: RSS 피드로 반환할 항목 수
exploreFtpServer() 함수를 호출해서 FTP 서버에서 파일과 시간 목록을 구하고, $itemCount에 지정된 값 보다 목록에 포함된 항목이 부족하면 둘 중에 작은 수를 사용한다.

function generateRssFeed($host, $user, $pass, $path, $itemCount)
{  
   // 파일/시간으로 된 배열을 구한다
   $fileList = exploreFtpServer($host, $user, $pass, $path);

   // 사용자가 지정한 카운트 값이 발견한 파일 수 중에서 작은 쪽을 선택한다
   if(count($fileList)<$itemCount) $itemCount=count($fileList);

앞으로 사용할 변수들을 선언한다. 첫번째는 FTP 프로토콜에서 사용할 호스트이름 앞에 사용할 접두어를 지정하는 $linkPrefix이며, 두번째는 RSS 피드의 게시일을 지정하는 $channelPubDate이다. 또한, 생성한 RSS 항목들을 저장하고 있는 배열 $items를 생성한다.

   // 피드에 사용할 링크의 접두어
   $linkPrefix = "ftp://" . $host;

   // 채널 게시일
   $channelPubDate = null;

   // 항목에 배열
   $items = array();

이제, 가장 최근에 변경된 파일들을 찾아낼 수 있고, 각 항목에 대한 RSS 항목을 만들어낼 수 있다. 여기서는 간단하게 하기 위해 다음과 같은 규칙을 따른다. 각 항목은 title, link, date로 된다. 그러나, 취향에 따라 다른 항목을 추가해도 된다.(파일의 확장자에 해당하는 아이콘을 표시하는 것도 좋은 아이디어가 될 수 있다)

exploreFtpServer()가 반환하는 $fileList는 최근 것이 먼저 오도록 시간에 따라 정렬되어 있다. 배열에 대한 루프를 수행하면서 RSS 항목의 게시일을 생성하기 위해 이 시간을 사용한다. 첫번째(가장 최근에 변경된) 시간은 RSS 피드의 게시일로도 사용된다.

   // 가장 최근에 변경된 파일 목록에서 RSS 항목을 위한 배열을 생성
   foreach ($fileList as $filePath => $time)
   {
      // RFC822에 따라 항목의 게시일 항목을 생성
      $itemPubDate = date("r", $time);

      // 첫번째 항목의 게시일을 채널 게시일로 사용
      if($channelPubDate==null) $channelPubDate = $itemPubDate;

다음으로, 파일에 대해 "ftp://"로 시작하는 URI를 생성한다. $fileUri 변수는 RSS 항목의 title과 link 정보를 제공하기 위해 사용된다. 파일이름에 사용된 모든 공백문자는 URI 표준에 맞게 "%20"으로 바꿔주면 된다.

이제, 필요한 모든 정보를 알고 있기 때문에 각 RSS 항목을 만들기 위한 XML만 생성하면 된다. 이 작업이 끝나면, 나중에 사용하기 위해 $items 배열에 추가한다. $itemCount 값까지만 이 루프를 실행한다.

      // ftp 파일에 대한 URI를 생성
      $fileUri = ereg_replace(" ", "%20", $linkPrefix . $filePath);

      // 항목을 생성
      $item = "mmlt;itemmmgt;"
      ."mmlt;titlemmgt;" . $fileUri . "mmlt;/titlemmgt;"
      ."mmlt;linkmmgt;". $fileUri . "mmlt;/linkmmgt;"
      ."mmlt;pubDatemmgt;" . $itemPubDate . "mmlt;/pubDatemmgt;"
      ."mmlt;/itemmmgt;";

      // 항목을 배열에 추가한다
      array_push($items, $item);

      // 피드의 최대 개수에 도달하면 중단한다.
      if(count($items)==$itemCount) break;
   }

마지막으로 RSS 피드 자체를 생성한다. PHP에서 문자열을 사용해서 XML을 작성하는 것은 쉽지만, 알아보기는 생각처럼 쉽지 않다. RSS 항목을 모두 추가하기 위해 join()을 사용하고 있으며, 마지막에는 줄바꿈(\n)이 들어가 있다.(이 줄바꿈이 꼭 필요한 것은 아니지만, 피드에 문제가 생겼을 때 직접 읽을 때 도움이 된다)

   // RSS 피드를 구축한다.
   $rss = "mmlt;rss version="2.0"mmgt;"
   . "mmlt;channelmmgt;"
   . "mmlt;titlemmgt;FTP Monitor: " . $host . "mmlt;/titlemmgt;"
   . "mmlt;linkmmgt;" . $linkPrefix . "mmlt;/linkmmgt;"
   . "mmlt;descriptionmmgt;The " . $itemCount." latest changes on "
   . $host . $path . " (out of " . count($fileList)
   . " files)mmlt;/descriptionmmgt;"
   . "mmlt;pubDatemmgt;" . $channelPubDate . "mmlt;/pubDatemmgt;" . "\n"
   . join("\n", $items) . "\n"
   . "mmlt;/channelmmgt;"
   . "mmlt;/rssmmgt;";

이제, 피드를 내보낼 준비가 되었다. 남은 것은 XML 문서라는 것을 알려주기 위해 HTTP 헤더를 설정하고, 피드를 출력하는 것 뿐이다.

   // 헤더에 XML mime 타입을 설정한다.
   header("Content-type: text/xml; charset=UTF-8");
  
   // RSS 피드를 표시한다.
   echo($rss);
}

이제, 모든 작업을 마쳤다. 지금까지 예제를 작성하지 않았다면 ftp_monitor.php의 전체 소스 코드를 다운로드할 수 있다.
함께 활용하기

ftp_monitor.php를 PHP를 사용할 수 있는 웹 서버에 올려놓았으면, 다른 PHP 스크립트에서 다음 예제처럼 이 스크립트를 참고할 수 있다.

  
   // FTP 모니터를 가져오기
   require_once("ftp_monitor.php");
    
   // FreeBSD 스냅샷을 모니터링하기 위한 연결 매개변수
   $host = "ftp.freebsd.org";
   $user = "anonymous";
   $pass = "guest@anon.com";
   $path = "/pub/FreeBSD/snapshots";
  
   // 가장 최신의 FreeBSD 스냅샷을 보기 위한 RSS 2.0 피드를 생성
   generateRssFeed($host, $user, $pass, $path, 10);
?>

위의 연결 매개변수, freebsd.xml에서 예제 출력을 얻고, RSS 리더기인 SharpReader에서 보면 다음과 같다.

그림2
그림2. ftp.freebsd.org를 모니터링한 항목

대부분의 RSS 수집기는 description 요소가 없으면 해당 항목의 링크로 자동이동한다. SharpReader도 같은 방식으로 동작하며, ftp:// 프로토콜을 지원한다. 따라서, FTP 모니터로부터 항목을 클릭하는 것으로 다운로드를 할 수 있다. FTP 서버가 익명(anonymous) 연결을 하용하면 이는 제대로 동작한다. 그러나, ftp_monitor.php에서 실제 사용자 이름과 비밀번호를 제공해야 한다면 "클릭 & 다운로드" 기능은 현재 사용하고 있는 RSS 리더에서 FTP 인증화면을 표시할 수 있느냐에 따라 다르다.

수행성능 개선하기

이 스크립트를 사용하려면 알아둬야 할 몇가지 결점들이 있다. 첫째, 대부분의 FTP 서버는 충분히 빠르지 못하며, 이 스크립트의 수행 성능은 FTP 명령의 응답 속도에 따라 결정된다. 단순히 말해서, 재귀를 수행하는 디렉터리가 많을 수록 더 오래 걸린다. 따라서, 모니터링할 필요가 있는 부분으로 제한하는 것이 좋다.
둘째, 이 스크립트는 동시 접속자가 많은 환경을 고려한 것이 아니다. 속도 문제도 있지만, 다른 문제는 FTP 연결수이다. 모든 접속자가 이 스크립트를 사용해서 접속한다면 FTP 서버에 연결이 생성되며, 이용할 수 있는 연결수도 최대값에 도달하게 된다. 따라서, 많은 사용자에게 RSS 피드를 제공해야 한다면, 주기적으로 이 스크립트를 호출해서 캐시(정적 파일)를 생성하고, 이 스크립트는 숨겨둬야 한다. 즉, 실제 부하는 ftp_monitor 스크립트가 아니라 캐시에서 처리하게 해야 한다.
이런 제약사항들을 염두에 둔다면 응답 시간에 관계없이 필요한 정보들을 사용자에게 제공할 수 있을 것이다.
공헌하기

만약, 이 스크립트가 마음에 들었거나, 이것을 멋지게 수정했다면 그에 대한 것을 듣고 싶다. xml.com 커뮤니티에서 배웠던 것들에 대해 코멘트하고, 공유하기 바란다.
TAG :
댓글 입력
자료실