Введение
Для хранения и работы с данными в системе 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.
- Как добавить готовый модуль в TeamWox
- Как добавить страницу в модуль TeamWox
- Построение пользовательского интерфейса
- Взаимодействие с СУБД
- Создание пользовательских отчетов
- Файловое хранилище - Часть 1
- Файловое хранилище - Часть 2
- Настройка окружения пользовательских модулей - Часть 1
- Настройка окружения пользовательских модулей - Часть 2
- Поиск и фильтрация - Часть 1
- Поиск и фильтрация - Часть 2
- Настройка "Онлайн-консультанта" на вашем сайте
- Как создать дополнительный языковой пакет для TeamWox
2011.01.28