Загрузка файлов на сервер: php+C#

31 июл. 2012 г. | | |

Сценарий простой: клиент (winforms) хочет править миром загружать файлы на сервер (php). Первая мысль - использовать WebClient, у которого есть замечательный метод UploadFileAsync. На сервер вместе с файлом нужно передать пару-тройку дополнительных параметров, т.е. отправить стандартную форму типа:
<form action="http://localhost/uplad.php" enctype="multipart/form-data" method="POST">
<input type="text" name="title" value="" />
<input type="text" name="description" />
<input type="FILE" name="uploadfile" />
<input type="submit" name="send" value="Upload" />
</form>
И тут начинаются проблемы. Теоретически, это можно сделать, используя QueryString у WebClient:
If the QueryString property is not an empty string, it is appended to address. [MSDN]
Но практически тут нужен бубен:
Unfortunately, most file upload scenarios are HTML form based and may contain form fields in addition to the file data. This is where WebClient falls flat. After review of the source code for WebClient, it is obvious that there is no possibility of reusing it to perform a file upload including additional form fields.  [CodeProject]

На том же CodeProject после гугления по теме была найдена статья, в которой рассмотрена реализация расширенного метода UploadFile: загрузка файла + возможность указать тип загружаемых данных + возможность прикрепить куки + возможность отправить дополнительные переменные.  Минус полученного решения в его однопоточности. Далее будет приведена реализация асинхронной загрузки файла на сервер.

I. Рефакторинг

Первое, что захотелось сделать после копипаста кода в предварительно созданный класс FileUploadingAsync, это отрефакторить его.
Итак, сначала из метода UploadFileEx вынесем в отдельный метод добавление get-параметров в url:

private string AddParamsToUrl(string url, NameValueCollection querystring)
{
  string postdata = "";
  if (querystring != null)
  {
   /*Если в url уже есть get-параметры, то ? не нужен, просто продолжим их добавлять*/

     if (Regex.IsMatch(@"(\.[A-Za-z\d]{2,5}\?)", url)) postdata = "&"; 
     else postdata = "?";
     foreach (string key in querystring.Keys)
     {
        postdata += key + "=" + querystring.Get(key) + "&";
     }
     postdata = postdata.TrimEnd('&');
   }
   return url + postdata;
}

Теперь вынесем в константы магические числа и строки, раскиданные по всему UploadFileEx:

public const string DEFAULT_FILE_VARIABLE_NAME = @"file"; 
public const string DEFAULT_CONTENT_TYPE = @"application/octet-stream"; 
       const int DATA_BLOCK_SIZE = 4096; 
       const string NEW_LINE = "\r\n";

Затем выделим метод BuildPostHeader:

private byte[] BuildPostHeader(string uploadfile, string fileFormName, string contenttype, string boundary)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("--");
  sb.Append(boundary);
  sb.Append(NEW_LINE);
  sb.Append("Content-Disposition: form-data; name=\"");
  sb.Append(fileFormName);
  sb.Append("\"; filename=\"");
  sb.Append(Path.GetFileName(uploadfile));
  sb.Append("\"");
  sb.Append(NEW_LINE);
  sb.Append("Content-Type: ");
  sb.Append(contenttype);
  sb.Append(NEW_LINE);
  sb.Append(NEW_LINE);
  return Encoding.UTF8.GetBytes(sb.ToString());
}


И хорошо было бы еще выделить в отдельный метод часть кода, ответственную за запись данных в поток:

private void WriteRequestDataToStream(string uploadfile, byte[] postHeaderBytes,
                                      byte[] boundaryBytes, Stream requestStream)
{
    requestStream.Write(postHeaderBytes, 0, postHeaderBytes.Length);
    using (FileStream fileStream = new FileStream(uploadfile,
                                   FileMode.Open, FileAccess.Read))
    {
       // Write out the file contents
       byte[] buffer = new Byte[checked((uint)Math.Min(DATA_BLOCK_SIZE, 

                                                      (int)fileStream.Length))];
       int bytesRead = 0;
       while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
       requestStream.Write(buffer, 0, bytesRead);
       // Write out the trailing boundary
       requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
       fileStream.Close();
    }
    requestStream.Close();
}

II. Кодинг

Теперь можно реализовать загрузку файла на сервер в отдельном потоке:

public void UploadFileAsync(string uploadfile, string url, string fileFormName = DEFAULT_FILE_VARIABLE_NAME, string contenttype = DEFAULT_CONTENT_TYPE, NameValueCollection querystring = null, CookieContainer cookies = null)
{
   Thread th = new Thread(() => UploadFileEx(uploadfile, url, fileFormName, contenttype, querystring, cookies));
   th.Start();
}

У метода UploadFileEx меняем область видимости на private, чтобы клиентский код имел доступ только к UploadFileAsync().
Было бы неплохо, если б вызывающий код мог отслеживать окончание загрузки файла на сервер и получать результат операции (success, fail). Для этого добавим событие: public event Action<ServerErrorCodes, string, string> UploadCompleted;
которое вызывается в конце метода UploadFileEx.
В итоге UploadFileEx выглядит следующим образом:

private void UploadFileEx(string uploadfile, string url, string fileFormName, string contenttype, NameValueCollection querystring, CookieContainer cookies)
{
    if (String.IsNullOrEmpty(fileFormName))
    {
        fileFormName = DEFAULT_FILE_VARIABLE_NAME;
    }

    if (String.IsNullOrEmpty(contenttype))
    {
        contenttype = DEFAULT_CONTENT_TYPE;
    }

    Uri uri = new Uri(AddParamsToUrl(url, querystring));

    string boundary = "----------" + DateTime.Now.Ticks.ToString("x");
    HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(uri);
    webrequest.CookieContainer = cookies;
    webrequest.ContentType = "multipart/form-data; boundary=" + boundary;
    webrequest.Method = "POST";

    // Build up the post message header
    byte[] postHeaderBytes = BuildPostHeader(uploadfile, fileFormName, contenttype, boundary);

    // Build the trailing boundary string as a byte array
    // ensuring the boundary appears on a line by itself
    byte[] boundaryBytes = Encoding.ASCII.GetBytes(NEW_LINE + "--" + boundary + NEW_LINE);

    
    FileInfo fi = new FileInfo(uploadfile);
    long length = postHeaderBytes.Length + fi.Length + boundaryBytes.Length;
    webrequest.ContentLength = length;

    // Write out our post header
    WriteRequestDataToStream(uploadfile, postHeaderBytes, boundaryBytes, 

                             webrequest.GetRequestStream());

    WebResponse responce = webrequest.GetResponse();
           
    using (StreamReader sr = new StreamReader(responce.GetResponseStream()))
    {
        var result = sr.ReadToEnd();
        if (result == SUCCESS_FLAG) 

             UploadCompleted(ServerErrorCodes.Success, uploadfile, String.Empty);
        else UploadCompleted(ServerErrorCodes.FileUploadFailed, uploadfile, result);
    }
}


Чтобы реализовать уведомление о процессе загрузки файла на сервер т.е. сколько всего загружено мегабайт, сколько осталось загрузить, нужно добавить еще одно событие FileUploadingProgressChanged и немного изменить метод WriteRequestDataToStream:

private void WriteRequestDataToStream(string uploadfile, byte[] postHeaderBytes,
                                      byte[] boundaryBytes, Stream requestStream)
{
    requestStream.Write(postHeaderBytes, 0, postHeaderBytes.Length);
    using (FileStream fileStream = new FileStream(uploadfile,
                                   FileMode.Open, FileAccess.Read))
    {

       int totalsent = 0;
       // Write out the file contents
       byte[] buffer = new Byte[checked((uint)Math.Min(DATA_BLOCK_SIZE, 

                                                      (int)fileStream.Length))];
       int bytesRead = 0;
       while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
      
       {
           requestStream.Write(buffer, 0, bytesRead);

           totalsent += bytesRead;
           OnFileUploadingProgressChanged(uploadfile, totalsent);
       }

       // Write out the trailing boundary
       requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
       fileStream.Close();
    }
    requestStream.Close();
}


Теперь осталось реализовать CancelAsync() для прерывания загрузки файла на сервер. Этот момент очень интересен, т.к. пользователь может запустить загрузку нескольких файлов параллельно:
fileUploader.UploadFileAsyc(...);
fileUploader.UploadFileAsyc(...);

Эту задачу оставлю на следующую заметку, многобукв одноразово может вызвать несварение мозга :)

III. Пример

На десктопе клиент отправляет файл:

void UploadFile(String backupname)
{
    FileUploadingAsync fu = new FileUploadingAsync();
    fu.UploadCompleted += UploadFileCompleted;
    fu.UploadFileAsync(backupname, settings.HelpCenterServer + "/upload.php?id=" + settings.UserID + "&tid=" + e.Result + "&fname=" + Path.GetFileName(backupname), "bf");
}

void UploadFileCompleted(ServerErrorCodes result, String filename, String errorMessage)
{
    if (result == ServerErrorCodes.Success)
    {
        MessageBox.Show("File was uploaded.");
    }
    else
    {
        MessageBox.Show("File was not uploaded: " + errorMessage);               
    }           
}

На сервере php-скрипт выполняет загрузку файлов:
<? require_once 'common.php';
$data_dir = $_SERVER['DOCUMENT_ROOT']."/users/files/";
$fname = getCorrectFileName($_GET['fname']);
$filename = time()."_".$fname;
//проверки данных пропущены

$file_location = $data_dir."/".$filename;
$result = move_uploaded_file($_FILES['bf']['tmp_name'], $file_location);
if (TRUE === $result) echo "Success";
else echo $result;
?>

Для наглядности опущены проверки данных, полученных через get-запрос.
Код на текущий момент.

0 коммент.:

Отправить комментарий