파일 교체를 위해 DeleteFile 후에 MoveFile을 하는 기존 코드가 있어서
이번에 MoveFileEx로 바꿨습니다. MoveFileEx의 경우 플래그를 지정하여,
기존 MoveFile에 비해 다양한 동작을 할 수 가 있는데요.(MSDN 참고)

그중 MOVEFILE_REPLACE_EXISTING 플래그를 이용하여
DeleteFile을 하지 않고 파일을 오버라이팅 하는 기능을 사용하였습니다.

기존에 어떤 문제인지(안티 바이러스로 의심하고 있습니다.) 파일 권한 때문에
DeleteFile과 MoveFile을 해서 파일을 교체하는데 실패가 빈번히 발생하고 있어서 수정을 해봤는데요.

MoveFileEx로 교체 후에는 특이하게 사용자의 OS가 Vista일 경우에 에러 5를 내뱉으며 실패를 하게 되었습니다.
(MoveFileEx는 윈도우 2000 이상부터 사용할 수 있습니다.)

에러코드 5번은 Access denied로 파일에 권한이 없어 실패한 경우 입니다.
이것도 테스트 결과 기존에 있던 파일에 권한은 상관이 없고 새로 교체하려는 파일에 권한이 없을 경우에 발생하는 에러입니다.
(신기하게 기존에 있던 파일에 접근 권한이 없더라도 새로운 파일로 교체가 되더군요.)

사용자들의 OS 분포도를 비교했을 때 확실히 윈XP와 윈7에서 실패율은 떨어졌지만
Vista의 실패가 급증해서 결국 기존과 비슷하거나 상황이 더 악화되어버린 것이죠.

그래서 왜 Vista에서 MoveFileEx가 실패하는지에 대해 여러곳으로 검색을 해봤는데요.
결론은 Vista에서 돌아가고 있는 Window Search(SearchIndexer.exe)라는 서비스 때문이였습니다.

Vista의 경우 빠른 파일 검색을 위해 Window Search라는 서비스가 돌아가고 있는데요.
Window Search가 인덱싱을 위해 파일에 어떤 짓을 하는지 모르겠지만
서비스를 정지시켜놓고 MoveFileEx를 테스트 할 경우 실패를 하지 않고, 서비스가 돌아갈 때는 에러 5로 실패를 하더군요.

아래는 구글링을 통해 찾은 정보 입니다.
http://stackoverflow.com/questions/153257/random-movefileex-failures-on-vista-access-denied-looks-like-caused-by-search-i

윈도우XP와 윈도우7에도 Window Search 서비스는 존재하는데요.
윈도우 XP는 나중에 업데이트를 통해 추가 된 기능입니다.
즉, Vista를 통해 Window Search 기능의 문제가 될만한 점을 파악하여
MS에서 나중에 들어간 윈XP와 윈7에는 수정을 한게 아닌가 싶습니다.

윈도우XP와 윈도우7에서는 Window Search 서비스의 시작 유무에 상관 없이 잘 되고있거든요.

그리고 위 URL 주소에 해결책으로 파일 속성을 FILE_ATTRIBUTE_TEMPORARYFILE_ATTRIBUTE_NOT_CONTENT_INDEXED로 설정하면 Vista에서 MoveFileEx의 설치 실패율이 떨어진다고 되어있는데요.
테스트 결과 FILE_ATTRIBUTE_TEMPORARY는 확실히 설치 실패율이 떨어지지만
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED는 별다른 영향을 미치지 않은 것으로 나타났습니다.

FILE_ATTRIBUTE_TEMPORARY 속성은 인터넷 임시 파일 처럼 곧 삭제될 또는 삭제 되어도 아무 상관 없다는 의미를 가지고 있습니다.
클라이언트가 일반적으로 사용하는 파일에 적용하기에는 무리가 따르는 속성인데요.

결국 Vista에서 MoveFileEx를 사용할 수 없다는 결론을 내렸습니다.
(물론 사용자의 OS가 Vista이고 Window Search 서비스가 돌아가는지 판단하여 코드를 짤 수도 있겠지만,
이를 위해 클라이언트에 부하를 주느니 Vista 유저일 경우는 전부 DeleteFile과 MoveFile을 이용하는게 나을 것 같습니다.)

저번에는 WinInet을 이용하여 C++로 파일 전송을 하였습니다.
이제 웹서버 측에서 파일을 받아야 할텐데요.

서버는 윈도우NT 계열이며 ASP.NET을 사용하였습니다.

1. Upload.aspx
우선 전송 받을 페이지를 만들어야 할텐데요.
어차피 전송 하는쪽에서 페이지를 완성하여 보내기 때문에
우리가 만들 페이지에는 별다른 내용이 없습니다. - _-;

<%@ Page Language="C#" CodeFile="ngmUpload.aspx.cs" %>

Upload.aspx 페이지에 내용이며 여기에는 단순히 CodeFile만 지정됩니다.

2. Upload.aspx.cs
실제로 서버에 파일을 저장하게 될 CodeFiel의 내용이 중요한데요.
ASP.NET을 사용하기 때문에 무지 단순합니다 ㅋ
protected void Page_Load(object sender, EventArgs e)
{
    string strFileName = Request.Files["VirusFile"].FileName;
    string strSaveLocation = Server.MapPath("Data") + "\\" + strFileName;

    try
    {
		Request.Files["VirusFile"].SaveAs(strSaveLocation);
		Response.Write("The file has been uploaded.");
    }
    catch ( Exception ex )
    {
		Response.Write("Error : " + ex.Message);
    }
}

얼마나 단순 합니까? ㅋ
단순히 Page_Load 이벤트 하나에 모든일이 다 해결 됩니다.

이전 포스트에 제가 name은 웹 서버에서 파일을 받기 위해 식별자로 사용한다고 했습니다.
그리고 그때 name은 TransferFile로 지정했었죠.
strContentInfo.Format( ( "--MULTI-PARTS-FORM-DATA-BOUNDARY\r\nContent-Disposition: form-data; name=\"TransferFile\"; filename=\"%s\"\r\nContent-Type: application/octet-stream\r\n\r\n" ), szUploadName );

바로 그 지정된 name을 통해 파일 이름을 얻어오고( 이 또한 우리가 filename으로 지정한 값입니다. )

사실 저장할 파일명은 임의로 다시 정해도 됩니다.
문제는 동일한 파일명을 저장할려고 할 때 덮어씌워져 버린다는 것이죠!
따라서 파일명은 절대로 유니크 해야 합니다.

저는 이미 파일을 전송할 때 사용자의 로컬 타임과 난수등을 사용하여 filename에 유니크한 값을 지정하였습니다.
그래서 지금 아무런 변경 없이 그대로 사용하고 있는 거구요.
( 어느 쪽에서든 파일 이름을 유니크하게만 해주시면 됩니다. ^^ )

그리고 서버에 저장할 경로까지 지정을 해준 다음에 SaveAs() 함수를 사용하면 끝! 입니다 ^^

이후에는 서버에서 파일을 잘 저장했는지 Response를 보내는 부분입니다.
사실 저번에 파일을 전송하는 부분까지만 코드가 짜져있었는데요.
파일을 성공적으로 전송한 후에 서버로부터 Response를 받아 실패 유무를 확인 해야 합니다.

이부분이 코드로 빠져있었는데... 이건 모 여러분이 충분히 알아 볼 수 있을 겁니다. ㅋ
요청해주시면( 설마 누가 이런 포스트를 보겠어! ) 이 부분도 올리도록 하겠습니다.
드디어 코드에 대한 포스팅을 하게 되네요!
사실 Syntax Highlighter를 사용하고 싶어서 블로그도 옮기고
열심히 해야지 해놓고.. 한동안 방치를 해두고 있었네요 ;;;

요근래에 파일을 서버로 전송할 필요가 생겨 WinInet 함수들을 이용하여 짜보았습니다.
(사실 만들고 한참이 지나 포스팅을 하게 되네요 - _-; 현재는 해당 파일에 대한 확인이 끝나 파일 전송 기능은 꺼놓은 상태 입니다. orz...)

1. GET VS POST
먼저 HTTP는 웹에서 사용을 하며 본래 텍스트를 주고 받는데 사용하고 있습니다.
여기에 GET 방식과 POST 방식이 있습니다.
GET 방식은 보내고자 하는 Data를 Url에 붙여서 보내고
POST 방식은 Url에 보이지 않도록 붙이지 않고 Data를 웹서버로 전송합니다.
그러다 보니 GET 방식의 경우는 전송할 수 있는 Data 용량에 제한이 있습니다.
(Url이 몇천글자씩 넘어가면 당연히 뻗겠죠? ^^)

그래서 파일을 전송하기 위해서는 POST 방식이 적합합니다.
(웹쪽은 정확하게 알지를 못해서 GET과 POST가 왜 나뉘었는지는 정확히 모르겠습니다. 막현히 Get 방식이 당연히 더 속도 측면에서 우수하지 않을까 싶습니다.)

2. Header
Header 값에 우리가 파일을 보내고 있다는 내용을 알려야지
웹 서버에서 파일을 전송 받을 수 있습니다.

Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; boundary=MULTI-PARTS-FORM-DATA-BOUNDARY

Encoding 방식과 현재 Type 그리고 boundary 값들을 셋팅해주어야 합니다.
Encoding과 Type은 파일이기 때문에 정해진 값을 사용하지만
boundary는 파일의 시작과 끝을 알려주기 위한 구분자입니다.
따라서 파일에서 절대 중복되지 않을 문자열 값을 셋팅해주면 됩니다.

본문이 시작할때 "--boundary값"으로 시작을 하고 끝날 때는 "boundary값--"으로 약속이 되어있습니다.

3. 코드
BOOL PostHTTPFileStream( LPCTSTR szUrl, LPSTREAM pStream, LPCTSTR szUploadName )

우선 제가 만든 함수의 원형입니다. 웹서버에서 전송받을 Url과 파일 스트림,
그리고 서버에 저장될 파일 이름을 받아 웹서버로 전송을 해주는 함수입니다.
(WinInet 함수에 대해 하나 하나 설명 하지는 않겠습니다. MSDN을 참고해주세요.)
CUrl		url;
CString		strFile;
HINTERNET	hInternet	= NULL;
HINTERNET	hConnect	= NULL;
HINTERNET	hRequest	= NULL;

url.CrackUrl( szUrl );
strFile.Format( _T( "%s%s" ), url.GetUrlPath(), url.GetExtraInfo() );

hInternet	= ::InternetOpen( _T("HELIO"), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0 );

hConnect =::InternetConnect( hInternet, url.GetHostName(),
	url.GetPortNumber(), NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0 );

hRequest = ::HttpOpenRequest( hConnect, _T("POST"), strFile, HTTP_VERSION, _T(""), NULL,
			INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_FORMS_SUBMIT, 0);

이제 WinInet을 사용하여 해당 Url에 연결을 합니다. 여기서 CUrl을 사용하고 있는데요.
Url 관련 스트링을 다룰때 굉장히 유용한 클래스 입니다.
(url 스트링을 열심히 파싱하고 계셨다면 한번 알아보고 사용해보세요. ^^)
연결을 할때는 당연히 POST 방식을 사용할 것이기 때문에 HttpOpenRequest에 POST로 설정을 합니다.
CString strHeaderEncoding	= _T( "Content-Transfer-Encoding: binary\r\n" );
CString strHeaderContentType	= _T( "Content-Type: multipart/form-data; boundary=MULTI-PARTS-FORM-DATA-BOUNDARY\r\n" );

::HttpAddRequestHeaders( hRequest, strHeaderEncoding, strHeaderEncoding.GetLength(), HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE );
::HttpAddRequestHeaders( hRequest, strHeaderContentType, strHeaderContentType.GetLength(), HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE );

이제 우리가 파일을 보낸다는 것을 알리기 위해 Header를 설정하여 서버에 알려줍니다.
CStringA	strContentInfo;
CStringA	strContentEnd	= "\r\nMULTI-PARTS-FORM-DATA-BOUNDARY--\r\n";

strContentInfo.Format( ( "--MULTI-PARTS-FORM-DATA-BOUNDARY\r\nContent-Disposition: form-data; name=\"TransferFile\"; filename=\"%s\"\r\nContent-Type: application/octet-stream\r\n\r\n" ), szUploadName );

이제 파일 내용을 담은 본문을 작성해야 합니다. 파일의 처음과 끝을 설정하기 위해
아까 정한 boundary 값을 이용합니다. 여기서 name은 웹서버에서 파일을 받기 위해 사용할 식별자 입니다.
참! 그리고 중요한 것은 반드시 ANSI 이어야 합니다!(보면 CStringA를 쓰고 있죠? ^^)
파일 인코딩 형식을 사용하기 때문에 서버는 ANSI로 Content를 작성해야 읽어들일 수 있습니다.

HGLOBAL		hGlobal;
DWORD		dwPostBufferLength;
INTERNET_BUFFERS	BufferIn	= {0};

::GetHGlobalFromStream( pStream, &hGlobal );

dwPostBufferLength = strContentInfo.GetLength() + static_cast( ::GlobalSize( hGlobal ) ) + strContentEnd.GetLength();

BufferIn.dwStructSize	= sizeof( INTERNET_BUFFERS );
BufferIn.dwBufferTotal	= dwPostBufferLength;
BufferIn.Next		= NULL;

::HttpSendRequestEx( hRequest, &BufferIn, NULL, HSR_INITIATE, 0);

파일을 전송하기 전에 웹서버에 전송할 전체 사이즈를 알려주어야 합니다.
(파일 스트림은 내부 버퍼에 할당을 했습니다. 이를 통해 스트림의 사이즈를 가져왔습니다.)
DWORD dwBytesWritten;

::InternetWriteFile( hRequest, strContentInfo, strContentInfo.GetLength(), &dwBytesWritten );

DWORD dwBytesRead;
BYTE pBuffer[1024*4];
LARGE_INTEGER liBegin = {0};

pStream->Seek( liBegin, STREAM_SEEK_SET, NULL );
do
{
	pStream->Read(pBuffer, sizeof(pBuffer), &dwBytesRead );
	::InternetWriteFile( hRequest, pBuffer, dwBytesRead, &dwBytesWritten )
} while ( dwBytesRead == sizeof(pBuffer) );

::InternetWriteFile( hRequest, strContentEnd, strContentEnd.GetLength(), &dwBytesWritten );
::HttpEndRequest( hRequest, NULL, 0, 0 );

이제 파일의 내용을 순서대로 전송하면 끝이 납니다!
(소스의 내용을 줄이려다보니 코드를 보시면 아시겠지만 예외처리에 대한건 하나도 안 했습니다 - _-)

아 시간은 엄청 들여서 포스팅을 하는데(근무 시간인데 이러다 짤리겠...)
별로 깔끔해 보이지 않는군요. -_ㅜ

다음에는 웹서버에서 전송 받은 파일을 저장하는 법을 포스팅 하겠습니다~

Syntax Highlighter라고 해서 코드를 MSDN 형식으로 보여주는 멋진 녀석입니다!
사실 다른 곳에서 블로그를 하다가 Syntax Highlighter 때문에 티스토리로 옮기게 되었는데요.
저는 티스토리가 기본으로 지원해주는 줄 알았습니다. _-_

그래서 여차저차 Syntax Highlighter를 설치해봤습니다.
Syntax Highlighter는 자바스크립트로 업로드를 하고 특정 HTML 태그의 클래스를 사용하면 됩니다.

우선 다운로드 주소 입니다.
http://alexgorbatchev.com/wiki/SyntaxHighlighter:Download

최신 버전을 다운 받으신 후에 압축을 풉니다.(저는 2.0.320 버전을 사용했습니다.)
아래 그림과 같이 scripts, src, styles 폴더가 있구요.
LPGLv3.txt, test.html 파일이 있습니다.

사용자 삽입 이미지
3개의 폴더는 Sytanx Highlighter가 돌아가기 위한 파일들이 존재하구요.
test.html은 테스트 샘플 페이지 입니다.
(LPGLv3.txt는 라이센스 파일이니 신경 끄시면 됩니다. - 심심하시면 읽어보셔도 되구요 - _-; )

우선 세개 폴더의 모든 파일들을 티스토리에 업로드 합니다!
관리 페이지에 스킨 -> 직접올리기를 통해 파일을 업로드 할 수 있습니다.
(업로드를 한 후에 완료 되었습니다! 같은 메세지 박스는 안 뜨더군요. _-_)


이제 티스토리에서 사용하기위해 자바스크립트 파일들을 연결합시다!
test.html 파일을 소스보기로 통해 <head></head> 사이에 있는 <script>와 <link> 태그 내용을만
티스토리에 추가해주시면 됩니다. 관리 페이지에 스킨 -> HTML/CSS 편집을 통해 skin.html에
<head></head> 사이에다가 test.html에서 복사해준 내용을 붙여넣어주면 됩니다.

자~ 이제 여기서 문제가 생기는데요 ㅠ
test.html은 세개의 폴더가 존재한다고 가정하고 js 파일과 css 파일을 연결하고 있습니다.
즉, 다음과 같이 상대경로를 설정하고 있습니다.
src="scripts/shBrushVb.js", href="styles/shCore.css"
티스토리에 업로드를 할때 폴더 단위가 아닌 파일들만 업로드가 되는데요.
이때 파일이 업로드되는 상대경로는 ./images 입니다! (이걸 몰라서 한참을 헤맸습니다. ㅠㅠ)
(드래한 부분이 상대경로를 수정하여 <head></head>사이에 들어간 내용입니다.
클릭해서 확대해서 보세요 ^^)

따라서 상대경로 위치를 수정해주시면 됩니다.
src="./images/shBrushVb.js", href="./images/shCore.css"

연결이 다 된후에는 이제 특정 태그를 통해 사용을 하시면 되는데요 +_+
test.html에 있는 코드를 통해 확인해보겠습니다.
<pre class="brush: c-sharp;">
function test() : String
{
 return 10;
}
</pre>

이 내용을 HTML 편집모드로 전환하여 붙여넣으면 짠~

function test() : String
{
	return 10;
}

이제 원하시는 소스코드를 깔끔하게 블로그에 올릴 수 있습니다~ ^^

카테고리

분류 전체보기 (28)
주절주절 (15)
바라보기 (5)
구경하기 (4)
기억하기 (4)

최근에 받은 트랙백

달력

«   2019/08   »
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Total : 12,717
Today : 3 Yesterday : 0
Statistics Graph