Асинхронная запись и чтение файла без использования модальных методов и временных файлов




Принцип обмена данными из 1С с сайтом (на MySQL) и выдачи (публикации) этих данных по запросу.
PHP-Скрипт автоматической загрузки данных из файла данных в формате CSV в базу данных сайта работающего на WordPress.

В продолжение моей темы: 1С:Альфа-Авто Автосалон Автосервис: обмен с сайтом.
С помощью данного скрипта можно загружать в автоматическом режиме, по расписанию, данные сервисных книжек (ремонтов авто) из 1С:Альфа-Авто Автосалон Автосервис.
Также можно загружать данные в ручном режиме: для этого делается скрытая страница, где размещается специальная кнопка.
Комментарии размещенные внутри скрипта разъяснят логику и порядок действия.
Комментарии с "/////    echo" использовались для отладки.
Дополнительно создана таблица для журналирования результатов загрузки данных.
Скрипт включает в себя защиту от SQL инъекций (думаю безопасность соблюдена в полной мере).
В кратце:
1. Пишется скрипт, который запускает этот.
2. Создается регламентное задание в WordPress, по которому запускается скрипт из п.1. 
3. Этот скрипт осуществляет проверку на существование файла обмена в папке.
4. Если данные не новые, загрузка не производится.
5. Если данные новые, очищается таблица сервисных книжек.
6. Загружаются новые данные.

Собственно сам скрипт:

<?php // Полная загрузка сервисных книжек, создан 2024-01-05 12:44:55

global $wpdb2;
global $failure;
global $file_hist;

/////  echo '<H2><b>Старт загрузки</b></H2><br>';

$failure=FALSE;
//подключаемся к базе
$wpdb2 = include_once 'connection.php'; ; // подключаемся к MySQL
// если не удалось подключиться, и нужно оборвать PHP с сообщением об этой ошибке
if (!empty($wpdb2->error))
{
/////   echo '<H2><b>Ошибка подключения к БД, завершение.</b></H2><br>';
$failure=TRUE;
wp_die( $wpdb2->error );
}

$m_size_file=0;
$m_mtime_file=0;
$m_comment='';
/////проверка существования файлов выгрузки из 1С
////файл выгрузки сервисных книжек
$file_hist = ABSPATH.'/_1c_alfa_exchange/AA_hist.csv';
if (!file_exists($file_hist))
{
/////   echo '<H2><b>Файл обмена с сервисными книжками не существует.</b></H2><br>';
$m_comment='Файл обмена с сервисными книжками не существует';
$failure=TRUE;
}

/////инициируем таблицу лога
/////если не существует файла то возврат и ничего не делаем
if ($failure){
///включает защиту от SQL инъекций и данные можно передавать как есть, например: $_GET['foo']
/////   echo '<H2><b>Попытка вставить запись в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>$m_comment));
wp_die();
/////    echo '<H2><b>Возврат в начало.</b></H2><br>';
return $failure;
}
/////проверка лога загрузки, что бы не загружать тоже самое
$masiv_data_file=stat($file_hist);   ////передаем в массив свойство файла
$m_size_file=$masiv_data_file[7];    ////получаем размер файла
$m_mtime_file=$masiv_data_file[9];   ////получаем дату модификации файла
////создаем запрос на получение последней удачной загрузки
////выбираем по штампу времени создания (редактирования) файла загрузки AA_hist.csv, $m_mtime_file

/////   echo '<H2><b>Размер файла: '.$m_size_file.'</b></H2><br>';
/////   echo '<H2><b>Штамп времени файла: '.$m_mtime_file.'</b></H2><br>';
/////   echo '<H2><b>Формирование запроса на выборку из лога</b></H2><br>';
////препарируем запрос
$text_zaprosa=$wpdb2->prepare("SELECT * FROM `vin_logs` WHERE `last_mtime_upload` = %s", $m_mtime_file);
$results=$wpdb2->get_results($text_zaprosa);

if ($results)
{   foreach ( $results as $r)
{
////если штамп времени и размер файла совпадают, возврат
if (($r->last_mtime_upload==$m_mtime_file) && ($r->last_size_upload==$m_size_file))
{////echo '<H2><b>Возврат в начало, т.к. найдена запись в логе.</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>'Загрузка отменена, новых данных нет, т.к. найдена запись в логе.'));
wp_die();
return $failure;
}
}
}
////если данные новые, пишем в лог запись о начале загрузки
/////echo '<H2><b>Попытка вставить запись о начале загрузки в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>0, 'last_size_upload'=>$m_size_file, 'comment'=>'Начало загрузки'));

////очищаем таблицу
$clear_tbl_zap=$wpdb2->prepare("TRUNCATE TABLE %s", 'vin_history');
$clear_tbl_zap_repl=str_replace("'","`",$clear_tbl_zap);
$results=$wpdb2->query($clear_tbl_zap_repl);
/////   echo '<H2><b>Очистка таблицы сервисных книжек</b></H2><br>';
if (empty($results))
{
/////   echo '<H2><b>Ошибка очистки таблицы книжек, завершение.</b></H2><br>';
//// если очистка не удалась, возврат
$failure=TRUE;
wp_die();
return $failure;
}

////загружаем данные
$table='vin_history';         // Имя таблицы для импорта
//$file_hist Имя CSV файла, откуда берется информация     // (путь от корня web-сервера)
$delim=';';          // Разделитель полей в CSV файле
$enclosed='"';      // Кавычки для содержимого полей
$escaped='\

23 Comments

  1. palsergeich

    Есть один маленький и очень тонкий ньюанс:

    При использовании временного хранилища с адресом УИД формы, если надо передать больше чем 1 файл, точнее передать потом еще раз передать, то в дело вступает механизм кеширования и результат будет следующий (не знаю починили ли в последних релизах):

    Поместили Адрес = ПоместитьВоВременноеХранилище(Файл1,ЭтаФорма.УникальныйИдентификатор)

    Метод ПолучитьИзВременногоХранилища(Адрес)

    На сервере — Файл 1

    На клиенте — Файл 1

    следом Адрес = ПоместитьВоВременноеХранилище(Файл2,ЭтаФорма.УникальныйИдентификатор) Форма та же, без переоткрытия

    Метод ПолучитьИзВременногоХранилища(Адрес)

    На сервере Файл 2

    На клиенте Файл 1

    При этом где помещаем — клиент или сервер — не важно.

    Reply
  2. Alxby

    (1) На платформе 8.3.10.2699 провел несколько экспериментов:

    &НаКлиенте
    Процедура Команда1(Команда)
    //1 эксперимент
    Адрес = ПоместитьВоВременноеХранилище(«А», ЭтаФорма.УникальныйИдентификатор);
    Сообщить(ПолучитьИзВременногоХранилища(Адрес));
    ПоместитьВоВременноеХранилище(«Б», Адрес);
    Сообщить(ПолучитьИзВременногоХранилища(Адрес));
    
    //2 эксперимент
    Адрес1 = ПоместитьВоВременноеХранилище(«А», ЭтаФорма.УникальныйИдентификатор);
    Сообщить(ПолучитьИзВременногоХранилища(Адрес1));
    Адрес2 = ПоместитьВоВременноеХранилище(«Б», ЭтаФорма.УникальныйИдентификатор);
    Сообщить(ПолучитьИзВременногоХранилища(Адрес2));
    
    //3 эксперимент
    Адрес = ПоместитьВоВременноеХранилище(«А», ЭтаФорма.УникальныйИдентификатор);
    ПолучитьНаСервере(Адрес);
    ПоместитьВоВременноеХранилище(«Б», Адрес);
    ПолучитьНаСервере(Адрес);
    
    //4 эксперимент
    Адрес1 = ПоместитьВоВременноеХранилище(«А», ЭтаФорма.УникальныйИдентификатор);
    ПолучитьНаСервере(Адрес1);
    Адрес2 = ПоместитьВоВременноеХранилище(«Б», ЭтаФорма.УникальныйИдентификатор);
    ПолучитьНаСервере(Адрес2);
    
    КонецПроцедуры
    
    &НаСервереБезКонтекста
    Процедура ПолучитьНаСервере(Адрес)
    Сообщить(ПолучитьИзВременногоХранилища(Адрес));
    КонецПроцедуры
    

    Показать

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

    Reply
  3. palsergeich

    (2)

    УникальныйИдентификатор);

    ПолучитьНаСервере(Адрес1);

    Адрес2 = ПоместитьВоВременноеХранилище(«Б», ЭтаФорма.УникальныйИдентификатор);

    ПолучитьНаСервере(Адрес2);

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

    Но я уже привык не доверять этому механизму, ибо в свое время на этом очень сильно обжегся (изменение по одному адресу во временном хранилище)

    Reply
  4. LexSeIch

    Интересная и полезная статья, особенно для тех, кто под «нажимом непреодолимой силы» вынужден менять привычные платформы и конфигурации… При переписывании обработок на управляемые формы вопрос корректной работы с файлами обязательно возникает…

    Reply
  5. rusmil

    (3)

    изменение по одному адресу во временном хранилище

    и как выкрутились с адресом во временном хранилище?

    Reply
  6. logos

    Давайте я немного наброшу на вентилятор. А как будет работать Ваша схема с файлами больше 4 Гб?

    Reply
  7. Alxby

    (6)

    Я бы слегка перефразировал вопрос и разбил его на несколько:

    1) Как работают с большими файлами платформенные механизмы передачи файлов между клиентом и сервером? Какие есть ограничения для тонкого и веб-клиента? Есть ли зависимость от браузера?

    2) Как работают с большими объемами данных механизмы потоков?

    Впрочем, мне кажется, что ответы на эти вопросы имеют скорее теоретическое значение, так как если система спроектирована таким образом, что передача больших файлов указанными средствами платформы — штатная функция системы, то это недосмотр проектировщика или архитектора. Все же подобные задачи лучше решать другими средствами, к примеру — FTP. А для «защиты от дурака» достаточно встроить проверку размера файла перед передачей. Да, конечно же не забываем, что на клиенте для этого необходимо использовать асинхронный НачатьПолучениеРазмера

    Reply
  8. logos

    (7)

    1) Какая разница как они работают «под капотом»? Во временное хранилище нельзя положить больше 2#k8SjZc9Dxk32 байт. Просто нельзя.

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

    2) А вот механизм потоков работает просто отлично, он может и небольшими порциями работать через ПотокВПамяти.

    Reply
  9. Alxby

    (8)

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

    Reply
  10. androgin

    Я конечно не хочу казаться умником, но чем вас не устраивает метод НачатьПомещениеФайла / НачатьПомещениеФайлов ? У вас куча процедур расписана (на мой взгляд совершенно лишних)

    я выбираю файлы таким образом:

    &НаКлиенте
    Процедура ВыбратьФайл(Команда)
    
    Диалог = Новый ДиалогВыбораФайла(РежимДиалогавыборафайла.Открытие);
    Диалог.Заголовок = «Выберите файл…»;
    Диалог.Фильтр = «Текстовый документ MS Office (*.doc;*.docx)|*.doc;*.docx»;
    
    Оповещение = Новый ОписаниеОповещения(«ЗакончитьПомещениеФайла», ЭтотОбъект);
    НачатьПомещениеФайлов(Оповещение, , Диалог, Истина, ЭтаФорма.УникальныйИдентификатор);
    
    КонецПроцедуры
    
    &НаКлиенте
    Процедура ЗакончитьПомещениеФайла(ПомещенныеФайлы, ДопПараметры) Экспорт
    
    Если ПомещенныеФайлы = Неопределено Тогда
    Возврат;
    КонецЕсли;
    
    Хранение = ПомещенныеФайлы[0].Хранение;
    
    КонецПроцедуры

    Показать

    Reply
  11. Alxby

    (10) Основное отличие моего варианта от Вашего — возможность обработать выбранный пользователем файл до передачи на сервер или получения с сервера. Например: сообщить пользователю «Вы не можете загрузить на сервер файл ‘Игра престолов (все сезоны).mkv'» или «Вы не можете перезаписать файл ‘устав проекта.docx’, выберите другой». Кроме того, в Вашем варианте все заканчивается после получения адреса загруженного файла, в моем — дополнительно описана процедура обработки содержимого с помощью механизма потоков, поэтому и методов в примере больше. Но конечно же, Ваш вариант проще, и может быть вполне применим в большинстве случаев.

    Reply
  12. androgin

    (11) при превышении объема файла над размером памяти — система сама выдаст ошибку. Вам останется лишь вывести ее в удобочитаемом виде.

    Reply
  13. androgin

    (11) еще можно создать диалог — выбрать и инициализировать файл и получить его размер. Если размер устраивает — передать Диалог дальше в метод получения файла.

    Reply
  14. Alxby

    (12)


    12. Виктор Назаров (androgin) 12.04.18 13:48

    (11) при превышении объема файла над размером памяти — система сама выдаст ошибку. Вам останется лишь вывести ее в удобочитаемом виде

    А если нам надо ограничить размер файла своим значением, например 10Мб, исходя из своих целей? А если надо наложить условие: можно загружать все файлы, кроме определенных типов? В этих случаях как раз и надо проверить, что выбрал пользователь. Причем сделать это лучше ДО остальных действий по формированию и передаче файла.

    Reply
  15. Alxby

    (13) Именно такая схема, с отдельным диалогом, и приведена в статье. Я не стал вставлять дополнительные проверки, чтобы не загромождать пример. И, раз уж мы получили имя файла, его и надо передавать в метод передачи файла, а не Диалог.

    Reply
  16. androgin

    (15) ну вам виднее. Но я бы так не делал)

    Reply
  17. androgin

    (15) вы в своем коде все равно тащите все на сервер — в чем смысл?

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

    Использование диалога позволяет применять фильтр!

    НЕ?

    Reply
  18. Alxby

    (17)

    Мне кажется в нашем споре каждый имеет в виду что-то свое. Я описываю решение следующей задачи: 1) дать возможность пользователю выбрать файл, 2) проверить, что он выбрал (проверки могут быть самые разные: размер, расширение, каталог), 3) и только если файл удовлетворяет заданным критериям, отправить его на сервер для дальнейшей обработки. В моем примере выбор файла — асинхронный вызов диалога, по его завершению мы получим имя файла, который после всех проверок будем использовать при передачи на сервер. Зачем при передаче на сервер еще раз указывать диалог? Чтобы пользователь два раза выбирал файл? Да, конечно же, можно использовать интерактивный режим вызова метода НачатьПомещениеФайлов без предварительного вызова диалога. Но при этом Вам просто негде будет вставить свои проверки. Единственное что Вы можете — это указать маску допустимых имен файла. Но и в этом случае Вы не сможете задать условие вида: ‘выбрать все файлы, кроме *.exe, *.vbs’. Что касается задачи получения файла с сервера, там ситуация немного иная. Если, как в моем примере, файл не хранится в базе, а формируется каким либо образом — это формирование может занять какое-то время. Тогда, с точки зрения юзабилити, лучше дать возможность выбрать файл для записи, удостовериться что он его выбрал, и выбрал то, что надо, а только потом заниматься длительным формированием файла. При использовании Вашего подхода (т.е. использования диалога выбора при вызове метода передачи) мы сначала затратим время на формирование и сохранение файла во временном хранилище, а только потом будем спрашивать у пользователя куда он хочет его сохранить, и не передумал ли он, «потому что забыл дома флешку».

    Reply
  19. androgin

    (18) так вы сами же описываете то, что я и говорю: выбор файла и проверку его ( ваш второй пункт очень странный — расширение и каталог в диалоге выбирается! а размер можно получить после выбора файла или настроить вид диалога — показать там размер).

    Это топтание на месте.

    Мне вообще непонятно: зачем использовать сервер, если все на клиенте делается: и выбор файла и получение его размера. Только для того, чтобы поиграться с потоком?)))

    Можно использовать и не_интерактивный метод выбора (признак такой имеется в методе) и это никак не мешает вставить свои проверки. Фильтр «выбрать все, кроме ххх, ххх» — ну это ж дурость! Создайте необходимые фильтры заранее и пусть пользователь сам выбирает, что ему нужно.

    Когда вы тащите хранилище на сервер — вы уже получаете файл!

    Вам вообще знакомы клиент-серверные работы с файлами?

    Ваш метод, мало того, что получает файл и помещает в хранилище, так вы потом его еще на сервер тащите и там получаете из хранилища!!

    Что за абсурд?

    Все ваши манипуляции вполне себе на клиенте работают!

    Reply
  20. Alxby

    (19)

    Несмотря на вашу горячность, я надеюсь, что Вы не троллите, а просто невнимательно читаете мои сообщения. Хорошо, давайте возьмем Ваш пример из (10). Добавьте в него возможность загружать любые файлы, кроме потенциально опасных, например *.exe. Нет, это не дурость, подобный функционал есть в системах, построенных на базе БСП, там есть что-то вроде «Запретить загрузку файлов с расширениями». В случае попытки выбрать такой файл добавьте сообщение об этом пользователю. Если у Вас найдется способ сделать это с помощью интерактивного режима метода НачатьПомещениеФайлов без дополнительного вызова диалога, поделитесь. Далее: в моем примере при выборе файла не используется сервер, и конечно же для получения размера файла сервер тоже не нужен. Безусловно, есть задачи, которые можно решить только на клиенте. Но я специально выбрал такую задачу, для которой нужен сервер. Я это подчеркнул в начале статьи. Вы и сами сможете привести примеры таких задач. Вы же согласитесь, что с объектами базы данных можно работать только на сервере? Вот и в моем примере используется запись и чтение объектов ИБ. Может Вы имеете в виду иное — хранение файла целиком в базе данных, в каком-либо реквизите? Это немного другая задача.

    Reply
  21. androgin

    (20) «кроме потенциально опасных» — вы вообще читали, что я писал?

    вы программист — вам легко это проверить после выбора! Даже если вы не укажете опасное расширение — вы его можете проверить ПОСЛЕ выбора и вывести пользователю сообщение, что «ай-я-яй, такие файлы нельзя выбирать!» и удалить их из выбора. Сложно? НЕТ!

    Далее: НачатьПомещениеФайлов с диалогом как раз и контролирует расширения, о чем я вам выше и писал!

    «специально выбрал такую задачу, для которой нужен сервер» — именно это и является узким местом! вы тащите все на сервер! а это и есть «очень плохо».

    Я же настаиваю, что все операции с проверками делать нужно на клиенте!

    Reply
  22. Alxby

    (22)

    Вы все-таки невнимательно прочитали статью и мои комментарии. Поясню еще раз: на первом шаге мы вызываем диалог выбора файла. При вызове можно указать (как Вы верно говорите) свойства диалога: фильтр, начальный каталог, etc. Результат работы диалога обрабатывается в ОповещениеПослеВыбораФайлаДляЧтения . Здесь мы можем выполнить любые проверки — проверить расширение файла, путь к файлу и так далее. Я не стал загромождать пример, поэтому проверяю только то, что пользователь не отказался от выбора. Если вы посмотрите внимательно, то увидите директиву &НаКлиенте. Это означает, что эти операции будут выполнены на клиенте. С чего Вы взяли, что я делаю это на сервере? Если посмотрите внимательно, то увидите, что у меня ни в одной серверной процедуре нет проверок.

    Вы разобрались что происходит в моих серверных методах? А там я делаю то, что можно сделать только на сервере: работаю с данными информационной базы. Для решения поставленной в начале статьи задачи, мне нужно содержимое файла. Вполне логично, что я его получаю из хранилища. Что значат Ваши слова:

    «Ваш метод, мало того, что получает файл и помещает в хранилище, так вы потом его еще на сервер тащите и там получаете из хранилища!!

    Что за абсурд?

    Все ваши манипуляции вполне себе на клиенте работают! «? Как можно после помещения файла во временное хранилище, отдельно его «тащить» на сервер? Какая строчка моего кода заставила Вас так подумать? Как «манипуляции» по созданию объекта и записи его в ИБ «вполне себе на клиенте работают»? Или Вы считаете что таких задач не бывает? Что любые действия с содержимым файлов можно делать на клиенте, потому что на сервере это делать «есть «очень плохо»?

    Давайте, чтобы не быть голословным, напишите свой вариант решения рассмотренной в статье задачи и покажите в чем он быстрее / лучше / пригодней для дальнейшей адаптации. Потому что иначе от меня ускользает смысл того, что Вы хотите доказать.

    Reply
  23. Xershi

    Уже не нужно использовать «СериализаторXDTO»

    Наконец написал свою публикацию с блекджеком и ш…… Работа с файлами (обычная и управляемая форма) все структурировано и используется метод «ЧтениеДанных»!

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *