TeamWox SDK: Файловое хранилище - Часть 1

Введение

Для хранения и работы с данными в системе TeamWox используются два типа хранилищ: СУБД Firebird и файловое хранилище. Взаимодействие TeamWox с СУБД TeamWox мы рассмотрели в статье TeamWox SDK: Взаимодействие с СУБД. В этой статье мы поговорим о файловом хранилище. Вы узнаете, что такое файловое хранилище, как оно реализуется и где используется в TeamWox.

Для удобства восприятия, эта статья будет разбита на две части. В первой части помимо теоретических сведений мы рассмотрим простой пример сохранения описания записи в файловом хранилище, ее чтение и удаление. Вторая часть будет посвящена описанию инструментов для работы с ФХ, которые используются в TeamWox: поддержка WYSIWYG-редактора, списка прикрепленных файлов и комментариев к записи на примере учебного модуля Helloworld.


Файловое хранилище TeamWox

В TeamWox используется 2 способа хранения данных:

1. СУБД. В СУБД хранятся данные для сортировки и вывода списков: заголовки, даты создания и изменения записей и прочая служебная информация.

2. Файловое хранилище. Объемный контент (изображения, видео, аудио и пр.), который обычно в СУБД хранится в полях типа BLOB, в TeamWox хранится в собственном файловом хранилище. Обращение к такому контенту производится по id записи (key/value).

Такое разделение обусловлено тем, что хранить большие объемы данных в реляционных СУБД неэффективно с точки зрения быстродействия. Кроме того, если бы данные модулей хранились в СУБД, то стало бы невозможно управлять стратегией кэширования (в базе попросту не оставалось бы памяти на индексы).

Поскольку при разработке TeamWox во главу угла ставилось быстродействие системы и ее отказоустойчивость, все эти особенности были учтены. Использование файлового хранилища в TeamWox дает следующие преимущества:

  • Быстродействие. Поскольку в СУБД TeamWox не используется тип данных BLOB, размер базы данных остается минимальным. Кэш СУБД используется для хранения индексов.

  • Кэширование данных. Наиболее часто используемые в повседневной работе данные вплоть до 30-80 Мб (в зависимости от размера свободной памяти и разрядности системы) кэшируются в памяти, что повышает производительность системы.

  • Сжатие данных. Применяется для всех файлов за исключением некоторых MIME типов файлов, которые изначально сжаты (видео, mp3, архивы и пр.).

Область применения файлового хранилища TeamWox:

  • Тексты писем, задач, сообщений на форуме, документов, комментариев и т.д в модулях Задачи, Форум, Документы, Организации, Сотрудники, Контакты, Продукты.

  • Файлы (изображения, видео, аудио, диаграммы и т.д.) загруженные через встроенный WYSIWYG-редактор или прикрепленные пользователем в задачах, комментариях и т.д.

Данные файлового хранилища находятся в папке <папка_установки_TeamWox>\data. Данные для каждого модуля хранятся в соответствующей вложенной папке \data\<имя_модуля>. Когда модуль начинает пополняться данными, в папке \data\<имя_модуля>\ создается вложенная папка 1\. В этой папке создаются файлы с именами 1, 2, 3 и т.д. и расширением dat. В эти файлы и записываются данные модуля. Когда наберется 100 таких файлов, сервер создает папку 2\, затем, после ее заполнения, папку 3\ и т.д.

Для работы с файловым хранилищем, а точнее - с записями, которые в нем хранятся, модуль должен реализовывать интерфейс IFilesManager, который мы сейчас и рассмотрим.

 

Интерфейс для работы с данными

Для работы с данными в файловом хранилище TeamWox API предоставляет интерфейс IFilesManager. Основными его методами являются методы сохранения данных (FileStore), чтения данных (FileRead) и удаления данных (FileDelete). Они аналогичны методам C.R.U.D., которые мы реализовали для взаимодействия с СУБД.

В методе FileStore сохраняемым данным можно выставлять определенные флаги, которые будут определять логику модуля. Если флаг не выставлен (значение 0), то данные сразу сохраняются в хранилище. При этом определяется MIME-тип файла, и затем в зависимости от этого типа файл либо сжимается (текст, изображения и пр.), либо сохраняется без компрессии (архивы, видео, музыка и пр.)

Флаг FILE_FLAG_TEMP помечает сохраняемые данные как временные. При сохранении выполняется ряд действий, и какое-либо из них может выполниться неудачно. Если транзакция по каким-либо причинам прошла неудачно, файлы останутся отмечены как временные (или сохранены с временным флагом). В таком случае в служебное время сервера TeamWox все временные данные будут удалены, а хранилище не будет засоряться поврежденными данными.

Флаг FILE_FLAG_COMPRESSED помечает данные как сжатые, т.е. для них не применяется сжатие при сохранении. Флаг FILE_FLAG_UNKNOWN_MIME позволяет разработчику самому указать MIME-тип для файла.

Если загружаемые в хранилище данные помечаются как временные, то с помощью метода FilesCommit можно подтвердить их загрузку, убрав флаг временной записи и выставив идентификаторы записи, к которой привязаны данные файлы.

Метод FileInfoGet позволяет получить информацию о файле при его чтении из хранилища.

Приведем описания некоторых методов работы с файлами, которые будем использовать в этой статье.

 

Сохранение записи в файловое хранилище из памяти

virtual TWRESULT  FileStore(const Context *context,const void *src,FileInfo *info,INT64 *file_id,int flags)
Параметр Тип Описание
*context Context Контекст обработки запроса.
*src void Указатель на область памяти, которая записывается в файловое хранилище. Размер указывается в FileInfo *info.
*info FileInfo Информация о записи.
*file_id INT64 Идентификатор записи в файловом хранилище. Если file_id=0, то создается новая запись, иначе - перезаписывается существующая.
flags int Флаги из перечисления EnFilesFlags.

 

Удаление записи из файлового хранилища

virtual TWRESULT  FileDelete(const Context *context,INT64 file_id)
Параметр Тип Описание
*context Context Контекст обработки запроса.
*file_id INT64 Идентификатор записи в файловом хранилище.

 

Чтение записи из файлового хранилища в память

virtual TWRESULT  FileRead(const Context *context,const INT64 file_id,void *dst,UINT64 *dst_len,const UINT64 offset)
Параметр Тип Описание
*context Context Контекст обработки запроса.
*file_id INT64 Идентификатор записи в файловом хранилище.
*dst void Область памяти, куда будет читаться запись.
*dst_len UINT64 Размер данных, которые будут записываться (в байтах).
offset UINT64 Смещение в записи в файловом хранилище, с которого нужно произвести чтение.

 

Получение информации о записи

virtual TWRESULT  FileInfoGet(const Context *context,const INT64 file_id,FileInfo *info)
Параметр Тип Описание
*context Context Контекст обработки запроса.
*file_id INT64 Идентификатор записи в файловом хранилище.
*info FileInfo Информация о записи.

 

Подтверждение сохранения записи с временным флагом

virtual TWRESULT  FilesCommit(const Context *context,const INT64 *file_ids,const int count,const INT64 type_id,const INT64 record_id) 
Параметр Тип Описание
*context Context Контекст обработки запроса.
*file_ids INT64 Массив идентификаторов записей.
count int Количество идентификаторов записей.
type_id INT64 Тип записи. Задается разработчиком и зависит от логики модуля. Берется из структуры FileInfo.
record_id INT64 Идентификатор записи, который нужно установить файлам, указанным в массиве file_ids. Берется из структуры FileInfo.

 

Пример добавления текстовой записи в хранилище

В этой статье в качестве примера мы рассмотрим сохранение простого текста в файловом хранилище. Другие варианты работы с файлами мы рассмотрим во второй части нашей статьи. Мы продолжим работать над учебным модулем Hello World. Исходные коды со всеми изменениями вы сможете найти в прилагаемом к статье архиве.

 

1. Получение интерфейса IFilesManager

Для начала в классе менеджера нам нужно получить указатель на объект класса, реализующего интерфейс IFilesManager.

1.1. Объявление

  • CHelloWorldManager
private:
   IFilesManager    *m_files_manager;                  // файловый менеджер

1.2. Реализация

  • Конструктор - CHelloWorldManager::CHelloWorldManager()
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CHelloWorldManager::CHelloWorldManager() : m_server(NULL), m_files_manager(NULL), m_next_id(0)
  {
//---
//---
  }
  • Инициализация модуля - CHelloWorldManager::Initialize(IServer *server, int prev_build)
//---
   if(RES_FAILED(res=m_server->GetInterface(L"IFilesManager",(void**)&m_files_manager)) || m_files_manager==NULL)
      ReturnErrorExt(res,NULL,"failed to get IFilesManager interface");

1.3. Поскольку интерфейс IFilesManager получается один раз при инициализации модуля и далее изменяться не будет, синхронизация не потребуется. И для удобства мы объявим метод, который позволит быстро получать интерфейс при обработке запросов на страницах.

  • CHelloWorldManager
public:
   IFilesManager*    FilesManagerGet() { return(m_files_manager); };

2. Расширение структуры данных

Для хранения записей в файловом хранилище нам потребуется расширить структуру данных, добавив в нее идентификатор записи.

2.1. Добавьте новое поле в таблицу HELLOWORLD. Это будет идентификатор записи.

  • HelloWorld.h
//+------------------------------------------------------------------+
//| Структура записи                                                 |
//+------------------------------------------------------------------+
struct HelloWorldRecord
  {
   INT64             id;
   wchar_t           name[256];
   int               party;
   INT64             description_id;
  }; 
  • CHelloWorldManager::DBTableCheck - Проверка существующих/добавление новых полей в таблицу.
//---
   if(RES_FAILED(res=sql->CheckTable("HELLOWORLD",
       "ID              BIGINT        DEFAULT 0   NOT NULL,"
       "NAME            VARCHAR(256)  DEFAULT ''  NOT NULL,"
       "PARTY           INTEGER       DEFAULT 0   NOT NULL,"
       "DESCRIPTION_ID  BIGINT DEFAULT 0 NOT NULL",
       "PRIMARY KEY (ID)",
       "DESCENDING INDEX IDX_HELLOWORLD_ID_DESC (ID)",
                                             NULL,
                                             NULL,
                                              0)))
      ReturnError(res);

2.2. Соответственным образом обновите SQL-запросы в реализации методов C.R.U.D.

  • CHelloWorldManager::InfoGet - Выборка полей из таблицы с сортировкой.
//--- текст SQL-запроса на выборку записей из таблицы HELLOWORLD с сортировкой по полю ID. 
   char query_select[]="SELECT id,name,party,description_id FROM helloworld ORDER BY id ROWS ? TO ?";
//--- "Привязываем" данные к параметрам запроса
   SqlParam params_query_select[] ={
      SQL_INT64, &rec_info_get.id,            sizeof(rec_info_get.id),
      SQL_WTEXT,  rec_info_get.name,          sizeof(rec_info_get.name),
      SQL_LONG,  &rec_info_get.party,         sizeof(rec_info_get.party),
      SQL_INT64, &rec_info_get.description_id,sizeof(rec_info_get.description_id)
      };
  • CHelloWorldManager::Get - Получение строки из таблицы.
//--- текст SQL-запроса на получение строки из таблицы HELLOWORLD по указанному ID. 
   char query_select_string[]="SELECT id,name,party,description_id FROM helloworld WHERE id=?";

//--- "Привязываем" данные к параметрам запроса
   SqlParam params_query_select_string[] ={
      SQL_INT64, &record->id,            sizeof(record->id),
      SQL_WTEXT,  record->name,          sizeof(record->name),
      SQL_LONG,  &record->party,         sizeof(record->party),
      SQL_INT64, &record->description_id,sizeof(record->description_id)
      };
  • CHelloWorldManager::Update - Сохранение/обновление существующей записи.
//--- текст SQL-запроса на добавление новой записи в таблицу HELLOWORLD
   char query_insert[] ="INSERT INTO helloworld(party,name,description_id,id) VALUES(?,?,?,?)";
//--- текст SQL-запроса на изменение существующей записи в таблице HELLOWORLD
   char query_update[] ="UPDATE helloworld SET party=?,name=?,description_id=? WHERE id=?";
//--- "Привязываем" данные к параметрам запроса
   SqlParam params_query[] ={
      SQL_LONG,  &record->party,         sizeof(record->party),
      SQL_WTEXT,  record->name,          sizeof(record->name),
      SQL_INT64, &record->description_id,sizeof(record->description_id),
      SQL_INT64, &record->id,            sizeof(record->id)
      };

 

3. Работа с файлами в хранилище

В менеджере мы реализуем методы сохранения и удаления данных из хранилища, поскольку данные в нашем примере связаны с записями в СУБД.

3.1. Сохранение записи - CHelloWorldManager::Update. Метод FilesCommit снимает временный флаг с загруженных данных, после чего они хранятся на постоянной основе. Сохраняем данные по указанному идентификатору и связываем их с идентификатором записи в таблице.

   TWRESULT res=RES_S_OK;
...........................
//---
   if(record->description_id!=0 && RES_FAILED(res=m_files_manager->FilesCommit(context,&record->description_id,1,0,record->id)))
      ExtLogger(context,LOG_STATUS_ERROR) << "failed to commit description [" << res << "]";

3.2. Удаление данных - CHelloWorldManager::InfoDelete. По идентификатору записи данные удаляются из хранилища.

   TWRESULT res=RES_S_OK;
   HelloWorldRecord record={0};
//--- проверки
   if(context==NULL || m_server==NULL || m_files_manager==NULL || id<=0) ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)                                                ReturnError(RES_E_INVALID_CONTEXT);
................................
//--- удаляем содержимое описания из файлового хранилища
   if(RES_FAILED(res=m_files_manager->FileDelete(context,record.description_id)))
      ExtLogger(context,LOG_STATUS_ERROR) << "failed to delete record #" << id << " description [" << res << "]";

 

4. HTTP API

В HTTP API мы реализуем методы загрузки данных в хранилище и их чтения из хранилища. Поскольку в модуле Hello World редактирование данных выполняется на странице PageEdit, методы загрузки и чтения данных мы реализуем в классе только этой страницы. Чтение данных на странице PageNumberTwo вы сможете при желании реализовать самостоятельно.

4.1. CPageEdit - Для работы с файловым хранилищем со страницы PageEdit, в классе этой страницы объявите указатель на менеджер файлов. Для вводимого текста объявите соответствующий символьный массив.

private:
   IFilesManager    *m_files_manager;
   wchar_t          *m_description;

4.2. В конструкторе класса страницы PageEdit проинициализируйте указатель на менеджер файлов и массив для строки текста.

  • Конструктор
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CPageEdit::CPageEdit() : m_server(NULL), m_files_manager(NULL), m_description(NULL)
  {
//---
   ZeroMemory(&m_record,sizeof(m_record));
//---
  }
  • Деструктор. Освободите выделенную память ее по завершении обработки страницы.
//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
CPageEdit::~CPageEdit()
  {
   if(m_description!=NULL)
     {
      delete [] m_description;
      m_description=NULL;
     }
//---
   m_server       =NULL;
   m_files_manager=NULL;
  }

4.3. При обработке запроса страницы PageEdit получим менеджер файлов.

  • CPageEdit::Process
//---
   m_server       =server;
   m_files_manager=manager->FilesManagerGet();

4.4. Объявите и реализуйте метод сохранения записи в хранилище.

  • Описание. Передаваемые в этот метод строки будут сохраняться в записи файлового хранилища.
private:
   TWRESULT          StoreDescription(const Context *context,const wchar_t *description);
  • Реализация. Перед сохранением текста задаем тип файла, размер файла в байтах, а также указываем MIME-тип для корректной обработки содержимого файла браузером. В методе FileStore последним аргументом указывается флаг FILE_FLAG_TEMP, который помечает загружаемые в хранилище данные как временные.
//+------------------------------------------------------------------+
//| Сохранение описания                                              |
//+------------------------------------------------------------------+
TWRESULT CPageEdit::StoreDescription(const Context *context,const wchar_t *description)
  {
   TWRESULT res=RES_S_OK;
//--- проверки
   if(m_files_manager==NULL || context==NULL || description==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->user==NULL)                                         ReturnError(RES_E_INVALID_CONTEXT);
//---
   FileInfo description_info={0};
//--- заполним структуру с информацией о записи
   description_info.type=TW_FILE_TYPE_HTML;
   description_info.size=(wcslen(description)+1)*sizeof(wchar_t);
   StringCchCopy(description_info.mime_type,_countof(description_info.mime_type),L"text/html");
//--- если записи еще не было заполним другие поля
   if(m_record.description_id==0)
     {
      description_info.type_id  =0;
      description_info.record_id=m_record.id;
     }
//--- запишем данные в файловое хранилище
   if(RES_FAILED(res=m_files_manager->FileStore(context,(void*)description, &description_info, &m_record.description_id, FILE_FLAG_TEMP)))
     {
      ExtLogger(context,LOG_STATUS_ERROR) << "CPageEdit::StoreDescription: failed to store description";
      //---
      return(res);
     }
//---
   return(RES_S_OK);
  }

4.5. Объявите и реализуйте метод чтения/проверки данных из хранилища.

  • Описание
private:
   void              CheckDescription();
  • Реализация. Сначала мы прочитаем информацию о файле, затем выделим для него память, после чего прочитаем туда содержимое файла в указанном размере. В нашем примере текстовые файлы будут небольшого размера, и в данном случае не обязательно задавать для их чтения смещение.

    Если логика модуля будет подразумевать чтение файлов больших размеров, то читать данные записи следует блочно, указывая смещение при вызове метода FileRead.

//+------------------------------------------------------------------+
//| Проверка описания                                                |
//+------------------------------------------------------------------+
void CPageEdit::CheckDescription()
  {
   TWRESULT res=RES_S_OK;
//--- проверки
   if(m_files_manager==NULL) return;
//--- уже прочитали
   if(m_record.description_id==0 || m_description!=NULL) return;
//---
   FileInfo blob_info={0};
//--- 1. получим информацию о записи в файловом хранилище
   if(m_files_manager->FileInfoGet(NULL,m_record.description_id,&blob_info)==RES_S_OK)
     {
      //--- 2. выделяем память для содержимого записи
      UINT64 sz    =UINT64((blob_info.size+sizeof(wchar_t))/sizeof(wchar_t));
      m_description=new (std::nothrow) wchar_t[sz];
      //---
      if(m_description==NULL)
        {
         ExtLogger(NULL, LOG_STATUS_ERROR) << "CPageEdit::CheckDescription: failed to allocate memory";
         return;
        }
      //---
      ZeroMemory(m_description,size_t(blob_info.size));
      //--- 3. читаем содержимое записи в память
      sz *= sizeof(wchar_t);
      if(RES_FAILED(res=m_files_manager->FileRead(NULL,m_record.description_id,m_description,&sz,0)))
         ExtLogger(NULL, LOG_STATUS_ERROR) << "CPageEdit::CheckDescription: failed to load file record [" << res << "]";
     }
  }

4.6. В метод сохранения записи в СУБД CPageEdit::OnUpdate добавим вызов метода сохранения данных в хранилище, который мы до этого реализовали. Текст мы будем передавать с помощью токена description.

//---
   if(RES_FAILED(res=StoreDescription(context,context->request->GetString(IRequest::POST,L"description"))))
      ExtLogger(context,LOG_STATUS_ERROR) << "failed to store description";

4.7. В методе обработки токена CPageEdit::Tag реализуйте токен с именем description.

//---
   if(TagCompare(L"description",tag))
     {
      CheckDescription();
      //---
      if(m_description!=NULL && m_description[0]!=NULL)
        {
         context->response->WriteSafe(m_description,IResponse::REPLACE_JAVASCRIPT);
        }
      //---
      return(false);
     }

5. Пользовательский интерфейс

В пользовательском интерфейсе редактирования записей таблицы HELLOWORLD (шаблон edit.tpl) добавим поле для ввода текста, который будет сохраняться в файловом хранилище. В качестве контейнера вводимого текста будет использоваться токен description, реализованный ранее в классе страницы PageEdit.

5.1. В элементе управления Form добавьте новый элемент в массив параметров для ключа items. Здесь мы воспользуемся элементом управления Input.Textarea, который добавляет поле ввода для многострочного текста.

      items   : [
                  [
                     TeamWox.Control('Label','<lngj:HELLOWORLD_NAME>','name'),
                     TeamWox.Control('Input','text','name','<tw:name />')
                  ],
                  [
                     TeamWox.Control('Label','<lngj:HELLOWORLD_PARTY>','party'), 
                     TeamWox.Control('Input','combobox','party','<tw:party />', {
                        options : [
                           [0,'<lngj:HELLOWORLD_REPORT_REPUBLICAN />'], 
                           [1,'<lngj:HELLOWORLD_REPORT_DEMOCRATIC />'],
                           [2,'<lngj:HELLOWORLD_REPORT_DEMOREP />'],
                           [3,'<lngj:HELLOWORLD_REPORT_FEDERALIST />'],
                           [4,'<lngj:HELLOWORLD_REPORT_WHIG />'],
                           [5,'<lngj:HELLOWORLD_REPORT_NATUNION />'],
                           [6,'<lngj:HELLOWORLD_REPORT_NOPARTY />']
                     ]})
                  ],
                  [
                     TeamWox.Control('Label','<lngj:HELLOWORLD_DESCRIPTION>','description'),
                     TeamWox.Control('Input','textarea','description','<tw:description />').Style({height: "120px"})
                  ]
                ]

5.2. Добавьте перевод для текстовой метки к новому полю ввода.

[eng]
;---
HELLOWORLD_NAME                ="Name"
HELLOWORLD_PARTY               ="Party"
HELLOWORLD_DESCRIPTION         ="Description"

[rus]
;---
HELLOWORLD_NAME                ="Имя"
HELLOWORLD_PARTY               ="Партия"
HELLOWORLD_DESCRIPTION         ="Описание"

5.3. Скомпилируйте модуль, обновите шаблон на сервере и запустите TeamWox. На странице редактирования записи введите текст и сохраните его.

Сохранение текста как файла в хранилище

Данные успешно сохранились в файловом хранилище. Об этом говорит отсутствие сообщений об ошибках. Также вы можете убедиться в этом по изменившемуся размеру и дате файла <сервер_TeamWox>\data\helloworld\1\1.dat.

Для удобства разработки вы можете также снабжать реализацию методов интерфейса IFilesManager отладочными сообщениями с помощью класса CSmartLogger.

 

Заключение

Мы рассмотрели, что такое файловое хранилище и как с ним взаимодействовать через модули. На примере модуля Hello World вы научились сохранять простой текст в виде файла в хранилище.

Во второй части статьи мы рассмотрим реальные примеры использования файлового хранилища в модулях TeamWox. Вы научитесь добавлять комментарии к записям, прикреплять файлы к сообщениям и вставлять различные файлы в WYSIWYG-редакторе TeamWox.


helloworld-filestorage-part1-ru.zip (171.75 KB)

2011.01.28