Взаимодействие PostgreSQL с внешним сервисом для хранения изображений


    Доброго времени суток. При работе с базой данных для сайта иногда возникает ситуация, когда приходится выбирать, как и где хранить изображения. Среди возможных вариантов, как правило, имеются следующие:
    • изображения находятся целиком в БД
    • изображения находятся в файловой системе, в БД хранится имя файла
    • изображения находятся во внешнем специализированном сервисе

    Хоть PostgreSQL и предоставляет возможность хранения в БД файлов (непосредственно в bytea полях или через large objects), это наименее оптимальный вариант, как в плане скорости, так и потребляемой памяти. Другой, общепринятой практикой, является хранение изображений в виде файлов на диске, для сайта формируется путь к изображению. Из преимуществ — возможность кеширования или использование специализированной файловой системы. И третий вариант — для изображений выделяется отдельный сервис, в котором может быть кеширование, маштабирование на лету, изменение формата. Попробуем реализовать взаимодействие PostgreSQL с таким сервисом.

    Реализация


    Обрисуем немного картину происходящего. У нас имеется http-сервис, по типу этого, для изображений, поддерживающий такие команды:
    • загрузка изображения — отправка POST-запроса с формой, в ответ приходит JSON с некоторой информацией об изображении, среди которой сгенерированный идентификатор
    • получение изображения — отправка GET-запроса c идентификатором изображения my.service.local/1001
    • удаление изображения — отправка DELETE-запроса c идентификатором изображения my.service.local/1001

    В БД будут хранится идентификаторы изображений, в таком случае, на страницах сайта можно будет втраивать теги вида:
    <img src="http://my.service.local/1001"/>

    Со стороны пользователя загрузка изображения (равно как сохранение и удаление) должна выглядеть как вызов функции upload_image (с параметром filename), которая возращает идентификатор изображения в сервисе, записуемый затем в таблицу. Так как напрямую из PostgreSQL нельзя доступится к http запросам, необходимо реализовывать требуемый функционал на хранимых функциях на С, а в них уже есть где разгулятся. Для простоты, обойдёмся библиотеками curl и jansson (последняя для работы с JSON). Можем начинать.

    Определим в заголовочном файле barberry_impl.h наши прототипы функций:
    // get last error
    char* barberry_error();
    
    // upload file to BarBerry's service and return ID
    int barberry_upload_file(const char *host, const char *filename);
    
    // download file from BarBerry's service by ID
    int barberry_download_file(const char *host, int id, const char *filename);
    
    // delete file from BarBerry's service by ID
    int barberry_delete_file(const char *host, int id);
    

    В файле с исходном кодом barberry_impl.c поместим следующие глобальные переменные:
    char last_error[1024];
    FILE *file = NULL;
    int result = 0;
    

    Переменная last_error будет хранить последнюю ошибку, file — это указатель на файл, создаваемый при получении данных от сервиса, а в result будет сохранятся результат функций работы с сервисом.

    Реализация функции barberry_error тривиальна — возврат last_error. Разберем подробно функцию barberry_upload_file.

    Перед тем, как начать работу с библиотекой curl, необходимо проинициализировать окружение для неё (командой curl_gobal_init) и создать сессию (командой curl_easy_init, возращающей указатель на хэндл сессии). Далее, создаем submit-форму (через curl_formadd) и заполняем следующие опции:
    • CURLOPT_URL — хост, с которым мы работаем
    • CURLOPT_HTTPPOST — форма, отправляемая методом POST
    • CURLOPT_WRITEFUNCTION — CALLBACK-функция для ответа от хоста

    Реализация barberry_upload_file:
    int barberry_upload_file(const char *host, const char *filename)
    {
    	result = -1;
    
    	curl_global_init(CURL_GLOBAL_ALL);
    
    	CURL *curl = curl_easy_init();
    
    	if (curl)
    	{
    		curl_easy_setopt(curl, CURLOPT_URL, host);
    
    		struct curl_httppost *httppost = NULL;
    		struct curl_httppost *last_ptr = NULL;
    
    		curl_formadd(&httppost, &last_ptr, CURLFORM_COPYNAME, "sendfile", CURLFORM_FILE, filename, CURLFORM_END);
    		curl_formadd(&httppost, &last_ptr, CURLFORM_COPYNAME, "submit", CURLFORM_COPYCONTENTS, "send", CURLFORM_END);
    
    		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, upload_response);
    		curl_easy_setopt(curl, CURLOPT_HTTPPOST, httppost);
    
    		CURLcode res = curl_easy_perform(curl);
    
    		if (res != CURLE_OK)
    		{
    			sprintf(last_error, "%s", curl_easy_strerror(res));
    		}
    
    		curl_easy_cleanup(curl);
    		curl_formfree(httppost);
    	}
    
    	return result;
    }
    

    CALLBACK-функция upload_response имеет прототип:
    size_t function(char *ptr, size_t size, size_t nmemb, void *userdata);
    

    с параметрами:
    • ptr — указатель на получаемые данные
    • size * nmemb — их размер
    • userdata — указатель на FILE*, при необходимости устанавливаемый через опцию CURLOPT_WRITEDATA

    Функция должна возратить фактической размер обработанных данных, т.е. size * nmemb. В данном, в этой функции необходимо распарсить JSON передаваемый в ответе:
    size_t upload_response(char *ptr, size_t size, size_t nmemb, void *userdata)
    {
    	(void)userdata;
    
    	parse_upload_response(ptr);
    
    	return size * nmemb;
    }
    

    Поручим это другой функции, в которой используем jansson для разбора ответа:
    void parse_upload_response(const char *text)
    {
    	if (!strcmp(text, "{}"))
    	{
    		sprintf(last_error, "%s", "Empty file");
    
    		return;
    	}
    
    	json_error_t error;
    
    	json_t *root = json_loads(text, 0, &error);
    
    	if (!root)
    	{
    		sprintf(last_error, "%s", text);
    
    		return;
    	}
    
    	json_t *id = json_object_get(root, "id");
    
    	if(!json_is_integer(id))
    	{
    		sprintf(last_error, "%s", text);
    
    		json_decref(root);
    
    		return;
    	}
    
    	result = json_integer_value(id);
    
    	json_decref(root);
    }
    

    В случае пустого файла, нам прийдёт ответ {}, обработаем этот случай. Если всё в порядке, файл был успешно загружен ответ прийдет в виде: { «id»:1001, «ext»:«png»… }. Интересует только id, его и записываем в result.

    Функция для сохранения файла немного проще — нужно лишь сформировать GET-запрос, получить ответ и записать его в файл (обработав ситуацию, когда файл с нужным id не найден):
    barberry_download_file
    int barberry_download_file(const char *host, int id, const char *filename)
    {
    	result = 0;
    
    	file = fopen(filename, "wb");
    
    	if (!file)
    	{
    		sprintf(last_error, "%s", "Can't create file");
    
    		return -1;
    	}
    
    	curl_global_init(CURL_GLOBAL_ALL);
    
    	CURL *curl = curl_easy_init();
    
    	if (curl)
    	{
    		char buffer[1024];
    
    		sprintf(buffer, "%s/%d", host, id);
    
    		curl_easy_setopt(curl, CURLOPT_URL, buffer);
    		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, download_response);
    
    		CURLcode res = curl_easy_perform(curl);
    
    		if (res != CURLE_OK)
    		{
    			sprintf(last_error, "%s", curl_easy_strerror(res));
    
    			result = -1;
    		}
    
    		curl_easy_cleanup(curl);
    	}
    
    	fclose(file);
    
    	return result;
    }
    


    download_response
    size_t download_response(char *ptr, size_t size, size_t nmemb, void *userdata)
    {
    	(void)userdata;
    
    	if (!strcmp(ptr, "{}"))
    	{
    		sprintf(last_error, "%s", "File on server not found");
    
    		result = -1;
    	}
    	else
    	{
    		fwrite(ptr, size * nmemb, 1, file);
    	}
    
    	return size * nmemb;
    }
    


    Удаление файла в сервисе — это DELETE-запрос (тип запроса для curl устанавливается через опцию CURLOPT_CUSTOMREQUEST):
    barberry_delete_file
    int barberry_delete_file(const char *host, int id)
    {
    	result = 0;
    
    	curl_global_init(CURL_GLOBAL_ALL);
    
    	CURL *curl = curl_easy_init();
    
    	if (curl)
    	{
    		char buffer[1024];
    
    		sprintf(buffer, "%s/%d", host, id);
    
    		curl_easy_setopt(curl, CURLOPT_URL, buffer);
    		curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, delete_response);
    
    		CURLcode res = curl_easy_perform(curl);
    
    		if (res != CURLE_OK)
    		{
    			sprintf(last_error, "%s", curl_easy_strerror(res));
    
    			result = -1;
    		}
    
    		curl_easy_cleanup(curl);
    	}
    
    	return result;
    }
    


    delete_response
    size_t delete_response(char *ptr, size_t size, size_t nmemb, void *userdata)
    {
    	(void)ptr;
    	(void)userdata;
    
    	return size * nmemb;
    }
    


    Прежде чем перейти к PostgreSQL-части, напишем небольшую консольную утилиту для тестирования наших функций. В ней проверяем переданные параметры, если они соответствуют ожидаемым (пример в print_help), то делаем нужные действия:
    barberry_test.c
    #include "barberry_impl.h"
    
    void print_help()
    {
    	fprintf(stdout, "Usage:\n");
    	fprintf(stdout, "  bbtest upload my.service.local /home/username/image1000.png\n");
    	fprintf(stdout, "  bbtest download my.service.local 1000 /home/username/image1000.png\n");
    	fprintf(stdout, "  bbtest delete my.service.local 1000\n\n");
    }
    
    int main(int argc, char *argv[])
    {
    	(void)argc;
    	(void)argv;
    
    	if (argc <= 2)
    	{
    		print_help();
    
    		return 0;
    	}
    
    	if (!strcmp(argv[1], "upload"))
    	{
    		if (argc != 4)
    		{
    			print_help();
    
    			return 0;
    		}
    
    		int id = barberry_upload_file(argv[2], argv[3]);
    
    		if (id != -1)
    		{
    			fprintf(stdout, "File uploaded with id %d\n", id);
    		}
    		else
    		{
    			fprintf(stderr, "%s\n", barberry_error());
    		}
    	}
    	else if (!strcmp(argv[1], "download"))
    	{
    		if (argc != 5)
    		{
    			print_help();
    
    			return 0;
    		}
    
    		int result = barberry_download_file(argv[2], atoi(argv[3]), argv[4]);
    
    		if (result != -1)
    		{
    			fprintf(stdout, "%s\n", "File downloaded");
    		}
    		else
    		{
    			fprintf(stderr, "%s\n", barberry_error());
    		}
    	}
    	else if (!strcmp(argv[1], "delete"))
    	{
    		if (argc != 4)
    		{
    			print_help();
    
    			return 0;
    		}
    
    		int result = barberry_delete_file(argv[2], atoi(argv[3]));
    
    		if (result != -1)
    		{
    			fprintf(stdout, "%s\n", "File deleted");
    		}
    		else
    		{
    			fprintf(stderr, "%s\n", barberry_error());
    		}
    	}
    	else
    	{
    		print_help();
    	}
    
    	return 0;
    }
    


    Собираем всё это дело (пути в Вашей ОС к заголовочным файлам и библиотекам могут отличатся) и тестируем:
    cc -c barberry_impl.c
    cc -c barberry_test.c
    cc -L/usr/lib -lcurl -ljansson -o bbtest barberry_test.o barberry_impl.o
    ./bbtest upload my.service.local ~/picture01.png
    File uploaded with id 1017
    

    Если всё в порядке, можно переходит к PostgreSQL-части нашей библиотеки (подробней о хранимых функциях на C в PostgreSQL описано в [4]).

    Обьявим экспортируемые для БД функции (с версией 1):
    PG_FUNCTION_INFO_V1(bb_upload_file);
    PG_FUNCTION_INFO_V1(bb_download_file);
    PG_FUNCTION_INFO_V1(bb_delete_file);
    

    Для конвертирования из text (тип в PostgreSQL) в c-string поможет небольшая функция:
    char* text_to_string(text *txt)
    {
    	size_t size = VARSIZE(txt) - VARHDRSZ;
    
    	char *buffer = (char*)palloc(size + 1);
    
    	memcpy(buffer, VARDATA(txt), size);
    
    	buffer[size] = '\0';
    
    	return buffer;
    }
    

    Реализация экспортируемых функций заключается в делегировании написанному ранее функционалу, с генерацией ошибки, если что-то пошло не так:
    bb_upload_file
    Datum bb_upload_file(PG_FUNCTION_ARGS)
    {
    	char *host = text_to_string(PG_GETARG_TEXT_P(0));
    	char *filename = text_to_string(PG_GETARG_TEXT_P(1));
    
    	int result = barberry_upload_file(host, filename);
    
    	if (result == -1)
    	{
    		elog(ERROR, "%s", barberry_error());
    	}
    
    	pfree(host);
    	pfree(filename);
    
    	PG_RETURN_INT32(result);
    }
    


    bb_download_file
    Datum bb_download_file(PG_FUNCTION_ARGS)
    {
    	char *host = text_to_string(PG_GETARG_TEXT_P(0));
    	int id = PG_GETARG_INT32(1);
    	char *filename = text_to_string(PG_GETARG_TEXT_P(2));
    
    	int result = barberry_download_file(host, id, filename);
    
    	if (result == -1)
    	{
    		elog(ERROR, "%s", barberry_error());
    	}
    
    	pfree(host);
    	pfree(filename);
    
    	PG_RETURN_VOID();
    }
    


    bb_delete_file
    Datum bb_delete_file(PG_FUNCTION_ARGS)
    {
    	char *host = text_to_string(PG_GETARG_TEXT_P(0));
    	int id = PG_GETARG_INT32(1);
    
    	int result = barberry_delete_file(host, id);
    
    	if (result == -1)
    	{
    		elog(ERROR, "%s", barberry_error());
    	}
    
    	pfree(host);
    
    	PG_RETURN_VOID();
    }
    


    Собираем динамическую библиотеку и копируем ее к PostgreSQL (пути в Вашей ОС к заголовочным файлам и библиотекам могут отличатся):
    rm -rf *.o
    cc -I/usr/include/postgresql/server -fpic -c barberry.c
    cc -I/usr/include/postgresql/server -fpic -c barberry_impl.c
    cc -L/usr/lib -lpq -lcurl -ljansson -shared -o barberry.so barberry.o barberry_impl.o
    cp *.so /usr/lib/postgresql
    

    SQL-функции, создаваемые в БД, имеют вид:
    CREATE OR REPLACE FUNCTION public.bb_upload_file ( p_host text, p_filename text )
    RETURNS integer AS
    'barberry', 'bb_upload_file'
    LANGUAGE c VOLATILE STRICT;
    
    CREATE OR REPLACE FUNCTION public.bb_download_file ( p_host text, p_id integer, p_filename text )
    RETURNS void AS
    'barberry', 'bb_download_file'
    LANGUAGE c VOLATILE STRICT;
    
    CREATE OR REPLACE FUNCTION public.bb_delete_file ( p_host text, p_id integer )
    RETURNS void AS
    'barberry', 'bb_delete_file'
    LANGUAGE c VOLATILE STRICT;
    

    Оформим динамическую библиотеку и SQL-скрипт в виде расширения к PostgreSQL (подробнее описано в [5]). Для этого потребуется управляющий файл barberry.control:
    # BarBerry image service
    comment = 'BarBerry image service'
    default_version = '1.0'
    module_pathname = '$libdir/barberry'
    relocatable = true
    

    SQL-скрипт для нашего расширения необходимо назвать как barberry--1.0.sql (согласно документации PostgreSQL). Скопируем эти два файла туда, где PostgreSQL хранить свои расширения.

    Создание и использование расширения предельно простое:
    CREATE EXTENSION barberry;
    UPDATE avatar SET image = bb_upload_file ( 'my.service.local', 'images/avatar_admin.png' ) WHERE name = 'admin';
    

    Исходные файлы


    Библиотека выша как небольшая утилита, поэтому не размещена на github. Для облегчения сборки добавлен Makefile с целями barberry, barberry_test, clean, rebuild, install.
    barberry_impl.h
    #ifndef BARBERRY_IMPL_H
    #define BARBERRY_IMPL_H
    
    #include <stdio.h>
    #include <string.h>
    #include <curl/curl.h>
    #include <jansson.h>
    
    // get last error
    char* barberry_error();
    
    // upload file to BarBerry's service and return ID
    int barberry_upload_file(const char *host, const char *filename);
    
    // download file from BarBerry's service by ID
    int barberry_download_file(const char *host, int id, const char *filename);
    
    // delete file from BarBerry's service by ID
    int barberry_delete_file(const char *host, int id);
    
    #endif // BARBERRY_IMPL_H
    


    barberry_impl.c
    #include "barberry_impl.h"
    
    char last_error[1024];
    FILE *file = NULL;
    int result = 0;
    
    void parse_upload_response(const char *text)
    {
    	if (!strcmp(text, "{}"))
    	{
    		sprintf(last_error, "%s", "Empty file");
    
    		return;
    	}
    
    	json_error_t error;
    
    	json_t *root = json_loads(text, 0, &error);
    
    	if (!root)
    	{
    		sprintf(last_error, "%s", text);
    
    		return;
    	}
    
    	json_t *id = json_object_get(root, "id");
    
    	if(!json_is_integer(id))
    	{
    		sprintf(last_error, "%s", text);
    
    		json_decref(root);
    
    		return;
    	}
    
    	result = json_integer_value(id);
    
    	json_decref(root);
    }
    
    size_t upload_response(char *ptr, size_t size, size_t nmemb, void *userdata)
    {
    	(void)userdata;
    
    	parse_upload_response(ptr);
    
    	return size * nmemb;
    }
    
    size_t download_response(char *ptr, size_t size, size_t nmemb, void *userdata)
    {
    	(void)userdata;
    
    	if (!strcmp(ptr, "{}"))
    	{
    		sprintf(last_error, "%s", "File on server not found");
    
    		result = -1;
    	}
    	else
    	{
    		fwrite(ptr, size * nmemb, 1, file);
    	}
    
    	return size * nmemb;
    }
    
    size_t delete_response(char *ptr, size_t size, size_t nmemb, void *userdata)
    {
    	(void)ptr;
    	(void)userdata;
    
    	return size * nmemb;
    }
    
    
    char* barberry_error()
    {
    	return last_error;
    }
    
    int barberry_upload_file(const char *host, const char *filename)
    {
    	result = -1;
    
    	curl_global_init(CURL_GLOBAL_ALL);
    
    	CURL *curl = curl_easy_init();
    
    	if (curl)
    	{
    		curl_easy_setopt(curl, CURLOPT_URL, host);
    
    		struct curl_httppost *httppost = NULL;
    		struct curl_httppost *last_ptr = NULL;
    
    		curl_formadd(&httppost, &last_ptr, CURLFORM_COPYNAME, "sendfile", CURLFORM_FILE, filename, CURLFORM_END);
    		curl_formadd(&httppost, &last_ptr, CURLFORM_COPYNAME, "submit", CURLFORM_COPYCONTENTS, "send", CURLFORM_END);
    
    		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, upload_response);
    		curl_easy_setopt(curl, CURLOPT_HTTPPOST, httppost);
    
    		CURLcode res = curl_easy_perform(curl);
    
    		if (res != CURLE_OK)
    		{
    			sprintf(last_error, "%s", curl_easy_strerror(res));
    		}
    
    		curl_easy_cleanup(curl);
    		curl_formfree(httppost);
    	}
    
    	return result;
    }
    
    int barberry_download_file(const char *host, int id, const char *filename)
    {
    	result = 0;
    
    	file = fopen(filename, "wb");
    
    	if (!file)
    	{
    		sprintf(last_error, "%s", "Can't create file");
    
    		return -1;
    	}
    
    	curl_global_init(CURL_GLOBAL_ALL);
    
    	CURL *curl = curl_easy_init();
    
    	if (curl)
    	{
    		char buffer[1024];
    
    		sprintf(buffer, "%s/%d", host, id);
    
    		curl_easy_setopt(curl, CURLOPT_URL, buffer);
    		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, download_response);
    
    		CURLcode res = curl_easy_perform(curl);
    
    		if (res != CURLE_OK)
    		{
    			sprintf(last_error, "%s", curl_easy_strerror(res));
    
    			result = -1;
    		}
    
    		curl_easy_cleanup(curl);
    	}
    
    	fclose(file);
    
    	return result;
    }
    
    int barberry_delete_file(const char *host, int id)
    {
    	result = 0;
    
    	curl_global_init(CURL_GLOBAL_ALL);
    
    	CURL *curl = curl_easy_init();
    
    	if (curl)
    	{
    		char buffer[1024];
    
    		sprintf(buffer, "%s/%d", host, id);
    
    		curl_easy_setopt(curl, CURLOPT_URL, buffer);
    		curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, delete_response);
    
    		CURLcode res = curl_easy_perform(curl);
    
    		if (res != CURLE_OK)
    		{
    			sprintf(last_error, "%s", curl_easy_strerror(res));
    
    			result = -1;
    		}
    
    		curl_easy_cleanup(curl);
    	}
    
    	return result;
    }
    


    barberry.c
    #include <postgres.h>
    #include <fmgr.h>
    
    #include "barberry_impl.h"
    
    #ifdef PG_MODULE_MAGIC
    	PG_MODULE_MAGIC;
    #endif
    
    PG_FUNCTION_INFO_V1(bb_upload_file);
    PG_FUNCTION_INFO_V1(bb_download_file);
    PG_FUNCTION_INFO_V1(bb_delete_file);
    
    char* text_to_string(text *txt)
    {
    	size_t size = VARSIZE(txt) - VARHDRSZ;
    
    	char *buffer = (char*)palloc(size + 1);
    
    	memcpy(buffer, VARDATA(txt), size);
    
    	buffer[size] = '\0';
    
    	return buffer;
    }
    
    Datum bb_upload_file(PG_FUNCTION_ARGS)
    {
    	char *host = text_to_string(PG_GETARG_TEXT_P(0));
    	char *filename = text_to_string(PG_GETARG_TEXT_P(1));
    
    	int result = barberry_upload_file(host, filename);
    
    	if (result == -1)
    	{
    		elog(ERROR, "%s", barberry_error());
    	}
    
    	pfree(host);
    	pfree(filename);
    
    	PG_RETURN_INT32(result);
    }
    
    Datum bb_download_file(PG_FUNCTION_ARGS)
    {
    	char *host = text_to_string(PG_GETARG_TEXT_P(0));
    	int id = PG_GETARG_INT32(1);
    	char *filename = text_to_string(PG_GETARG_TEXT_P(2));
    
    	int result = barberry_download_file(host, id, filename);
    
    	if (result == -1)
    	{
    		elog(ERROR, "%s", barberry_error());
    	}
    
    	pfree(host);
    	pfree(filename);
    
    	PG_RETURN_VOID();
    }
    
    Datum bb_delete_file(PG_FUNCTION_ARGS)
    {
    	char *host = text_to_string(PG_GETARG_TEXT_P(0));
    	int id = PG_GETARG_INT32(1);
    
    	int result = barberry_delete_file(host, id);
    
    	if (result == -1)
    	{
    		elog(ERROR, "%s", barberry_error());
    	}
    
    	pfree(host);
    
    	PG_RETURN_VOID();
    }
    


    barberry_test.c
    #include "barberry_impl.h"
    
    void print_help()
    {
    	fprintf(stdout, "Usage:\n");
    	fprintf(stdout, "  bbtest upload my.service.local /home/username/image1000.png\n");
    	fprintf(stdout, "  bbtest download my.service.local 1000 /home/username/image1000.png\n");
    	fprintf(stdout, "  bbtest delete my.service.local 1000\n\n");
    }
    
    int main(int argc, char *argv[])
    {
    	(void)argc;
    	(void)argv;
    
    	if (argc <= 2)
    	{
    		print_help();
    
    		return 0;
    	}
    
    	if (!strcmp(argv[1], "upload"))
    	{
    		if (argc != 4)
    		{
    			print_help();
    
    			return 0;
    		}
    
    		int id = barberry_upload_file(argv[2], argv[3]);
    
    		if (id != -1)
    		{
    			fprintf(stdout, "File uploaded with id %d\n", id);
    		}
    		else
    		{
    			fprintf(stderr, "%s\n", barberry_error());
    		}
    	}
    	else if (!strcmp(argv[1], "download"))
    	{
    		if (argc != 5)
    		{
    			print_help();
    
    			return 0;
    		}
    
    		int result = barberry_download_file(argv[2], atoi(argv[3]), argv[4]);
    
    		if (result != -1)
    		{
    			fprintf(stdout, "%s\n", "File downloaded");
    		}
    		else
    		{
    			fprintf(stderr, "%s\n", barberry_error());
    		}
    	}
    	else if (!strcmp(argv[1], "delete"))
    	{
    		if (argc != 4)
    		{
    			print_help();
    
    			return 0;
    		}
    
    		int result = barberry_delete_file(argv[2], atoi(argv[3]));
    
    		if (result != -1)
    		{
    			fprintf(stdout, "%s\n", "File deleted");
    		}
    		else
    		{
    			fprintf(stderr, "%s\n", barberry_error());
    		}
    	}
    	else
    	{
    		print_help();
    	}
    
    	return 0;
    }
    


    barberry--1.0.sql
    CREATE OR REPLACE FUNCTION public.bb_upload_file ( p_host text, p_filename text )
    RETURNS integer AS
    'barberry', 'bb_upload_file'
    LANGUAGE c VOLATILE STRICT;
    
    CREATE OR REPLACE FUNCTION public.bb_download_file ( p_host text, p_id integer, p_filename text )
    RETURNS void AS
    'barberry', 'bb_download_file'
    LANGUAGE c VOLATILE STRICT;
    
    CREATE OR REPLACE FUNCTION public.bb_delete_file ( p_host text, p_id integer )
    RETURNS void AS
    'barberry', 'bb_delete_file'
    LANGUAGE c VOLATILE STRICT
    


    barberry.control
    # BarBerry image service
    comment = 'BarBerry image service'
    default_version = '1.0'
    module_pathname = '$libdir/barberry'
    relocatable = true
    


    Makefile
    #################################
    # Makefile for barberry library #
    #################################
    
    # options
    
    CC=cc
    CFLAGS=-fpic -c
    INCLUDEPATH=-I/usr/include/postgresql/server
    LIBS=-L/usr/lib -lpq -lcurl -ljansson
    
    # targets
    
    all: barberry barberry_test
    
    barberry: barberry.o barberry_impl.o
    	$(CC) $(LIBS) -shared -o barberry.so barberry.o barberry_impl.o
    
    barberry_test: barberry_test.o barberry_impl.o
    	$(CC) $(LIBS) -o bbtest barberry_test.o barberry_impl.o
    
    barberry.o:
    	$(CC) $(INCLUDEPATH) $(CFLAGS) barberry.c
    
    barberry_impl.o:
    	$(CC) $(INCLUDEPATH) $(CFLAGS) barberry_impl.c
    
    barberry_test.o:
    	$(CC) $(INCLUDEPATH) $(CFLAGS) barberry_test.c
    
    clean:
    	rm -rf *.o *.so bbtest
    
    rebuild: clean all
    
    install:
    	cp *.so /usr/lib/postgresql
    	cp *.control /usr/share/postgresql/extension
    	cp *.sql /usr/share/postgresql/extension
    


    Примечания


    • так как динамическая библиотека загружается от имени postgres (пользователь по умолчанию для СУБД), он же должен иметь доступ к загружаемым файлам и право на создание сохраняемых файлов
    • можно расширить идею, сделав интерфейс для доступа к curl из PostgreSQL, прикрутив описание формы, заголовков и всего прочего в XML-формате, распарсивая потом в C-коде и выполняя соответствующие команды в curl

    Список литературы


    1. Документация по PostgreSQL.
    2. Документация по curl.
    3. Документация по jansson.
    4. Хранимые функции на C в PostgreSQL.
    5. Создание расширений в PostgreSQL.
    Метки:
    Поделиться публикацией
    Комментарии 11
    • 0
      Если уж и идти путем хранимых процедур, json/http гораздо проще сделать на plpython/plperl. Вы смотрели в их сторону?
      • –1
        Честно говоря, нет, так как мне ближе C/C++.
    • +6
      Я не смог в посте найти ответа на главный вопрос — почему логика работы с изображениями находится в БД? У Вас вся бизнес логика в базе? Или Вы пишите некий фреймворк для каких-то целей?
      В начале поста я увидел доводы только за внешний сервис работы с изображениями.
      • 0
        На счёт работы с изображениями спора нет, но на счёт бизнес-логики в БД хотел бы уточнить, что в этом плохого?
        • 0
          Ну, если логика специфичная (например, нам надо переворотить миллионы записей из разных таблиц — с точки зрения производительности), имеет смысл переваривать данные ближе к источнику и не выносить в какой-нибудь бекэнд.
          А если в БД находится __вся__ логика, то весь проект сильнее рискует превратиться в набор лапши, чем написанная на какой-нибудь Джаве. Нет поддержки IDE в написании кода, мало библиотек (если они вообще есть).
          Если короче — много рисков.
          • 0
            Как показывает практика, поддерживать логику на уровне БД гораздо проще, нежели выносить это на клиентскую сторону. Не знаю, может мы о разных вещах говорим, просто я веду биллинг на PGSQL, с моей стороны это видится так.
            • 0
              Имхо, это зависит от логики, её объёма и в особенности от правильности рук того, кто пишет эту логику.
              Если от Вашего проекта счастливы разработчики, клиенты, и бизнес получает кучу бабла, то Вы определённо на правильном пути. :)
              • 0
                Ну как бы не жаловались :)
                Вообще свалили всё на меня. И БД, и клиента. Благо тянуть это не сложно, и объёмы небольшие, и бизнес-процессы несложные. Зато интересно :)
      • 0
        Вообще, это требуется для только для загрузки изображений в сервис, с учетом того, что имена файлов находятся уже в БД:
        UPDATE avatar SET image = bb_upload_file ( 'my.service.local', filename );
        

        Возможно, проще это было сделать скриптом на PHP, так что понимайте это как еще один способ.

        У Вас вся бизнес логика в базе?

        Можно сказать, что да.
    • 0
      .
    • +1
      Вопрос «зачем» уже был задан, так что повторяться не буду.

      Как ведет себя PostgreSQL при обработке таких запросов?
      UPDATE должен же блокировать строку / таблицу при выполнении.
      Получается, что если сервис картинок перегружен / лег отдохнуть, то запрос блокирует таблицу на достаточно длительный период?

      Да и в обычном режиме, присутствует неопределенная задержка в скрипте, который блокирует таблицу…

      P.S. после прочтение статьи теги доставляют отдельное удовольствие :)

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.