July 17, 2009

Indy Http Upload

Задача: написать файл-сервер для закачки файлов по http протоколу.
Казалось бы, что может быть проще для быдлокодера сваять за пять минут такое под СиБилдером. Но практика выявила некоторые подводные камни, описанием расположения которых я хотел бы поделиться.

Гугль, а он не может ошибаться, говорит про единственный способ сделать это затратив минимум усилий: Internet Direct (www.indyproject.org). Качаем, компилируем. Есть две версии Indy: стабильная девятая, и последняя десятая. На форумах советуют, как лекарство от большинства проблем, пользоваться десятой. Так как проблем действительно много, с неё и начинаем.

Подводный камень №1: Десятая версия не компилится под C++ Builder 5.0. Пробуйте. У меня не вышло. На форумах пишут, что не только у меня, и решения ни у кого нет.

Так как десятая версия под пятым билдером действительно не хочет работать, можем для очистки совести скачать C++ Builder 6.0. В нём уже стоит старая версия Indy, ставим поверх новую. И натыкаемся на:

Подводный камень №2: После установки новой версии Indy поверх старой компилятор вылетает с массой ошибок, ведущих в недра Indy. Это обычно означает, что где-то в прописанных путях остались старые заголовки (если ошибки вывалятся на этапе компиляции), либо библиотеки (если на этапе компоновки). Проходимся поиском по папке Borland и нещадно давим все старые *.hpp и *.lib имеющие отношение к Indy.

Собственно, как показала практика, для нашей задачи вполне достаточно Indy 9.0, и я с радостью вернулся под пятый билдер.

Единственная особенность для девятой версии Indy заключается в нигде не документированном подводном камне.

Подводный камень №3: Стабильная девятая версия не создаёт поток для входящего файла. Это придется сделать вручную в событии OnCreatePostStream:

VPostStream = new TStringStream("");

Кстати, с другими типами потоков (TFileStream, TMemoryStream) Indy почему-то отказывается работать.

Итак, поток создан, заголовки прочитаны. Сохранив поток на диск, мы увидим, что он закодирован как multipart/form-data (см. RFC-1867 http://www.faqs.org/ftp/rfc/rfc1867.txt). Если в двух словах, то к загружаемому файлу в начале и в конце добавлены несколько строк, в которых описывается его формат, имя и границы (на случай передачи нескольких файлов). Если мы передаём картинку, архив или исполняемый файл, пара лишних байтов могут сделать его нечитаемым, поэтому мы начинаем искать способ, как его можно грамотно раскодировать.

И вот теперь начинают попадаться самые странные подводные камни: во всей палитре Indy нет ни одного компонента, который бы это делал! Внимательно читаем гугль, самые грамотные советы по Indy даёт один из разработчиков - Remy Lebeau, который подписывается как Gambit (запросите гугль по ключевым словам Remy Lebeau Gambit и вы найдете множество советов по использованию Indy с примерами). Он предлагает делать декодировку с помощью компонента TIdMessageDecoder, причем практически вручную. MIMEBoundary надо самому прочитать как строку, скормить декодеру, и надеясь, что поток непрерывный, в цикле пройтись по всем частям MIME, вручную считывая заголовки и обрабатывая ошибки! Значит, весь веб-сервер мы создали одним кликом, бросив компонент на форму, а чтобы прочитать переданный файл - должны писать сотни строк кода, самим разбираясь в структуре MIME. Кто-то спросил Реми, почему бы не создать компонент, который бы это делал сам, так так функция очень востребованная, на что Реми ответил, что да, в будущем надо подумать, и что неплохо бы это как-то упростить.

Самое печальное, что его советы устарели. Классы, которыми он пользуется, отсутствуют и в девятой и десятой версиях Indy, а пользоваться более старыми сборками страшно, почитав список пофиксенных ошибок. Курение мануалов помогает слабо - я так и не смог найти, чем можно заменить устаревшие классы.

Кстати, что характерно, есть две версии мануала - доступный для скачки, и онлайн. Так вот, тот, что доступен для скачки, относится к достаточно старой сборке, и во многих случаях далёк от реальности, а онлайн - практически бесполезен, так как содержит автоматически сгенерированное перечисление структуры классов, без коментариев по работе и назначению.

Кончилось всё тем, что я перечитал RFC-1867 столько раз, что понял, как можно выкусить файл вручную. Если вы захотите пойти по тернистому пути быдлокодера, вот вам под катом непричесаный но вполне рабочий исходник.

//---------------------------------------------------------------------------
//Извлечение из строки n-го слова
//Отсчет с нуля
//Если n больше чем кол-во слов, выводится последнее слово
#define SMB "/"
AnsiString parm(int n,AnsiString st)
{

  while(n>0)
  {
    st = st.Delete(1,st.Pos(SMB));

    n--;
  }
  st = st.Delete(st.Pos(SMB),st.Length()-st.Pos(SMB)+1);

  return st;
}
//Количество подстрок в строке
int SCount(AnsiString st)
{
   int a=0;

   while(st.Pos(SMB)>0)
   {
     st=st.Delete(st.Pos(SMB),1);

     a++;
   }
   return a;
}
//---------------------------------------------------------------------------
AnsiString ExtractString1(AnsiString source,AnsiString start,AnsiString end)
{

  // ExtractString1("his name=jim?","name=","?") = "jim"
  if(!source.Pos(start))return "";
  source = source.Delete(1,source.Pos(start)+start.Length()-1);

  if(!source.Pos(end))return source;
  source = source.Delete(source.Pos(end),source.Length());

  return source;
}
//---------------------------------------------------------------------------
// We are looking for pattern with length patlen in buf with length buflen
// starting from start. Returning first occurence, or -1 if not found
int memsearch(char * buf,int buflen,int start,char * pattern,int patlen)
{

  int found;
  for(int a=start;a<buflen-patlen+1;a++)
  {

    found = 1;
    int b=0;
    while(b<patlen)
    {

      if(buf[a+b]!=pattern[b])
      {
        found = 0;

        b = patlen;
      }
      b++;
    }
    if(found)

      return a;
  }
  return -1;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::IdHTTPServer1CommandGet(TIdPeerThread *AThread,

      TIdHTTPRequestInfo *ARequestInfo, TIdHTTPResponseInfo *AResponseInfo)
{
  //Address sample: /inv/11111
  //Address sample: /inv/11111/file.pdf
  int pcount = SCount(ARequestInfo->Document);

  if(pcount>1)
  if(parm(1,ARequestInfo->Document)=="inv")
  {

    int inv_no;
    try
    {
      inv_no = parm(2,ARequestInfo->Document).ToInt();
    }

    catch(...)
    {
      AResponseInfo->ContentType = "text/html";
      AResponseInfo->ContentText = (AnsiString)"<h1>Invalid invoice number "+parm(2,ARequestInfo->Document)+"</h1>";

      return;
    }
    AnsiString inv_s = (AnsiString)ROOTPATH+inv_no+"\\";

    ForceDirectories(inv_s);

    //+Upload
    if(ARequestInfo->Command=="POST")
    {

      //Извлечение имени файла
      AnsiString fname = ExtractString1(ARequestInfo->UnparsedParams,"filename=\"","\"");

      fname = (AnsiString)inv_s+ExtractFileName(fname);
      //Узнаем размер полного MIME-потока
      int size = ARequestInfo->PostStream->Size;

      //Создадим буфер, и скинем весь поток туда - так быстрее можно найти начало и конец
      char * buf = (char*)calloc(size+1,sizeof(char));

      ARequestInfo->PostStream->Position = 0;
      ARequestInfo->PostStream->ReadBuffer(buf,size);

      //Создадим буфер для MIMEboundary и прочитаем его
      int bound_length = memsearch(buf,size,0,"\r\n",2);

      char * bound = (char*)calloc(bound_length+1,sizeof(char));

      memcpy(bound,buf,bound_length);
      //Откроем файл для записи
      FILE * f = fopen(fname.c_str(),"wb");

      if(!f)
      {
        AResponseInfo->ContentType = "text/html";
        AResponseInfo->ContentText = (AnsiString)"<h1>Error creating file ["+fname+"] "+ARequestInfo->FormParams+"</h1>";

        return;
      }
      //Найдем начало файла: два CRLF подряд
      int p1 = memsearch(buf,size,0,"\r\n\r\n",4);

      if(p1<0)
      {
        AResponseInfo->ContentType = "text/html";

        AResponseInfo->ContentText = (AnsiString)"<h1>Upload error: 2xCRLF not found!</h1>";
        return;
      }

      p1 = p1+4; // Пропустим их
      //Найдем конец файла по MIMEboundary
      int p2 = memsearch(buf,size,p1,bound,bound_length);

      if(p2<0)ShowMessage("Oops2");
      p2 = p2-2; // Вернемся назад на один CRLF
      //Запишем файл на диск

      fwrite(buf+p1,sizeof(char),p2-p1,f);

      fclose(f);
      //Вернёмся в просмотр каталога
      AResponseInfo->Redirect((AnsiString)"/inv/"+inv_no);

      free(buf);
      free(bound);
      return;
    }  
    //-Upload


    if(pcount==2)
    {//Only browse
      AResponseInfo->ContentText = "Inv "+inv_s+"<hr><table border=1><tr><td>Name</td><td>Size</td></tr>";
    }

    //
    int fcount = 0;
    TSearchRec sr;
    int iAttributes = faAnyFile;

    if (FindFirst(inv_s+"\\*.*", iAttributes, sr) == 0)
    {

      do
      {
        if (sr.Name != ".")

        if (sr.Name != "..")
        if ((sr.Attr & iAttributes) == sr.Attr)
        {

          fcount++;
          if(pcount==2)
            AResponseInfo->ContentText = (AnsiString)AResponseInfo->ContentText + "<tr><td><a href=\""+ARequestInfo->Document+"/"+sr.Name+"\">"+sr.Name+"</a></td><td>"+sr.Size+"</td></tr>";

          if(pcount==3)
          if(sr.Name==parm(3,ARequestInfo->Document))
          {

            try
            {
              AResponseInfo->ContentStream = new TFileStream((AnsiString)inv_s+"\\"+sr.Name,fmOpenRead|fmShareDenyNone);
            }

            catch(...)
            {
              AResponseInfo->ContentType = "text/html";
              AResponseInfo->ContentText = (AnsiString)"<h1>Cannot open file "+sr.Name+"</h1>";

              FindClose(sr);
              return;
            }
            if(sr.Name.Pos(".pdf"))

              AResponseInfo->ContentType = "text/pdf";
            if(sr.Name.Pos(".jpg"))

              AResponseInfo->ContentType = "image/jpeg";
            if(sr.Name.Pos(".txt"))

              AResponseInfo->ContentType = "text/plain";
            FindClose(sr);
            return;
          }
        }
      } while (FindNext(sr) == 0);

      FindClose(sr);
    }
    if(pcount==2)
    {//Only browse
      AResponseInfo->ContentText = (AnsiString)AResponseInfo->ContentText+ "</table><br>" + fcount+" file(s).<hr>";

      AResponseInfo->ContentText = (AnsiString)AResponseInfo->ContentText +
        "<form action=\""+parm(2,ARequestInfo->Document)+"/upload_file.php\" method=\"post\" enctype=\"multipart/form-data\">"+

        "<label for=\"file\">Filename:</label><input type=\"file\" name=\"file\" id=\"file\" />"+
        "<input type=\"submit\" name=\"submit\" value=\"Upload\" /></form>";
      return;
    }
  }

  AResponseInfo->ContentText = (AnsiString)"Invalid address! "+pcount+" ["+parm(2,ARequestInfo->Document);

  return;
}
//---------------------------------------------------------------------------
// Создадим поток входящего файла, ибо Indy 9.0 это не сделает.
// Удалять потом его не надо, она сама.
void __fastcall TForm1::IdHTTPServer1CreatePostStream(
      TIdPeerThread *ASender, TStream *&VPostStream)
{

  VPostStream = new TStringStream("");
}
//---------------------------------------------------------------------------

No comments: