천객만래 [千客萬來] (It has an interminable succession of visitors)


이번 내용은 팁이라고 하기에는 좀 그렇고 제가 한참 고생하다 방법을 찾아서 너무 기쁜 나머지 여기에 올리게 되었습니다. ^^;;

대단한건 아니고 닷넷에서 파일 다운로드 구현할때 파일명에 한글이 있으면 다운로드 창에서 파일명이 깨져서 나오는 현상 해결 방법 입니다.

일단 소스를 보시면 다들 이해 하시리라 생각됩니다.



public static void FileDownload(string sFileName)
{
try
{
//파일 저장 경로
string sBoardDataPath = ConfigUtil.GetUploadRootPath() + "\\NB_BoardData_001001";

string sTmpFilePath = sBoardDataPath.Trim() + "\\" + sFileName.Trim();

//헤더에 파일 이름 지정
Response.ClearHeaders();
Response.ClearContent();

Response.ContentType = "multipart/form-data";
Response.AddHeader
("Content-Disposition", "attachment;filename=" + Server.UrlPathEncode(sFileName));
Response.WriteFile (sTmpFilePath);
Response.Flush();
Response.Close();
}
catch(Exception E)
{
throw E;
}
}






위 소스는 내보드에서 사용하는 파일 다운로드 함수를 약간 수정한 것 입니다.


위 소스에서 UrlPathEncode 이 부분이 한글 파일명을 안깨지고 보여지도록 하는 부분 입니다.


어딜 찾아보니 UrlEncode로 하면 된다고 하던데 안되더군요.

UrlEncode과 UrlPathEncode에 대한 자세한 설명은 다음 MSDN 페이지를 참조하시면 됩니다.

UrlEncode 설명
UrlPathEncode 설명

벌써 다들 알고있는 내용이었다면 대략낭패 ;;
Posted by SB패밀리

엑셀 파일 다운로드/업로드(소스)

//  프로그램설명 : DB Table 데이터를 엑셀로 저장하여 다운로드 받을 수 있도록 하거나,


//  엑셀의 데이터를 DB Table로 업로드할 수 있는 클래스


//  단, Excel 프로그램이 설치되어있어야 동작 가능함. SqlHelper 부분은 별도로 추가하셔야 합니다.

// 주석을 자세히 달았으므로 이해하는데는 문제가 없으실 겁니다.


 

using System;
using System.IO;
using System.Data;
using System.Reflection;
using Excel=Microsoft.Office.Interop.Excel;
using System.Collections;
using System.Web;
using System.Web.UI;
using System.Data.OleDb;
using System.Data.SqlClient;

namespace Utilities
{
    ///


    /// DataTable을 입력 데이타로 받아서 해당 데이타로 새로운 엑셀 파일을 만들며
    /// 3가지 유형의 함수를 현재 제공하고 있다.
    /// 향후에는 ADO.NET 처리를 하는 부분과 엑셀 파일에 컬럼명이 추가되도록 할 예정이다.
    /// DB Table 데이터를 엑셀로 저장하여 다운로드 받을 수 있도록 하거나,
    /// 엑셀의 데이터를 DB Table로 업로드할 수 있는 클래스
    /// 단, Excel 2003 혹은 최소 XP 프로그램이 설치되어있어야 동작 가능함.
    ///

    public class ExcelHandler
    {
        ///
        /// 임시 생성 엑셀파일 목록 관리
        ///

        private static ArrayList exportedFiles = new ArrayList();
        ///
        /// 임시 파일 삭제 주기(분 단위)
        ///

        private static int deleteMinutePeriod = 30;

        ///


        /// 임시 엑셀 파일 삭제 주기 설정한다.
        /// 단위는 분단위임. 설정하지 않을 시에는 기본 값이 30분이다.
        /// 30분간만의 임시 파일만 유지한다.
        ///

        public static int DeletePeriodMinute
        {
            set
            {
                deleteMinutePeriod = value;
            }
            get
            {
                return deleteMinutePeriod;
            }
        }

        ///


        /// 생성자
        ///

        public ExcelHandler()
        {
        }

        #region 새로운 엑셀 파일 생성 유형
        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 새로운
        /// 엑셀 파일을 만든다.
        /// 저장된 엑셀 파일을 다운로드하지 않는다.
        /// 1번째 행과 1번째 열부터 저장한다.
        ///

        /// 입력 데이타(DataTable)
        /// 저장 경로명과 파일명
        /// 저장 성공 여부
        public static bool ExportNewFile(DataTable dt, string savedFile)
        {
            return PExportNewFile(dt, savedFile, 1, 1, null);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 새로운
        /// 엑셀 파일을 만든다.
        /// 저장된 엑셀 파일을 다운로드하지 않는다.
        /// 1번째 행과 1번째 열부터 저장한다.
        /// 저장된 엑셀 파일을 다운로드한다.
        ///

        /// 입력 데이타(DataTable)
        /// 저장 경로명과 파일명
        /// 다운로드될 페이지
        /// 저장 성공 여부
        public static bool ExportNewFile(DataTable dt, string savedFile, Page downloadPage)
        {
            return PExportNewFile(dt, savedFile, 1, 1, downloadPage);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 새로운
        /// 엑셀 파일을 만든다.
        /// 저장된 엑셀 파일을 다운로드하지 않는다.
        ///

        /// 입력 데이타(DataTable)
        /// 저장 경로명과 파일명
        /// 엑셀에서 데이타 시작행
        /// 엑셀에서 데이타 시작열
        /// 저장 성공 여부
        public static bool ExportNewFile(DataTable dt, string SavedFile, int row, int col)
        {
            return PExportNewFile(dt, SavedFile, row, col, null);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 새로운
        /// 엑셀 파일을 만든다.
        /// 저장된 엑셀 파일을 다운로드한다.
        ///

        /// 입력 데이타(DataTable)
        /// 저장 경로명과 파일명
        /// 엑셀에서 데이타 시작행
        /// 엑셀에서 데이타 시작열
        /// 엑셀 파일 다운로드를 요청한 페이지
        /// 저장 성공 여부
        public static bool ExportNewFile(DataTable dt, string SavedFile, int row, int col, Page downloadPage)
        {
            return PExportNewFile(dt, SavedFile, row, col, downloadPage);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 새로운
        /// 엑셀 파일을 만든다.
        ///

        /// 입력 데이타(DataTable)
        /// 저장 경로명과 파일명
        /// 엑셀에서 데이타 시작행
        /// 엑셀에서 데이타 시작열
        /// 엑셀 파일 다운로드를 요청한 페이지
        /// 저장 성공 여부
        private static bool PExportNewFile(DataTable dt, string SavedFile, int row, int col, Page downloadPage)
        {
            try
            {
                DeleteTemporaryFiles();

                GC.Collect();// clean up any other excel guys hangin'' around...
                Excel.Application excelApp = new Excel.Application();
                // excelApp.Visible = true;
                Excel.Workbook excelWorkbook = excelApp.Workbooks.Add(Type.Missing);
                Excel.Sheets excelSheets = excelWorkbook.Worksheets;
                string currentSheet = "Sheet1";
                Excel.Worksheet excelWorksheet = (Excel.Worksheet)excelSheets.get_Item(currentSheet);
    
                int ColCounter = dt.Columns.Count;

                // 컬럼명을 먼저 기록한다.
                int columnidex = 0;
                foreach(DataColumn dc in dt.Columns)
                {
                    excelWorksheet.Cells[row, col+columnidex++] = dc.ColumnName;
                }

                // 컬럼명을 적은 Row 다음에 데이터를 적을 수 있게끔 증가한다.
                row++;

                DataRow[] rpt = dt.Select();
                foreach(DataRow dr in rpt)
                {
                    for(int i = 0; i < ColCounter; i++)
                    {
                        excelWorksheet.Cells[row, col+i] = dr[i].ToString();
                    }
                    row++;
                }

                string tempSavedFileName = SavedFile + "." + Guid.NewGuid().ToString();
               

                FileInfo f = new FileInfo(tempSavedFileName);
                if(f.Exists)
                    f.Delete(); // delete the file if it already exist.
                // Workbook 저장
                excelWorkbook.SaveAs(tempSavedFileName, Excel.XlFileFormat.xlWorkbookNormal, null, null, false, false, Excel.XlSaveAsAccessMode.xlExclusive, false, false, null, null, null);
                // 클린업 작업
                excelWorkbook.Close(null, null, null);
                excelApp.Workbooks.Close();
                // excelApp.Application.Quit();
                excelApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelWorksheet);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelWorkbook);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp);
                excelWorksheet = null;
                excelWorkbook = null;
                excelApp = null;
                GC.Collect();

                if(downloadPage != null)
                    Download(downloadPage, SavedFile, tempSavedFileName);

                return true;

            }
            catch( Exception theException )
            {
                GC.Collect();
                throw theException;
            }
        }
        #endregion

        #region 템플릿 파일 사용 유형 1
        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을
        /// 열어 엑셀 파일을 만든다.
        /// 저장된 파일의 다운로드를 요청하지 않는다.
        ///

        /// 입력 데이타
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// 저장 성공 여부
        public static bool ExportExistFile(DataTable dt, string OriginalFile, string SavedFile, string ws)
        {
            return PExportExistFile(dt, OriginalFile, SavedFile, ws, 1, 1, null);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을
        /// 열어 엑셀 파일을 만든다.
        /// 저장된 파일의 다운로드를 요청한다.
        ///

        /// 입력 데이타
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// 엑셀 파일 다운로드를 요청한 페이지
        /// 저장 성공 여부
        public static bool ExportExistFile(DataTable dt, string OriginalFile, string SavedFile, string ws, Page downloadPage)
        {
            return PExportExistFile(dt, OriginalFile, SavedFile, ws, 1, 1, downloadPage);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을
        /// 열어 엑셀 파일을 만든다.
        /// 저장된 파일의 다운로드를 요청하지 않는다.
        ///

        /// 입력 데이타
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// Sheet에서 데이타의 시작 행
        /// Sheet에서 데이타의 시작 컬럼
        /// 저장 성공 여부
        public static bool ExportExistFile(DataTable dt, string OriginalFile, string SavedFile, string ws, int row, int col)
        {
            return PExportExistFile(dt, OriginalFile, SavedFile, ws, row, col, null);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을
        /// 열어 엑셀 파일을 만든다.
        /// 저장된 파일의 다운로드를 요청한다.
        ///

        /// 입력 데이타
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// Sheet에서 데이타의 시작 행
        /// Sheet에서 데이타의 시작 컬럼
        /// 엑셀 파일 다운로드를 요청한 페이지
        /// 저장 성공 여부
        public static bool ExportExistFile(DataTable dt, string OriginalFile, string SavedFile, string ws, int row, int col, Page downloadPage)
        {
            return PExportExistFile(dt, OriginalFile, SavedFile, ws, row, col, downloadPage);
        }

        ///


        /// 데이타 테이블을 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을
        /// 열어 엑셀 파일을 만든다.
        ///

        /// 입력 데이타
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// Sheet에서 데이타의 시작 행
        /// Sheet에서 데이타의 시작 컬럼
        /// 엑셀 파일 다운로드를 요청한 페이지
        /// 저장 성공 여부
        private static bool PExportExistFile(DataTable dt, string OriginalFile, string SavedFile, string ws, int row, int col, Page downloadPage)
        {
            try
            {
                DeleteTemporaryFiles();

                GC.Collect();// clean up any other excel guys hangin'' around...
                Excel.Application excelApp = new Excel.Application();
                Excel.Workbook excelWorkbook = excelApp.Workbooks.Open(OriginalFile, 0, false, 5, "", "", false, Excel.XlPlatform.xlWindows, "", true, false, 0, true, false, false);
                Excel.Sheets excelSheets = excelWorkbook.Worksheets;
                Excel.Worksheet excelWorksheet = (Excel.Worksheet)excelSheets.get_Item(ws);
                int ColCounter = dt.Columns.Count;

                // 컬럼명을 먼저 기록한다.
                int columnidex = 0;
                foreach(DataColumn dc in dt.Columns)
                {
                    excelWorksheet.Cells[row, col+columnidex++] = dc.ColumnName;
                }

                // 컬럼명을 적은 Row 다음에 데이터를 적을 수 있게끔 증가한다.
                row++;

                DataRow[] rpt = dt.Select();
                foreach(DataRow dr in rpt)
                {
                    for(int i = 0; i < ColCounter; i++)
                    {
                        excelWorksheet.Cells[row, i+col] = dr[i].ToString();
                    }
                    row++;
                }

                string tempSavedFileName = SavedFile + "." + Guid.NewGuid().ToString();

                FileInfo f = new FileInfo(tempSavedFileName);
                if(f.Exists)
                    f.Delete(); // delete the file if it already exist.
                // Workbook 저장
                excelWorkbook.SaveAs(tempSavedFileName, Excel.XlFileFormat.xlWorkbookNormal, null, null, false, false, Excel.XlSaveAsAccessMode.xlExclusive, false, false, null, null, null);
                // 클린업 작업
                excelWorkbook.Close(null, null, null);
                excelApp.Workbooks.Close();
                // excelApp.Application.Quit();
                excelApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelWorksheet);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelWorkbook);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp);
                excelWorksheet = null;
                excelWorkbook = null;
                excelApp = null;
                GC.Collect();

                if(downloadPage != null)
                    Download(downloadPage, SavedFile, tempSavedFileName);

                return true;

            }
            catch( Exception theException )
            {
                GC.Collect();
                throw theException;
            }
        }
        #endregion

        #region 템플릿 파일 사용 유형 2
        ///


        /// Array를 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을 열어
        /// Array에 저장된 키, 값 형태의 데이터를 엑셀 파일 Sheet의 해당 위치에 저장되는 엑셀 파일을 만든다.
        /// 엑셀 파일을 만든 뒤 다운로드를 수행하지 않는 오버로드
        ///

        /// 입력 데이타(Array)
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// 저장 성공 여부
        public static bool ExportExistFile(Hashtable ht, string OriginalFile, string SavedFile, string ws)
        {
            return PExportExistFile(ht, OriginalFile, SavedFile, ws, null);
        }

        ///


        /// Array를 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을 열어
        /// Array에 저장된 키, 값 형태의 데이터를 엑셀 파일 Sheet의 해당 위치에 저장되는 엑셀 파일을 만든다.
        /// 엑셀 파일을 만든 뒤 다운로드를 수행하는 오버로드
        ///

        /// 입력 데이타(Array)
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// 엑셀 파일 다운로드를 요청한 페이지
        /// 저장 성공 여부
        public static bool ExportExistFile(Hashtable ht, string OriginalFile, string SavedFile, string ws, Page downloadPage)
        {
            return PExportExistFile(ht, OriginalFile, SavedFile, ws, downloadPage);
        }

        ///


        /// Array를 입력 데이타로 받아서 해당 데이타로 기존 엑셀 파일(혹은 서식 파일)을 열어
        /// Array에 저장된 키, 값 형태의 데이터를 엑셀 파일 Sheet의 해당 위치에 저장되는 엑셀 파일을 만든다.
        ///

        /// 입력 데이타(Array)
        /// 기존 엑셀 파일 경로명과 파일명
        /// 저장 경로명과 파일명
        /// 데이타가 저장될 Sheet명
        /// 엑셀 파일 다운로드를 요청한 페이지
        /// 저장 성공 여부
        private static bool PExportExistFile(Hashtable ht, string OriginalFile, string SavedFile, string ws, Page downloadPage)
        {
            try
            {
                GC.Collect();// clean up any other excel guys hangin'' around...
                Excel.Application excelApp = new Excel.Application();
                Excel.Workbook excelWorkbook = excelApp.Workbooks.Open(OriginalFile, 0, false, 5, "", "", false, Excel.XlPlatform.xlWindows, "", true, false, 0, true, false, false);
                Excel.Sheets excelSheets = excelWorkbook.Worksheets;
                Excel.Worksheet excelWorksheet = (Excel.Worksheet)excelSheets.get_Item(ws);

    
                foreach(string str in ht.Keys)
                {
                    int x = str.IndexOf(":");
                    string col = str.Substring(0, x);
                    string row = str.Substring(x + 1, str.Length - x -1);
                    excelWorksheet.Cells[row, col] = ht[str];
     
                }
    
                FileInfo f = new FileInfo(SavedFile);

                if(f.Exists)
                    f.Delete(); // delete the file if it already exist.
                // Workbook 저장
                excelWorkbook.SaveAs(SavedFile, Excel.XlFileFormat.xlWorkbookNormal, null, null, false, false, Excel.XlSaveAsAccessMode.xlExclusive, false, false, null, null, null);
                // 클린업 작업
                excelWorkbook.Close(null, null, null);
                excelApp.Workbooks.Close();
                // excelApp.Application.Quit();
                excelApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelWorksheet);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelWorkbook);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp);
                excelWorksheet = null;
                excelWorkbook = null;
                excelApp = null;
                GC.Collect();

                if(downloadPage != null)
                {
                    Download(downloadPage, SavedFile);
                }

                return true;

            }
            catch( Exception theException )
            {
                GC.Collect();
                throw theException;
            }
        }
        #endregion

        #region 임시 생성 엑셀 파일 정리
        private static void DeleteTemporaryFiles()
        {
            // 임시로 생성한 엑셀 파일들 삭제
            if(exportedFiles.Count > 0)
            {
                ArrayList tempFiles = new ArrayList();

                foreach(FileInfo fi in exportedFiles)
                {
                    TimeSpan diffTimes = new TimeSpan( DateTime.Now.Ticks - fi.CreationTime.Ticks );

                    if(diffTimes.TotalMinutes > deleteMinutePeriod)
                    {
                        tempFiles.Add(fi);
                        fi.Delete();
                    }
                }
       
                foreach(FileInfo fi in tempFiles)
                    exportedFiles.Remove(fi);
            }
        }
       
        private static void AddExportedFileName(FileInfo fi)
        {
            exportedFiles.Add(fi);
        }
        #endregion

        #region 엑셀 파일 DB 업로드

        ///


        /// 엑셀로부터 데이터를 추출하여 DataSet으로 반환한다.
        ///

        /// 업로드할 엑셀 파일
        /// 엑셀 데이터셋
        public static DataSet GetDataSetFromExcelFile(System.Web.UI.HtmlControls.HtmlInputFile hif)
        {
            return GetDataSetFromExcelFile(hif, "Sheet1", true);
        }

        ///


        /// 엑셀로부터 데이터를 추출하여 DataSet으로 반환한다.
        ///

        /// 업로드할 엑셀 파일
        /// 엑셀 파일에 컬럼명 포함 여부(기본값: 포함)
        /// 엑셀 데이터셋
        public static DataSet GetDataSetFromExcelFile(System.Web.UI.HtmlControls.HtmlInputFile hif, bool IsIncludeHeader)
        {
            return GetDataSetFromExcelFile(hif, "Sheet1", IsIncludeHeader);
        }

        ///


        /// 엑셀로부터 데이터를 추출하여 DataSet으로 반환한다.
        ///

        /// 업로드할 엑셀 파일
        /// 업로드할 엑셀 파일내의 워크 쉬트명(기본값:Sheet1)
        /// 엑셀 파일에 컬럼명 포함 여부(기본값: 포함)
        /// 엑셀 데이터셋
        public static DataSet GetDataSetFromExcelFile(System.Web.UI.HtmlControls.HtmlInputFile hif, string WorkSheet, bool IsIncludeHeader)
        {
            string FilePath = Upload(hif);
            string ConnectionString1 = string.Empty;
            string excelConnStrFormat = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + FilePath + ";Extended Properties=\"Excel 8.0;HDR={0}\"";

            if(IsIncludeHeader == true)
                ConnectionString1 = string.Format(excelConnStrFormat, "YES");
            else
                ConnectionString1 = string.Format(excelConnStrFormat, "NO");

            OleDbConnection conn = new OleDbConnection(ConnectionString1);

            string strQuery;
            strQuery = "select * from [" + WorkSheet + "$]";
   
            OleDbCommand cmd = new OleDbCommand(strQuery, conn);
            OleDbDataAdapter Adapter = new OleDbDataAdapter();
            Adapter.SelectCommand = cmd;
            DataSet dsExcel = new DataSet();
            Adapter.Fill(dsExcel);

            return dsExcel;
        }

        ///


        /// 원하는 엑셀 파일의 원하는 워크쉬트를 지정한 DB의 테이블에 업로드한다.
        /// 이 경우 ht 파라미터를 통해 워크쉬트의 특정열을 테이블의 특정 컬럼에 매핑할 수 있다.
        /// 단, 이 경우 엑셀 파일의 컬럼에 대해서는 F1, F2, F3 ... 식으로 이름을 붙여야 한다.
        /// 그렇지 않고 Excel 커넥션 스트링의 HDR=YES로 설정하면 첫째 Row가 컬럼명으로 인식되게
        /// 할 수도 있다.
        ///

        /// 업로드할 엑셀 파일
        /// 업로드할 엑셀 파일내의 워크 쉬트명(기본값:Sheet1)
        /// 업로드할 DB 연결 문자열
        /// 업로드할 테이블명
        /// 엑셀 파일 컬럼명과 DB 테이블 컬럼명과의 매핑 정보
        /// 엑셀파일 DB업로드 성공 여부
        public static bool ExcelFileDBUpload(System.Web.UI.HtmlControls.HtmlInputFile hif, string WorkSheet, string ConnectionString, string Table, Hashtable ht)
        {
            return ExcelFileDBUpload(hif, WorkSheet, ConnectionString, Table, ht, true);
        }

        ///


        /// 원하는 엑셀 파일의 원하는 워크쉬트를 지정한 DB의 테이블에 업로드한다.
        /// 이 경우 ht 파라미터를 통해 워크쉬트의 특정열을 테이블의 특정 컬럼에 매핑할 수 있다.
        /// 단, 이 경우 엑셀 파일의 컬럼에 대해서는 F1, F2, F3 ... 식으로 이름을 붙여야 한다.
        /// 그렇지 않고 Excel 커넥션 스트링의 HDR=YES로 설정하면 첫째 Row가 컬럼명으로 인식되게
        /// 할 수도 있다.
        ///

        /// 업로드할 엑셀 파일
        /// 업로드할 DB 연결 문자열
        /// 업로드할 테이블명
        /// 엑셀 파일 컬럼명과 DB 테이블 컬럼명과의 매핑 정보
        /// 엑셀파일 DB업로드 성공 여부
        public static bool ExcelFileDBUpload(System.Web.UI.HtmlControls.HtmlInputFile hif, string ConnectionString, string Table, Hashtable ht)
        {
            return ExcelFileDBUpload(hif, "Sheet1", ConnectionString, Table, ht, true);
        }

        ///


        /// 원하는 엑셀 파일의 원하는 워크쉬트를 지정한 DB의 테이블에 업로드한다.
        /// 이 경우 ht 파라미터를 통해 워크쉬트의 특정열을 테이블의 특정 컬럼에 매핑할 수 있다.
        /// 단, 이 경우 엑셀 파일의 컬럼에 대해서는 F1, F2, F3 ... 식으로 이름을 붙여야 한다.
        /// 그렇지 않고 Excel 커넥션 스트링의 HDR=YES로 설정하면 첫째 Row가 컬럼명으로 인식되게
        /// 할 수도 있다.
        ///

        /// 업로드할 엑셀 파일
        /// 업로드할 DB 연결 문자열
        /// 업로드할 테이블명
        /// 엑셀 파일 컬럼명과 DB 테이블 컬럼명과의 매핑 정보
        /// 엑셀 파일에 컬럼명 포함 여부(기본값: 포함)
        /// 엑셀파일 DB업로드 성공 여부
        public static bool ExcelFileDBUpload(System.Web.UI.HtmlControls.HtmlInputFile hif, string ConnectionString, string Table, Hashtable ht, bool IsIncludeHeader)
        {
            return ExcelFileDBUpload(hif, "Sheet1", ConnectionString, Table, ht, IsIncludeHeader);
        }

        ///


        /// 원하는 엑셀 파일의 원하는 워크쉬트를 지정한 DB의 테이블에 업로드한다.
        /// 이 경우 ht 파라미터를 통해 워크쉬트의 특정열을 테이블의 특정 컬럼에 매핑할 수 있다.
        /// 단, 이 경우 엑셀 파일의 컬럼에 대해서는 F1, F2, F3 ... 식으로 이름을 붙여야 한다.
        /// 그렇지 않고 Excel 커넥션 스트링의 HDR=YES로 설정하면 첫째 Row가 컬럼명으로 인식되게
        /// 할 수도 있다.
        ///

        /// 업로드할 엑셀 파일
        /// 업로드할 엑셀 파일내의 워크 쉬트명(기본값:Sheet1)
        /// 업로드할 DB 연결 문자열
        /// 업로드할 테이블명
        /// 엑셀 파일 컬럼명과 DB 테이블 컬럼명과의 매핑 정보
        /// 엑셀 파일에 컬럼명 포함 여부(기본값: 포함)
        /// 엑셀파일 DB업로드 성공 여부
        public static bool ExcelFileDBUpload(System.Web.UI.HtmlControls.HtmlInputFile hif, string WorkSheet, string ConnectionString, string Table, Hashtable ht, bool IsIncludeHeader)
        {

            //   dsExcel = SqlHelper.ExecuteDataset(ConnectionString1, CommandType.Text, strQuery);
            //   SqlHelper.FillDataset(ConnectionString1, CommandType.Text, strQuery, dsExcel, Table);

            DataSet dsExcel = GetDataSetFromExcelFile(hif, WorkSheet, IsIncludeHeader);

            string ConnectionString2 = ConnectionString;
            SqlConnection conn2 = new SqlConnection(ConnectionString2);
            string strQuery2;
            strQuery2 = "select * from " + Table;
            SqlCommand cmd2 = new SqlCommand(strQuery2, conn2);
            SqlDataAdapter adpt = new SqlDataAdapter();
            adpt.SelectCommand = cmd2;
   
            SqlCommandBuilder custCB = new SqlCommandBuilder(adpt);

            DataSet dsDB = new DataSet();
            adpt.FillSchema(dsDB, SchemaType.Mapped);
   
            foreach(DataRow excelRow in dsExcel.Tables[0].Rows)
            {
                DataRow dbRow = dsDB.Tables[0].NewRow();
                foreach(string excelCol in ht.Keys)
                {
                    string DBCol = ht[excelCol].ToString();
                    dbRow[DBCol] = excelRow[excelCol];
                }
                dsDB.Tables[0].Rows.Add(dbRow);
            }

            DataSet dsUpload = dsDB.GetChanges();
            int result = adpt.Update(dsUpload);
            if(dsUpload.Tables[0].Rows.Count == result)
                return true;
            else
                return false;

        }
        #endregion
  
        #region 엑셀 파일 Download
        ///


        /// 생성된 엑셀 파일을 다운로드를 수행하는 로직
        ///

        /// 저장된 엑셀파일의 다운로드를 요청한 페이지
        /// 다운로드시 보여줄 엑셀파일 경로명과 파일명
        /// 서버에 저장된 임시 엑셀파일 경로명과 파일명
        private static void Download(Page downloadPage, string excelfile, string tempSavedFileName)
        {
            using(System.Web.UI.Page dp = downloadPage)
            {
                FileInfo fi = new FileInfo(tempSavedFileName);
                AddExportedFileName(fi);
                dp.Response.Clear();
                dp.Response.AddHeader("Content-Disposition", "attachment; filename=" + Path.GetFileName(excelfile));
                dp.Response.AddHeader("Content-Length", fi.Length.ToString());
                dp.Response.ContentType = "application/vnd.ms-exce";
                dp.Response.Charset = "";
                dp.EnableViewState = false;
                dp.Response.CacheControl = "public";
                dp.Response.WriteFile(fi.FullName);
                dp.Response.End();
            }
        }

        ///


        /// 생성된 엑셀 파일을 다운로드를 수행하는 로직
        ///

        /// 저장된 엑셀파일의 다운로드를 요청한 페이지
        /// 다운로드시 보여줄 엑셀파일 경로명과 파일명
        private static void Download(Page downloadPage, string excelfile)
        {
            using(System.Web.UI.Page dp = downloadPage)
            {
                FileInfo fi = new FileInfo(excelfile);
                dp.Response.Clear();
                dp.Response.AddHeader("Content-Disposition", "attachment; filename=" + Path.GetFileName(excelfile));
                dp.Response.AddHeader("Content-Length", fi.Length.ToString());
                dp.Response.ContentType = "application/vnd.ms-exce";
                dp.Response.Charset = "";
                dp.EnableViewState = false;
                dp.Response.CacheControl = "public";
                dp.Response.WriteFile(fi.FullName);
                dp.Response.End();
            }
        }
        #endregion

        #region 엑셀 파일 Upload
        private static string Upload(System.Web.UI.HtmlControls.HtmlInputFile hif)
        {

            if(hif.PostedFile.FileName.Length == 0)
            {
                throw new Exception("업로드할 파일이 없습니다.");
            }
            if(hif.PostedFile.ContentLength == 0)
            {
                throw new Exception("업로드할 파일의 크기가 0바이트입니다.");
            }

            string UploadPath = Path.GetTempPath();
   
            if(hif.PostedFile != null)
            {
                string fileName = Path.GetFileName(hif.PostedFile.FileName.ToString());
                string uniqueFilePath = GetUniqueFileNameWithPath(UploadPath, fileName);
                hif.PostedFile.SaveAs(uniqueFilePath);
                return uniqueFilePath;
            }
            else
            {
                throw new ApplicationException("업로드할 파일이 없습니다.");
            }
        }

        private static string GetUniqueFileNameWithPath(string dirPath, string fileN)
        {
            string fileName = fileN;

            int indexOfDot = fileName.LastIndexOf(".");
            string strName = fileName.Substring(0, indexOfDot);
            string strExt = fileName.Substring(indexOfDot+1);

            bool bExist = true;
            int fileCount = 0;

            while(bExist)
            {
                if(File.Exists(Path.Combine(dirPath, fileName)))
                {
                    fileCount++;
                    fileName = strName + "(" + fileCount + ")." +strExt;
                }
                else
                {
                    bExist = false;
                }
            }

            return Path.Combine(dirPath, fileName);
        }
        #endregion
    }
}

Posted by SB패밀리

웹 다운로드

http://www.microsoft.com/korea/msdn/msdnmag/issues/06/09/WebDownloads/#S1

웹 응용 프로그램에서 보다 효율적인 ASP.NET 파일 다운로드 구축


<INPUT title="Print a printer-friendly version of this page" onclick="return OnPrintPage();" type=image alt="Print a printer-friendly version of this page" src="http://www.microsoft.com/korea/msdn/msdnmag/images/dingbats/rtg_btn_print.gif" border=0>

이 문서 내용:
  • ASP.NET 사이트에서 동적 다운로드
  • 즉석에서 링크 생성
  • 다시 시작 가능한 다운로드 및 사용자 지정 처리기
  • 사용자 지정 다운로드 메커니즘과 연관된 보안 개념
이 문서에는 다음 기술이 사용됩니다:
ASP.NET


Code download available at:
DesignerHosting.exe (174KB)

목차
기본적인 다운로드 링크
모든 파일 형식을 강제로 다운로드
큰 파일을 작은 조각으로 다운로드
더 나은 솔루션
실패한 다운로드 다시 시작


보충기사
System.Security

조직의 웹 사이트에서 사용자에게 파일 다운로드를 제공하는 경우가 많을 것입니다. 그리고 다운로드를 제공하는 것은 링크를 제공하는 것만큼 쉽기 때문에 이들 방법에 대한 기사를 읽을 필요성도 느끼지 못할 것입니다. 그러나 웹 기능이 워낙 다양하게 발전함에 따라 다운로드를 제공하는 것이 그렇게 쉽지만은 않습니다. 우선 파일을 브라우저에서 콘텐츠로 표시하기보다는 파일로 다운로드하기를 원할 수 있습니다. 또는 아직 파일 경로를 모르거나 파일이 디스크에 없기 때문에 단순한 HTML 링크로는 작업이 불가능한 경우도 있습니다. 또한 사용자가 대용량 파일을 다운로드하는 동안 연결이 끊어질 수 있다는 것도 고려해야 합니다.


이 기사에서는 사용자에게 보다 빠르고 오류 없는 다운로드 환경을 제공할 수 있도록 이러한 문제에 대한 몇 가지 해결책을 제시합니다. 또한 동적으로 생성된 링크에 대해 알아보고, 기본 파일 동작을 우회하는 방법을 설명하며, HTTP 1.1 기능을 사용하는 다시 시작 가능한 ASP.NET 기반 다운로드를 보여 줄 것입니다.


기본적인 다운로드 링크

먼저 링크 누락 문제를 살펴보겠습니다. 파일에 대한 경로를 미리 알 수 없는 경우에는 간단히 나중에 데이터베이스에서 링크 목록을 가져올 수 있습니다. 또는 선택한 디렉터리의 파일을 런타임에 동적으로 열거하여 링크 목록을 작성할 수도 있습니다. 여기서는 두 번째 방법에 대해 좀 더 자세히 설명합니다.


그림 1에서 보여 주는 것처럼 Visual Basic 2005에서 DataGrid를 작성하고 다운로드 디렉터리에 있는 모든 파일에 대한 링크로 채우는 과정을 살펴보겠습니다. 이를 위해서 해당 페이지에서 Server.MapPath를 사용하여 다운로드 디렉터리(이 경우 ./downloadfiles/)에 대한 전체 경로를 검색하고, DirectoryInfo.GetFiles를 사용하여 해당 디렉터리의 모든 파일 목록을 가져온 다음, 만들어진 FileInfo 개체의 배열을 사용하여 각 관련 속성에 대한 열이 포함된 DataTable을 구성할 수 있습니다. 이 DataTable은 해당 페이지의 DataGrid에 바인딩할 수 있으며 이를 통해 HyperLinkColumn 정의를 사용하여 다음과 같이 링크를 생성할 수 있습니다.

<asp:HyperLinkColumn DataNavigateUrlField="Name"  
    DataNavigateUrlFormatString="downloadfiles/{0}" 
    DataTextField="Name" 
    HeaderText="File Name:" 
    SortExpression="Name" />

이 링크를 클릭해보면 등록된 도우미 응용 프로그램에 따라 브라우저가 각 파일 형식을 다르게 처리하는 것을 알 수 있습니다. 기본적으로 .asp 페이지, .html 페이지, .jpg, .gif 또는 .txt를 클릭하면 브라우저 자체에서 파일이 열리며 [다른 이름으로 저장] 대화 상자는 나타나지 않습니다. 그 이유는 이러한 파일 확장명이 알려진 MIME 형식이기 때문입니다. 이러한 경우는 브라우저 자체에서 파일을 렌더링하는 방법을 알고 있거나 운영 체제에 해당 브라우저가 사용할 도우미 응용 프로그램이 포함된 경우입니다. 웹캐스트(.wmv, .avi 등), PodCast(.mp3 또는 .wma), PowerPoint 파일 및 모든 Microsoft Office 문서는 알려진 MIME 형식이며 이러한 파일을 기본적으로 인라인으로 열지 않으려고 하면 문제가 발생합니다.


그림 1 DataGrid의 단순 HTML 링크

이러한 방식의 다운로드를 사용하는 경우에는 매우 일반적인 액세스 제어 메커니즘만 선택할 수 있습니다. 디렉터리별로 다운로드 액세스를 제어할 수 있지만 개별 파일 또는 파일 형식에 대한 액세스를 제어하기 위해서는 세부적인 액세스 제어가 필요하므로 웹 마스터나 시스템 관리자의 작업이 상당히 많이 늘어납니다. 다행히도 ASP.NET과 .NET Framework는 다음과 같은 다양한 솔루션을 제공합니다.

  • Response.WriteFile 메서드 사용
  • Response.BinaryWrite 메서드를 사용한 파일 스트리밍
  • ASP.NET 2.0의 Response.TransferFile 메서드 사용
  • SAPI 필터 사용
  • 사용자 지정 브라우저 컨트롤에 쓰기


페이지 맨 위로

모든 파일 형식을 강제로 다운로드

방금 나열한 솔루션 중 가장 쉽게 사용할 수 있는 솔루션이 Response.WriteFile 메서드입니다. 기본 구문은 매우 단순합니다. 다음 ASPX 페이지는 쿼리 문자열 매개 변수로 지정된 파일 경로를 찾아 해당 파일을 클라이언트에 제공합니다.

<%@ Page language="VB" AutoEventWireup="false" %>

<html>
   <body>
        <%
            If Request.QueryString("FileName") Then
                Response.Clear()
                Response.WriteFile(Request.QueryString("FileName"))
                Response.End()
            End If
        %>

  </body>
</html>

IIS 작업자 프로세스(IIS 5.0의 aspnet_wp.exe 또는 IIS 6.0의 w3wp.exe)에서 실행 중인 코드가 Response.Write를 호출하면 ASP.NET 작업자 프로세스가 IIS 프로세스(inetinfo.exe 또는 dllhost.exe)로 데이터를 보내기 시작합니다. 작업자 프로세스에서 IIS 프로세스로 보낸 데이터는 메모리에 버퍼링됩니다. 대부분의 경우 이는 큰 문제가 되지 않지만 매우 큰 파일의 경우에는 그리 좋은 솔루션이 아닙니다.


이 솔루션의 장점은 해당 파일을 전송하는 HTTP 응답을 ASP.NET 코드에서 만들었기 때문에 모든 ASP.NET 인증 및 권한 부여 메커니즘에 대한 완전한 액세스가 가능하게 되어 인증 상태, 런타임 시 Identity 및 Principal 개체의 존재 여부, 필요한 기타 다른 메커니즘 등에 따라 의사 결정을 내릴 수 있다는 것입니다.


따라서 기본 제공 ASP.NET 사용자 및 그룹 메커니즘, 권한 부여 관리자 및 정의된 역할 그룹과 같은 Microsoft 서버 추가 기능, ADAM(Active Directory Application Mode) 또는 Active Directory와 같은 기존 보안 메커니즘을 통합하여 다운로드 권한에 대한 세부적인 제어를 제공할 수 있습니다.


또한 응용 프로그램 코드 내부에서 다운로드를 시작하면 알려진 MIME 형식에 대한 기본 동작을 바꿀 수 있습니다. 이를 위해서는 표시된 링크를 변경해야 합니다. 다음은 ASPX 페이지에서 제공할 하이퍼링크를 작성하는 코드입니다.

<!-- FileFetch.aspx의 DataGrid 정의 부분 -- >
<asp:HyperLinkColumn DataNavigateUrlField="Name"          
    DataNavigateUrlFormatString="FileFetch.aspx?FileName={0}" 
    DataTextField="Name" 
    HeaderText="File Name:" 
    SortExpression="Name" />


다음으로, 요청이 클라이언트의 브라우저로 전송할 filename 인수를 포함하는 다시 게시 작업인지 확인하기 위해 페이지가 요청될 때 쿼리 문자열을 확인해야 합니다(그림 2 참조). 이제 표에 포함된 링크 중 하나를 클릭하면 Content-Disposition 응답 헤더 덕분에 MIME 형식에 관계없이 저장 대화 상자가 나타납니다(그림 3 참조). 또한 IsSafeFileName라는 메서드를 호출하여 그 결과에 따라 다운로드 가능한 파일을 제한한 것을 확인할 수 있습니다. 이렇게 하는 이유와 이 메서드의 목적에 대한 자세한 내용은 보충 기사 "Unintended File Access"를 참조하십시오.


그림 3 파일 다운로드 대화 상자 강제 표시

이 기술을 사용할 때는 다운로드하는 파일의 크기를 중요하게 고려해야 합니다. 파일 크기를 제한해야 하며 그렇지 않으면 서비스 거부 공격에 사이트가 노출됩니다. 리소스에서 허용하는 것보다 큰 파일을 다운로드하려는 경우 해당 페이지를 표시할 수 없다는 런타임 오류가 발생하거나 다음과 같은 오류가 표시됩니다.

서버 응용 프로그램을 사용할 수 없습니다.

이 웹 서버에서 액세스하려는 
웹 응용 프로그램을 현재 사용할 수 
없습니다. 
웹 브라우저의 [새로 고침] 단추를 눌러 
다시 요청하십시오.

관리자 참고 사항: 특정 요청 오류의 원인을 
자세하게 설명하는 오류 메시지가 웹 서버의 
응용 프로그램 이벤트 로그에 있습니다. 
오류가 발생한 원인을 알아내려면 
이 로그 엔트리를 검토하십시오. 

다운로드 가능한 파일의 최대 크기를 결정하는 한 가지 요인은 서버의 하드웨어 구성 및 런타임 상태입니다. 이 문제를 해결하려면 기술 자료 문서 "FIX: Downloading Large Files Causes a Large Memory Loss and Causes the Aspnet_wp.exe Process to Recycle"(support.microsoft.com/kb/823409)을 참조하십시오.


이 방식을 사용하면 비디오와 같은 대용량 파일을 다운로드할 때, 특히 Windows 2000에서 IIS 5.0을 실행하거나 Windows Server 2003에서 호환 모드로 IIS 6.0을 실행하는 웹 서버에서 문제가 나타날 수 있습니다. 클라이언트가 다운로드하기 전에 파일을 서버 메모리로 로드해야 하기 때문에 이 문제는 최소 메모리를 사용하여 구성된 웹 서버에서 더욱 심해집니다.


필자의 테스트 시스템에서 확인한 결과에 따르면 2GB RAM에서 IIS 5.0을 실행하는 서버는 파일 크기가 200MB 정도가 되면 다운로드에 실패합니다. 프로덕션 환경에서 동시에 다운로드하는 사용자가 많아지면 서버의 메모리 제약이 심해져서 사용자가 다운로드를 할 수 없게 됩니다. 이 문제는 몇 줄의 간단한 코드로 해결할 수 있습니다.


페이지 맨 위로

큰 파일을 작은 조각으로 다운로드

이전 코드 샘플의 문제는 한 번의 Response.WriteFile 호출로 전체 원본 파일을 메모리로 버퍼링하기 때문에 발생하는 것입니다. 큰 파일을 처리하는 보다 좋은 방법은 그림 4의 예에서 보여 주는 것처럼 파일을 작고 관리하기 편한 청크로 읽어서 클라이언트에 전송하는 것입니다. 이 버전의 Page_Load 이벤트 처리기는 while 루프를 사용하여 파일을 한 번에 10,000바이트씩 읽고 이 청크를 브라우저로 전송합니다. 따라서 런타임에 파일의 일부만 메모리에 유지됩니다. 청크 크기는 현재 상수로 설정되어 있지만 프로그램에서 수정할 수 있습니다. 또는 이를 구성 파일로 옮겨서 서버 제약과 성능 요구에 맞도록 변경할 수 있습니다. 이 코드를 최대 1.6GB 크기의 파일로 테스트한 결과 다운로드가 빠르게 수행되었으며 서버 메모리도 대량으로 사용되지 않았습니다.


2GB보다 큰 파일의 다운로드는 IIS 자체에서 지원하지 않습니다. 이보다 큰 파일을 다운로드해야 하는 경우에는 FTP, 타사 컨트롤, Microsoft BITS(Background Intelligent Transfer Service)를 사용하거나 소켓을 통해 브라우저가 호스팅하는 사용자 지정 컨트롤로 데이터를 스트리밍하는 방법과 같은 사용자 지정 솔루션을 사용해야 합니다.


페이지 맨 위로

더 나은 솔루션

ASP.NET 개발 팀에서는 공통적인 파일 다운로드 요구 사항과 대체적으로 계속 증가하는 파일 크기를 감안하여 파일을 브라우저로 전송하기 전에 메모리에 버퍼링하지 않고 해당 파일을 다운로드하는 특수한 메서드를 ASP.NET에 추가하였습니다. ASP.NET 2.0에서 사용할 수 있는 Response.TransmitFile이 바로 그 메서드입니다.


TransmitFile은 WriteFile처럼 사용할 수 있으며 일반적으로 더 나은 성능 특성을 보여 줍니다. TransmitFile에는 또한 추가 기능이 포함되어 있습니다. 그림 5의 코드를 살펴보면 TransmitFile에 새로 추가된 몇 가지 기능을 사용하여 앞서 설명한 메모리 사용 문제를 해결하였음을 알 수 있습니다.


단지 몇 줄의 코드를 추가하여 보안 및 내결함성을 보완할 수 있었습니다. 먼저 요청된 파일의 확장명을 사용하여 MIME 형식을 확인하고, Response 개체의 "ContentType" 속성을 설정함으로써 요청된 MIME 형식을 HTTP 헤더에 지정하여 몇 가지 보안 및 논리 제약 조건을 추가했습니다.


Response.ContentType = "application/x-zip-compressed"

이 방법으로 특정 콘텐츠 유형만 다운로드하도록 제한하고 서로 다른 파일 확장명을 단일 콘텐츠 유형으로 매핑할 수 있었습니다. Content-Disposition 헤더를 추가하는 문도 주의하여 살펴보기 바랍니다. 이 문을 사용하여 서버의 하드 디스크에 있는 원래 파일 이름과 구별되도록 다운로드할 파일 이름을 지정할 수 있었습니다.


이 코드에서는 원래 이름에 접두사를 붙여 새 파일 이름을 만들었습니다. 여기서는 접두사가 정적이지만 다운로드하는 파일 이름이 사용자 하드 디스크의 기존 파일 이름과 충돌하지 않도록 동적으로 접두사를 만들 수 있습니다.


그러나 대용량 파일을 가져오는 도중 중단된 경우는 어떻게 될까요? 다운로드가 실패할까요? 이 코드는 단순 다운로드 링크에 비하면 많이 발전했지만 실패한 다운로드를 매끄럽게 처리하여 서버에서 클라이언트로 일부만 전송된 다운로드를 다시 시작하는 기능은 아직 제공하지 못하고 있습니다. 지금까지 살펴본 모든 솔루션은 오류가 발생하면 처음부터 다시 다운로드를 시작해야 합니다.


페이지 맨 위로

실패한 다운로드 다시 시작

실패한 다운로드 다시 시작이라는 과제를 해결하기 위해 전송할 파일을 청크로 만드는 코드를 직접 작성하는 방식을 다시 살펴보겠습니다. 직접 코드를 작성하는 경우 TransmitFile 메서드를 사용하는 코드만큼 간단하지는 않지만 파일을 청크로 읽고 전송할 수 있다는 장점이 있습니다. 특정 시점에 런타임 상태에는 이미 클라이언트로 전송한 바이트 수에 대한 정보가 포함되어 있으므로 전체 파일 크기에서 이를 빼면 전송할 남은 바이트 수를 알 수 있습니다.


코드를 다시 살펴보면 읽기/전송 루프에서 Response.IsClientConnected의 결과를 루프 조건으로 확인하는 것을 볼 수 있습니다. 이 테스트는 클라이언트의 연결이 끊어지면 해당 전송을 일시 중단하도록 만듭니다. 이 테스트에서 거짓(파일 다운로드를 시작한 웹 브라우저가 더 이상 연결되지 않음)이 반환되면 첫 번째 루프 반복에서 서버는 데이터 전송을 중단하고 해당 파일을 완성하는 데 필요한 남은 바이트를 기록합니다. 또한 사용자가 실패한 다운로드를 완료하려는 경우를 대비하여 클라이언트가 받은 부분 파일을 저장할 수 있습니다.


다시 시작 가능한 다운로드 솔루션의 나머지 부분은 HTTP 1.1 프로토콜의 잘 알려지지 않은 몇 가지 기능을 사용하여 해결할 수 있습니다. HTTP의 비저장 특성은 일반적으로 웹 개발자에게는 장애가 되지만 이 경우에는 이러한 HTTP 사양이 오히려 큰 도움이 됩니다. 특히 HTTP 1.1 헤더에는 이 작업과 관련이 있는 Accept-Ranges 및 Etag라는 두 가지 요소가 있습니다.


Accept-Ranges 헤더 요소는 간단하게 클라이언트(이 경우 웹 브라우저)에게 이 프로세스가 다시 시작 가능한 다운로드를 지원함을 알려 줍니다. Entity Tag 또는 Etag 요소는 세션에 고유 식별자를 지정합니다. 따라서 ASP.NET 응용 프로그램이 다시 시작 가능한 다운로드를 시작하기 위해 브라우저로 보내는 HTTP 헤더는 다음과 비슷한 형태가 됩니다.

HTTP/1.1 200 OK
Connection: close
Date: Mon, 22 May 2006 11:09:13 GMT
Accept-Ranges: bytes
Last-Modified: Mon, 22 May 2006 08:09:13 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 39551221

브라우저는 ETag 및 Accept-Ranges 헤더를 통해 웹 서버가 다시 시작 가능한 다운로드를 지원한다는 사실을 알 수 있습니다.


다운로드가 실패한 경우 파일을 다시 요청하면 Internet Explorer가 ETag, 파일 이름 및 중단되기 전까지 성공적으로 다운로드한 양을 나타내는 값 범위를 보내므로 웹 서버(IIS)가 다운로드를 다시 시작할 수 있습니다. 다음은 이러한 두 번째 요청의 예입니다.

GET http://192.168.0.1/download.zip HTTP/1.0
Range: bytes=933714-
Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMT
If-Range: "58afcc3dae87d52:3173"

If-Range 요소에는 서버가 다시 보낼 파일을 식별하는 데 사용할 수 있는 원래 ETag 값이 포함되어 있습니다. 또한 Unless-Modified-Since 요소에는 원래 다운로드를 시작했던 날짜와 시간이 포함되어 있습니다. 서버는 이 정보를 사용하여 원래 다운로드를 시작한 이후 해당 파일이 수정되었는지 여부를 판단합니다. 파일이 수정된 경우 서버는 처음부터 다시 파일을 다운로드합니다.


헤더에 있는 Range 요소는 파일을 완성하는 데 필요한 바이트 수를 서버에 알려 줍니다. 서버는 이 정보를 사용하여 부분적으로 다운로드된 파일에서 다시 시작해야 하는 위치를 결정합니다.


다른 브라우저에서는 이러한 헤더를 약간 다르게 사용합니다. 클라이언트가 파일을 고유하게 식별하기 위해 보낼 수 있는 다른 HTTP 헤더에는 If-Match, If-Unmodified-Since 및 Unless-Modified-Since가 있습니다. HTTP 1.1은 클라이언트가 어떤 헤더를 지원해야 하는지 세부적으로 지정하지 않고 있습니다. 따라서 일부 웹 브라우저에서 이러한 HTTP 헤더를 지원하지 않을 수도 있고 또 어떤 웹 브라우저에서는 Internet Explorer에서 사용하지 않는 다른 헤더를 사용할 수도 있습니다.


기본적으로 IIS는 다음과 같은 헤더 집합을 포함합니다.

HTTP/1.1 206 Partial Content
Content-Range: bytes 933714-39551221/39551222
Accept-Ranges: bytes
Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 2021408

이 헤더 집합에는 원래 요청과 다른 응답 코드가 포함되어 있습니다. 원래 응답에는 코드 200이 포함되어 있지만 이 요청은 다운로드 다시 시작을 나타내는 206 응답 코드를 사용합니다. 이것은 클라이언트에게 앞으로 보낼 데이터가 완전한 파일이 아니라 ETag에 식별되어 있는 파일에 대해 이전에 시작했던 다운로드를 계속하려는 것임을 알려 줍니다.

일부 웹 브라우저는 파일 이름 자체를 사용하지만 Internet Explorer는 매우 특별하게 ETag 헤더를 요구합니다. ETag 헤더에 초기 다운로드 응답 또는 다운로드 다시 시작과 관련된 정보가 없으면 Internet Explorer는 다운로드를 다시 시작하지 않고 새로운 다운로드를 시작합니다.

ASP.NET 다운로드 응용 프로그램에서 다시 시작 가능한 다운로드 기능을 구현하려면 브라우저에서 요청(다운로드 다시 시작용)을 가로챈 다음 해당 요청에 있는 HTTP 헤더를 사용하여 ASP.NET 코드로 적절한 응답을 구성할 수 있어야 합니다. 이렇게 하려면 일반 처리 순서에 약간 앞서 해당 요청을 수신해야 합니다.

바로 이때 .NET Framework가 도움이 됩니다. 이것은 개발자가 일상적으로 수행하는 표준적인 작업을 위한 기능을 잘 구성된 개체 라이브러리로 제공한다는 .NET의 기본적인 디자인 전제를 잘 보여 주는 예입니다.

이 경우에는 .NET Framework의 System.Web 네임스페이스에서 제공하는 IHttpHandler 인터페이스를 활용하여 사용자 지정 HTTP 처리기를 작성할 수 있습니다. IHttpHandler를 구현하는 클래스를 만들면 단순히 IIS에서 기본 동작을 사용하여 응답하는 것이 아니라 특정 파일 형식에 대한 웹 요청을 가로채어 사용자가 작성한 코드에서 이러한 요청에 응답할 수 있습니다.

이 기사의 코드 다운로드에는 다시 시작 가능한 다운로드를 지원하는 HTTP 처리기의 실제 구현이 포함되어 있습니다. 이 기능에는 상당히 많은 코드가 필요하며 구현하려면 HTTP 메커니즘을 이해해야 하지만 .NET Framework를 통해 상대적으로 단순하게 구현할 수 있습니다. 이 솔루션을 사용하면 매우 큰 파일을 다운로드할 수 있으며 다운로드 시작한 후에도 탐색을 계속할 수 있습니다. 하지만 제어가 불가능한 특정 인프라와 관련하여 고려해야 할 사항이 몇 가지 있습니다.

예를 들어 자체 캐싱 메커니즘을 유지 관리하는 기업 및 인터넷 서비스 공급자가 많이 있습니다. 이 경우 웹 캐시 서버가 손상되거나 잘못 구성되면 파일이 손상되거나 세션이 중간에 종료되어 대용량 다운로드가 실패할 수 있습니다. 특히 파일 크기가 255MB를 넘는 경우가 여기에 해당합니다.

255MB가 넘는 파일을 다운로드해야 하거나 다른 사용자 지정 기능이 필요한 경우 사용자 지정 또는 타사 다운로드 관리자를 사용할 것을 고려해 볼 수 있습니다. 예를 들어 다운로드를 관리하는 사용자 지정 브라우저 컨트롤 또는 브라우저 도우미 함수를 작성하거나 BITS를 활용하거나 사용자 지정 코드에서 파일 요청을 FTP 클라이언트로 넘겨줄 수 있습니다. 이러한 옵션은 끝이 없으며 특정 요구에 따라 수정할 수 있습니다.

.NET Framework와 ASP.NET은 두 줄의 코드를 통한 대용량 파일 다운로드부터 사용자 지정 보안 및 세그먼트 처리를 갖춘 다시 시작 가능한 다운로드까지 웹 사이트 사용자에게 가장 적합한 다운로드 환경을 구축하기 위한 다양한 옵션을 제공합니다.



Joe Stagner는 Microsoft의 .NET Client Team에서 테스트 업무를 수행하는 소프트웨어 디자인 엔지니어로 디자이너 호스트 및 기타 디자이너 기능을 담당하고 있습니다. Dinesh Chandnani는 아리조나 대학에서 컴퓨터 공학 석사 과정을 마쳤으며 2002년부터 Microsoft에서 근무하고 있습니다.

 

===============================================================

Figure 1 

There are a number of security-related issues to consider when dynamically generating a list of files to be made available for download. First, because you’re building the list of files dynamically at run time, all the files in the download directory will become available for the user to download whether you want them to or not. And displaying only some of the files in your download directory will not solve the problem. Users could still download all the files in the download directory by simply specifying the desired file name on the query string, even if the files are not displayed on your Web page. In order to restrict access to specific files in the download directory you would need to use ACLs, implement a roles-based authorization mechanism, or support some other filtering mechanism. They are beyond the scope of this article, but if you decide to use these kinds of controls, lots of information is available on MSDN online.
Another problem that needs to be addressed are canonicalization attacks. Michael Howard and David LeBlanc have an entire chapter dedicated to the subject in their book Writing Secure Code (Microsoft Press, 2003), so a few paragraphs here can’t do the topic justice. In short, however, canonicalization attacks are possible due to the existence of multiple, equivalent forms of a name for a single resource. For example, characters in a URL (in our case, a query string parameter) can be represented as standard ASCII characters, but they can also be represented with hexadecimal escape codes, HTML escape sequences, and more. And special sequences of characters (such as "..\") can be used to escape from the supported directory, allowing an attacker to retrieve files from elsewhere on the file system. To combat these types of attacks, one common solution is to use regular expressions to restrict the input that is accepted. For example, you’ll notice in most of the download examples in this article that I make use of a function named IsSafeFileName. This is a simple function that uses a regular expression to ensure that the specified file name is simply a series of up to 16 alphanumeric characters, followed by a dot, followed by a supported extension. Any other paths provided by a client (possibly a malicious attacker) will be rejected:
Private Function IsSafeFileName(ByVal fileName As String) As Boolean
    Return Not String.IsNullOrEmpty(fileName) AndAlso _
           Regex.IsMatch(fileName, _
                         "^\w{1,16}\.(asp|gif|htm|jpg|rtf|txt)$")
End Function

As is used in the sample code, IsSafeFileName rejects any provided file names that don’t conform to this pattern. This is a very important concept. When implementing similar systems, don’t attempt to look for and reject invalid data, allowing everything else to be accepted; there are so many types of attacks out there, chances are good that you’ll miss something that may come back to haunt you later. Instead, look for what should be allowed, and reject everything else.


Figure 2 

Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
    Dim dlDir As String = "downloadfiles/"
    Dim strFileName As String = Request.QueryString("FileName")
    Dim path As String = Server.MapPath( _
        dlDir + Request.QueryString("FileName"))
    Dim toDownload As System.IO.FileInfo = New System.IO.FileInfo(path)

    If IsSafeFileName(strFileName) AndAlso toDownload.Exists Then
        Response.Clear()
        Response.AddHeader("Content-Disposition", _
                           "attachment; filename=" & toDownload.Name)
        Response.AddHeader("Content-Length", _
                           file.Length.ToString())
        Response.ContentType = "application/octet-stream"
        Response.WriteFile(file.FullName)
        Response.End()
    Else
        BindFileDataToGrid("Name")
    End If
End Sub

Figure 4 

Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
    Dim dlDir As String = "downloadfiles/"
    Dim strFileName As String = Request.QueryString("FileName")
    Dim path As String = Server.MapPath( _
        dlDir + Request.QueryString("FileName"))
    Dim toDownload As System.IO.FileInfo = New System.IO.FileInfo(path)

    If IsSafeFileName(strFileName) AndAlso toDownload.Exists Then
        Const ChunkSize As Long = 10000
        Dim buffer(ChunkSize) As Byte
            
        Response.Clear()
        Using iStream As FileStream = File.OpenRead(path)
            Dim dataLengthToRead As Long = iStream.Length
            Response.ContentType = "application/octet-stream"
            Response.AddHeader("Content-Disposition", _
                               "attachment; filename=" & toDownload.Name)
            While dataLengthToRead > 0 AndAlso Response.IsClientConnected
                Dim lengthRead As Integer = _
                    iStream.Read(buffer, 0, ChunkSize)
                Response.OutputStream.Write(buffer, 0, lengthRead)
                Response.Flush()
                dataLengthToRead = dataLengthToRead - lengthRead
            End While
        End Using
        Response.Close()
    Else
        BindFileDataToGrid("Name")
    End If
End Sub

Figure 5 

Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
    Dim dlDir As String = "downloadfiles/"
    Dim strFileName As String = Request.QueryString("FileName")
    Dim path As String = Server.MapPath( _
        dlDir + Request.QueryString("FileName"))
    Dim toDownload As System.IO.FileInfo = New System.IO.FileInfo(path)

    If IsSafeFileName(strFileName) AndAlso toDownload.Exists Then
        Response.Clear()
        Select Case System.IO.Path.GetExtension(strFileName)
            Case ".zip"
                Response.ContentType = "application/x-zip-compressed"
                Response.AddHeader("Content-Disposition", _
                    "attachment;filename=NEWDL_" + toDownload.Name)
                Response.TransmitFile(path)

            Case Else
               ‘ File Extension not supported.
        End Select
        Response.End()
    Else
        BindFileDataToGrid("Name")
    End If
End Sub
Posted by SB패밀리