[C#] FTP에 접속해서 파일 다운로드, 업로드하는 방법


Development note/C#  2020. 2. 14. 09:00

안녕하세요. 명월입니다.


이 글은 C#에서 FTP에 접속해서 파일 다운로드, 업로드하는 방법에 대한 글입니다.


프로그램 통신 상에서 자주 사용되는 프로토콜이라고 하면, Http, Mail이 있고 그리고 FTP가 아닐까 싶습니다.

Samba도 네트워크 통신이기는 하지만 딱히 프로그램에서 설정해서 접속하지 않고 OS에서 네트워크 드라이브를 연결하거나 Linux환경이면 Mount해서 내부 드라이브처럼 접속할 수 있기 때문에, 딱히 프로그램에서 접속해서 사용하지는 않습니다.

전혀 없는 건 아닙니다만, 굳이 쉬운 길을 놔두고 어렵게 개발할 필요는 없겠습니다.


최근에는 FTP를 잘 사용하지 않고 아예 Http 해더를 이용해서 Content-Type을 application/octet-stream으로 지정해서 파일 업로드하는 방법을 많이 사용합니다.

최근 게임 서버등로 예전처럼 Server 프로그램을 만드는 것보다 Web Server(IIS나 Tomcat)에 붙여서 사용하는 방법을 사용하기 때문에 FTP 사용 빈도는 많이 낮아지겠네요.(반드시 웹서버에 게임 서버를 붙이는 건 아니고 사양에 따른 선택이라고 생각합니다.)


그래도 FTP를 잘 이용하면 여러가지 파일 관리 프로그램을 만들 수도 있고 FTP가 Http 프로토콜보다 많이 간단하기 때문에 잘 이용하면 성능 개선에도 도움이 될 것입니다.


FTP 서버 설치는 이전에 설명한 적이 있으므로 참조하시면 됩니다.

링크 - [CentOS] FTP 설정, vsftpd 설정

링크 - [Window] FTP 서버를 구축하는 방법

using System;
using System.Collections.Generic;
using System.Net;
using System.IO;

namespace FTPConnector
{
  class Program
  {
    private string id = "user";
    private string pwd = "password";
    public Program()
    {
      // 로컬의 파일을 업로드하는 함수 호출
      UploadFileList("ftp://localhost", @"d:\ftptest\upload");
      // FTP에서 파일을 다운로드 함수 호출
      DownloadFileList("ftp://localhost", @"d:\ftptest\download");
    }
    // FTP 서버 접속 함수
    private FtpWebResponse Connect(String url, string method, Action<FtpWebRequest> action = null)
    {
      // WebRequest 클래스를 이용해 접속하기 때문에 객체를 가져온다. (FtpWebRequest로 변환)
      var request = WebRequest.Create(url) as FtpWebRequest;
      // Binary 형식으로 사용한다.
      request.UseBinary = true;
      // FTP 메소드 설정(아래에 별도 설명)
      request.Method = method;
      // 로그인 인증
      request.Credentials = new NetworkCredential(id, pwd);
      // request.GetResponse()함수가 호출되면 실제적으로 접속이 되기 때문에, 그전에 설정할 callback 함수 호출
      if (action != null)
      {
        action(request);
      }
      // 접속해서 WebResponse함수를 가져온다.
      return request.GetResponse() as FtpWebResponse;
    }
    // 업로드 함수
    private void UploadFileList(String url, string source)
    {
      // 업로드할 경로의 속성을 구한다.
      var attr = File.GetAttributes(source);
      // 만약 디렉토리라면..
      if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
      {
        // 디렉토리 정보를 가져온다.
        DirectoryInfo dir = new DirectoryInfo(source);
        // 디렉토리 안의 파일 리스트를 가져온다.
        foreach (var item in dir.GetFiles())
        {
          // 파일을 업로드한다.
          UploadFileList(url + "/" + item.Name, item.FullName);
        }
        // 디렉토리 안의 하위 디렉토리 리스트를 가져온다.
        foreach (var item in dir.GetDirectories())
        {
          try
          {
            // ftp에 디렉토리를 생성한다.
            Connect(url + "/" + item.Name, WebRequestMethods.Ftp.MakeDirectory).Close();
          }
          catch (WebException)
          {
            // 만약에 ftp에 디렉토리가 존재한다면 에러가 날 것이다.
          }
          // 디렉토리를 업로드한다.(재귀 함수 호출)
          UploadFileList(url + "/" + item.Name, item.FullName);
        }
      }
      else
      {
        // 디렉토리가 아닌 파일을 경우인데, 파일의 stream을 취득한다.
        using (var fs = File.OpenRead(source))
        {
          // 파일을 업로드한다.
          Connect(url, WebRequestMethods.Ftp.UploadFile, (req) =>
          {
            // 파일 크기 설정
            req.ContentLength = fs.Length;
            // GetResponse()가 호출되기 전에 request스트림에 파일 binary를 넣는다.
            using (var stream = req.GetRequestStream())
            {
              fs.CopyTo(stream);
            }
          }).Close();
          // respose 객체를 닫는다.
        }
      }
    }
    // 다운로드 함수
    private void DownloadFileList(string url, string target)
    {
      var list = new List<String>();
      // ftp에 접속해서 파일과 디렉토리 리스트를 가져온다.
      using (var res = Connect(url, WebRequestMethods.Ftp.ListDirectory))
      {
        using (var stream = res.GetResponseStream())
        {
          using (var rd = new StreamReader(stream))
          {
            while (true)
            {
              // binary 결과에서 개행(\r\n)의 구분으로 파일 리스트를 가져온다.
              string buf = rd.ReadLine();
              // null이라면 리스트 검색이 끝난 것이다.
              if (string.IsNullOrWhiteSpace(buf))
              {
                break;
              }
              list.Add(buf);
            }
          }
        }
      }
      // ftp 리스트를 돌린다.
      foreach (var item in list)
      {
        try
        {
          // 파일을 다운로드한다.
          using (var res = Connect(url + "/" + item, WebRequestMethods.Ftp.DownloadFile))
          {
            using (var stream = res.GetResponseStream())
            {
              // stream을 통해 파일을 작성한다.
              using (var fs = File.Create(target + "\\" + item))
              {
                stream.CopyTo(fs);
              }
            }
          }
        }
        catch (WebException)
        {
          // 그러나 파일이 아닌 디렉토리의 경우는 에러가 발생한다.
          // 로컬 디렉토리를 만든다.
          Directory.CreateDirectory(target + "\\" + item);
          // 디렉토리라면 재귀적 방법으로 다시 파일리스트를 탐색한다.
          DownloadFileList(url + "/" + item, target + "\\" + item);
        }
      }
    }
    static void Main(string[] args)
    {
      // 프로그램 실행
      new Program();
      // 아무 키나 누르면 종료.
      Console.WriteLine("Press any key...");
      Console.ReadKey();
    }
  }
}

파일 다운로드할 때 리스트를 취득하는데 이게 directory와 파일이 구분이 없습니다. 물론 검색 메소드를 ListDirectory가 아닌 ListDirectoryDetail로 하면 파일과 디렉토리를 구분할 수는 있습니다만 그럼 결과를 파싱해야 하고 여러가지 귀찮기 때문에 try catch로 에러가 나면 디렉토리, 안나면 파일로 지정했습니다.

사실 if else로 구분할 수 있는데, 좋은 코드는 아니네요.. 저는 최대한 소스를 줄이기 위해서 이렇게 작성했지만 실제로 사용하시려면 ListDirectoryDetail로 구분을 해서 다운로드하는 게 좋습니다. try ~ catch는 성능에 매우 안 좋기 때문에..


저는 ftp서버를 ftptest/ftp로 지정했고 upload파일을 ftptest/upload, 다운로드할 곳을 ftptest/download로 설정해서 테스트를 하겠습니다.

이렇게 있는 파일들을 ftptest/ftp로 업로드가 되곘네요.

그리고 그걸 다시 다운로드 받아서 ftptest/download에 저장이 될 것입니다.

실행합니다.

로그를 하나도 안남겨서 아무것도 표시가 되지 않네요...

ftp 서버에는 업로드가 잘 되었습니다.

다시 ftp서버로 부터 다운로드 받는 것도 잘 되었습니다.


저의 경우는 특별히 예제를 만들 것이 없어서 파일을 읽어서 ftp에 작성하고 다시 ftp에서 파일을 읽어서 파일로 작성했습니다.

근데 어차피 stream으로 binary를 다루는 것이니 꼭 ftp 통신이라고 해서 파일 업로드 다운로드 용만 사용할 것 아닌 것 같습니다.

물론 데이터 베이스 만큼 빈번히 접속해서 검색하고 저장하게 되면 ftp 효율성은 매우 떨어 지겠지만, db에 넣기에는 좀 큰 데이터, 그러나 자주 참조는 해야 하지만 수정은 많이 없는 데이터를 상대로 사용해도 좋을 것 같네요.


아 그리고 참고 사항으로 request.Method에 대해 설명하곘습니다.

request.Method는 string형식으로 들어가지만 ftp 프로토콜에 맞는 method를 설정해야 합니다.

링크 - https://ko.wikipedia.org/wiki/FTP_명령어_목록

위의 경우는 NLST로 리스트를 취득하고, STOR로 업로드를 했고 MKD로 디렉토리를 생성했습니다. 그리고 RETR로 파일을 다운로드 했습니다.

이것을 C#에서는 자주 사용하는 명령어를 정리해 놓았습니다.

namespace System.Net
{
  public static class WebRequestMethods
  {
    public static class Ftp
    {
      // 파일 다운로드
      public const string DownloadFile = "RETR";
      // 리스트 검색
      public const string ListDirectory = "NLST";
      // 파일 업로드
      public const string UploadFile = "STOR";
      // 파일 삭제
      public const string DeleteFile = "DELE";
      // 파일을 이어서 작성
      public const string AppendFile = "APPE";
      // 파일 사이즈 요청
      public const string GetFileSize = "SIZE";
      // 파일 업로드 (파일 이름 유니크하게...근데 파일 이름이 같으면 OS에서 그냥 덮어 씌울 텐데..)
      public const string UploadFileWithUniqueName = "STOU";
      // 디렉토리 생성 요청
      public const string MakeDirectory = "MKD";
      // 디렉토리 삭제 요청
      public const string RemoveDirectory = "RMD";
      // 리스트 디테일하게 요청
      public const string ListDirectoryDetails = "LIST";
      // 파일 작성 시간 취득
      public const string GetDateTimestamp = "MDTM";
      // 디렉토리 이동(의미 없는 것)
      public const string PrintWorkingDirectory = "PWD";
      // 파일 이름 변경
      public const string Rename = "RENAME";
    }
  }
}

링크 - https://docs.microsoft.com/en-us/dotnet/framework/network-programming/how-to-upload-files-with-ftp

링크 - https://stackoverflow.com/questions/2781654/ftpwebrequest-download-file


여기까지 C#에서 FTP에 접속해서 파일 다운로드, 업로드하는 방법에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.