. FTP 열기

FTP 열기 대화상자는 FTP로부터 대상 파일의 이름을 조사할 뿐이지 직접 파일을 여는 것은 아니다. FTP 파일은 이 대화상자에서뿐만 아니라 MRU나 프로젝트에서도 열 수 있어야 하므로 결국은 OpenFromFile 함수에 의해 열려야 한다. 단순히 파일의 이름을 얻는데 저렇게 많은 코드가 필요한 이유는 FTP 서버의 폴더 구조를 보여주고 폴더를 바꿀 수 있도록 해야 하기 때문이다.

이 대화상자의 결과물은 결국 파라미터로 리턴되는 원격지의 파일 경로뿐이다. 이 경로는 다음 형식으로 작성되는데 경로명에 접속정보와 파일의 위치, 파일의 이름까지 완벽한 정보가 포함되어 있다. 이렇게 하지 않으면 FTP 파일을 열 때마다 접속정보를 물어야 할 것이며 일관된 방법으로 파일 입출력을 할 수가 없다.

 

ftp://사용자명:비밀번호:서버:포트/폴더/파일

 

당근이 정의하는 이 경로 포맷은 표준화된 FTP 경로 지정법은 아니며 독자적으로 정의한 것이다. 사실 FTP 경로를 지정하는 방법이 있기는 하지만 당근의 경우와는 맞지 않는 것 같아 고유의 경로 표현법을 만들었다. 어차피 당근이 만든 경로이고 당근만 사용하므로 표준이 아니라도 큰 상관은 없다.

OnCommand에서는 FTP 열기 대화상자를 호출하되 파라미터로 문자열 버퍼를 제공하며 이 버퍼에 사용자가 선택한 FTP 파일의 경로가 리턴된다. 이 경로를 OpenFromFile 함수로 넘겨주기만 하면 사용자가 선택한 파일을 즉시 열 수 있다.

 

void OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     case IDM_FILE_FTPOPEN:

          if (DialogBoxParam(g_hInst, MAKEINTRESOURCE(IDD_FTP),

              g_hFrameWnd, DGFtpProc,(LPARAM)Path)==IDOK) {

              if (lstrlen(Path)) {

                   OpenFromFile(Path);

              }

          }

          break;

 

OpenFileToChild 함수는 FTP 파일을 다룰 수 있도록 다음과 같이 기능이 확장된다.

 

BOOL OpenFileToChild(HWND hChild, TCHAR *Path)

{

     ....

     if (strnicmp(Path,"http",4) == 0) {

          dwSize=DgHttpDown(Path,TextBuf);

          if (dwSize==-1) {

              return FALSE;

          }

    } else if (strnicmp(Path,"ftp",3) == 0) {

        dwSize=DgFtpDown(Path,TextBuf);

        if (dwSize==-1) {

           return FALSE;

        }

     } else {

 

파일명의 앞 3자가 ftp로 시작되면 FTP 파일로 인식하고 DgFtpDown 함수를 호출하여 파일을 다운로드받도록 한다. HTTP로 파일을 다운로드받는 과정과 유사하되 다운로드를 받아주는 함수만 달라졌을 뿐이다.

이제 다음 할 일은 FTP 다운로드 함수인 DgFtpDown 함수를 작성하는 일인데 그 전에 FTP 경로로부터 FTP 접속정보를 추출해 내는 함수를 먼저 만들어야 한다. WinInet 라이브러리가 당근이 정의하고 있는 FTP 경로 포맷을 이해하지 못하기 때문에 일단 접속정보를 추출하여 접속한 후 폴더 경로를 찾아가서 파일을 읽어와야 한다. 이 함수는 프로젝트 전체에 걸쳐 사용되므로 Util.cpp에 작성한다.

 

void ParseFtpInfo(TCHAR *Path, TCHAR *Server, TCHAR *User, TCHAR *Pass, TCHAR *& File, int &Port)

{

     TCHAR *p,*p2;

 

     p=Path+6;

     p2=strchr(p,’:’);

     lstrcpyn(User,p,p2-p+1);

 

     p=p2+1;

     p2=strchr(p,’:’);

     lstrcpyn(Pass,p,p2-p+1);

 

     p=p2+1;

     p2=strchr(p,’:’);

     lstrcpyn(Server,p,p2-p+1);

 

     Port=atoi(p2+1);

 

     File=strchr(p2+1,’/’)+1;

}

 

Path에서 콜론으로 구분된 각각의 토큰을 분리하여 서버 주소, 사용자 ID, 비밀번호 포트 번호를 추출한다. 다음은 이 정보를 사용하여 FTP 서버에 접속하고 파일의 데이터를 다운로드받는 DgFtpDown 함수이다.

 

 

int DgFtpDown(TCHAR *Path, TCHAR *&Text)

{

     HINTERNET hRemote;

     DWORD FileSize,Size;

     DWORD dwRead;

     BOOL Result=FALSE;

     TCHAR Server[256];

     TCHAR User[32];

     TCHAR Pass[32];

     TCHAR Dir[MAX_PATH];

     int Port;

     TCHAR *p,*p2;

     MSG msg;

     TCHAR Mes[400];

     HWND hDlgDown;

 

     bContDown=TRUE;

     hDlgDown=CreateDialog(g_hInst, MAKEINTRESOURCE(IDD_DOWNLOAD),

          g_hFrameWnd, (DLGPROC)DGDownProc);

     SetDlgItemText(hDlgDown,IDC_STDOWN1,"인터넷 접속중...");

     SetWindowText(hDlgDown,"FTP 다운로드");

     ShowWindow(hDlgDown,SW_SHOW);

     UpdateWindow(hDlgDown);

     EnableWindow(g_hFrameWnd, FALSE);

 

     ParseFtpInfo(Path,Server,User,Pass,p,Port);

     if (DgFtpConnect(Server,User,Pass,Port)==FALSE) {

          MessageBox(g_hFrameWnd, "FTP 다운로드를 위해 인터넷에 접속할 수 없습니다","알림",MB_OK);

          goto NetFail;

     }

 

     for (;;) {

          p2=strchr(p,’/’);

          if (p2==NULL) {

              break;

          }

          lstrcpyn(Dir,p,p2-p+1);

          if (FtpSetCurrentDirectory(hFtp,Dir)==FALSE) {

              MessageBox(g_hFrameWnd,"FTP 서버에서 폴더를 찾을 수 없습니다.","알림",MB_OK);

              goto EndDown;

          }

          p=p2+1;

     }

 

     WIN32_FIND_DATA wfd;

     HINTERNET hFind;

     hFind=FtpFindFirstFile(hFtp,p,&wfd,0,0);

     if (hFind==NULL) {

          goto EndDown;

     }

     FileSize=wfd.nFileSizeLow;

     InternetCloseHandle(hFind);

 

     hRemote=FtpOpenFile(hFtp,p,GENERIC_READ,FTP_TRANSFER_TYPE_BINARY,0);

     if (hRemote == NULL) {

          MessageBox(g_hFrameWnd,"FTP 서버에서 파일을 찾을 수 없거나 권한이 없습니다.","알림",MB_OK);

          goto EndDown;

     }

 

     if (FileSize > 1048576 * 30) {

          MessageBox(g_hFrameWnd,"FTP로는 최대 30M까지만 열 수 있습니다.","알림",MB_OK);

          goto EndDown;

     }

 

     wsprintf(Mes,"위치 : %s",Path);

     SetDlgItemText(hDlgDown,IDC_STDOWN1,Mes);

 

     Text=(TCHAR *)malloc(FileSize+2);

     memset(Text,0,FileSize+2);

     p=Text;

 

     for (;;) {

          InternetQueryDataAvailable(hRemote,&Size,0,0);

          Result=InternetReadFile(hRemote,p,Size,&dwRead);

          if (Result==FALSE) {

              MessageBox(g_hFrameWnd, "FTP 서버의 파일을 읽을 수 없습니다","알림",MB_OK);

              break;

          }

          if (bContDown==FALSE) {

              break;

          }

          if (dwRead==0) {

              break;

          }

          p+=dwRead;

          SendMessage(hDlgDown,WM_USER+1,(WPARAM)(p-Text),(LPARAM)FileSize);

          while (PeekMessage(&msg, NULL,0,0,PM_REMOVE)) {

              if (!IsDialogMessage(hDlgDown, &msg)) {

                   TranslateMessage(&msg);

                   DispatchMessage(&msg);

              }

          }

     }

 

EndDown:

     DgFtpUnConnect();

NetFail:

     EnableWindow(g_hFrameWnd, TRUE);

     DestroyWindow(hDlgDown);

     if (Result == TRUE && bContDown == TRUE) {

          return FileSize;

     } else {

          return -1;

     }

}

 

전체적인 구조는 DgHttpDown 함수와 유사하며 버퍼를 관리하는 방법도 동일하다. 다만 접속하는 서버가 HTTP 서버가 아닌 FTP 서버라는 점만 다르다. 호출측에서 파일의 크기를 모르기 때문에 이 함수가 파일의 크기만큼 버퍼를 할당하며 그래서 Text인수는 포인터의 레퍼런스이다. 이 메모리는 호출측에서 해제해야 한다.

이 함수는 최대 30MB까지의 파일만 다운로드받아주며 그 이상의 파일에 대해서는 에러로 처리하고 있는데 이는 안전상의 이유 때문이다. FTP 서버는 HTTP 서버와는 달리 수백 MB정도의 대용량 파일들이 많이 있는데, 이런 파일을 잘못 선택했다가는 거의 프로그램이 다운 상태가 됨으로 이런 상태를 방지하기 위해 용량의 제한을 두었다.

사실 30MB 이상이나 되는 텍스트 파일을 다운로드받아서 편집한 후 다시 업로드하는 경우란 거의 없기 때문에 이런 제한이 별 문제가 되지는 않는다. 로컬 파일의 경우 메모리관리상의 이유로 30MB의 제한을 두었으며 30MB를 넘을 경우 정말로 열 것인지 질문을 했지만 FTP 다운로드는 제한을 넘을 경우 질문도 하지 않고 일방적으로 메시지만 보여주고 리턴해 버렸다. 그렇게 큰 파일을 편집하려면 따로 다운로드받아서 편집하는 것이 더 나은 방법이다.

FTP 서버에 있는 파일의 크기를 조사하기 위해 하나의 파일에 대해 열거를 하고 있는데 이런 방법 대신 FtpGetFileSize 함수로 훨씬 더 간단하게 조사할 수도 있다. 그러나 이 함수는 IE 5.0이상에서만 지원되며 비주얼 C++ 6.0에서는 아예 컴파일이 안되기 때문에 어쩔 수 없이 FtpFindFirstFile 함수를 사용하였다. 플랫폼 SDK를 설치하고 IE 5.0이상에 대해서만 실행되도록 한다면 FtpGetFileSize 함수를 사용해도 상관없지만 사용 환경에 제약이 생긴다는 점에서 바람직하지 못하다.