Файлы как часть разработки. Способ организации работы.

23 0

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

  1. В большинстве случаев файлы в разработке на Directum представляют собой веб-контролы или их части.
  2. Логотип компании, который вставляется во все генерируемые документы.
  3. Шаблоны писем, документов.
  4. Скрипты vbs, xsl-преобразования и другие служебные файлы.

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

В этой статье я покажу как может выглядеть решение, реализующее:

  1. Кэширование файлов на клиенте.
  2. Удобную отладку/редактирование, в том числе содержимого архивов.
  3. Удобный перенос между системами.
  4. Версионность.

В решении будут использоваться функции из библиотеки UDL Александра Тишина.

Реализация

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

  1. Есть версионность.
  2. Есть готовый простейший импорт/экспорт. У стандартного механизма экспорта-импорта записей справочников есть ограничения, поэтому в любом случае придется реализовывать экспорт-импорт самому.
  3. Есть предпросмотр, в том числе содержимого архивов.
  4. Обложки уже работают через документы.

Итак, делаем свою ТКД и выгрузку кэша в файловую систему.

Файлы будут кэшироваться в  %temp%\<Код системы>\cache\. Такой путь выбран чтобы файлы кэша были дочерними для html-файлов, выгружаемых в папку %temp%.

Листинг функции UDL_GetCacheFolderPath([RelativeToTemp: Логический = false])

Result = IfThen(RelativeToTemp; ''; GetTempFolder()) & Application.Connection.SystemInfo.Code & "\Cache\"

Карточка документа

Путь в папке кэша – относительный путь файла в папке, которую возвращает функция UDL_GetCacheFolderPath(). Если не указано расширение файла, то оно берется из последней действующей версии документа. Для zip-архивов имя файла используется как имя папки, куда будет распаковываться содержимое архива. Путь может содержать подпапки, например, “js/lib/jquery”. Этот реквизит используется как уникальный идентификатор файла при переносе документов между системами.

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

Листинг события ТКД «Запись. Сохранение после»

  SetConstant('LastCacheFilesModifiedDate'; FormatDate('yymd h:n:s'; Object.ISBEDocModifyDate))

Функция по проверке актуальности кэша и выгрузке кэша на диск UDL_UpdateCache()

Функцию UDL_UpdateCache() можно выполнять не при каждом показе карточки, а только в событии «Набор данных. Открытие».

Не нужно никому выдавать права на документы.

Проверка актуальности кэша – всего 1 чтение из файла.

Содержимое zip-архивов распаковывается в каталог.

Выгрузка кэша работает быстро – всего один SQL-запрос и запись изменившихся файлов на диск.

Листинг функции UDL_UpdateCache([Force: Логический = false];  [DocumentID: Целое число = ''])

  ActualDate = GetAssignedConst("LastCacheFilesModifiedDate")
  CacheFolderPath = UDL_GetCacheFolderPath()
  CacheDateFilePath = CacheFolderPath & "cache.dat"
  CacheDate = ''
  
  if not DirectoryExists(CacheFolderPath)
    ForceDirectories(CacheFolderPath)
  endif
  
  NeedUpdate = true
  if FileExists(CacheDateFilePath) and not Force
    CacheDate = ReadFile(CacheDateFilePath)
    if ActualDate == CacheDate
      NeedUpdate = false
    endif
  endif

  if NeedUpdate    
    Query = CreateQuery()
    Query.CommandText = "
    select
      lastActiveVersion.Body as Body
      , (select AppExt from MBAnalit where Analit = lastActiveVersion.Editor) as Extension
      , rtrim(ltrim(SBEDoc.Note)) as Path
    from
      SBEDoc
      cross apply ( select top 1
                      SBEDocVer.VersionData
                      , SBEDocVer.Editor 
                    from 
                      SBEDocVer 
                    where 
                      SBEDocVer.EDocID = SBEDoc.XRecID 
                      and SBEDocVer.LifeStage = 'Д'
                    order by
                      SBEDocVer.Number desc) as lastActiveVersion(Body, Editor)
    where
      SBEDoc.TypeID = (select TypeID from MBEDocType where kod like 'UDL_CachedFiles')
      " & IfThen(Assigned(CacheDate) and not Force; " and SBEDoc.ModifyDate > '" & CacheDate & "'"; '') 
       & IfThen(Assigned(DocumentID); " and SBEDoc.XRecID = " & DocumentID; '')
    Query.Open
    foreach File in Query
      CachedFilePath = CacheFolderPath & File.Fields("Path").AsString
      if not Assigned(ExtractFileDriveDirNameExt(CachedFilePath; 'E'))
        CachedFilePath = CachedFilePath & "." & File.Fields("Extension").AsString
      endif
      DeleteFile(CachedFilePath) 
      File.Fields("Body").SaveToFile(CachedFilePath)
      if File.Fields("Extension").AsString == 'ZIP'
        TargetFolder = ExtractFileDriveDirNameExt(CachedFilePath; 'D:\P\N\')
        DeleteFile(TargetFolder & "*.*")
        UDL_UNZIPArchive(CachedFilePath; TargetFolder)
        DeleteFile(CachedFilePath)
      endif
    endforeach
    WriteFile(CacheDateFilePath; ; ActualDate) 
  endif
  Result = CacheFolderPath  

Действие ТКД «Редактировать файл в кэше»

Открывает файл из папки кэша. Если файл - zip-архив, то открывает каталог с распакованным содержимым в проводнике. Результат редактирования можно сразу наблюдать в веб-контроле, обновив страницу(F5).

Листинг действия

  UDL_UpdateCache(; Object.SYSREQ_ID)
  
  FilePath = UDL_GetCacheFolderPath() & Object.SYSREQ_EDOC_NOTE
  if not Assigned(ExtractFileDriveDirNameExt(FilePath; 'E'))
    FilePath = FilePath & "." & Object.Info.Editor.Extension
  endif
  Ext = ExtractFileDriveDirNameExt(FilePath; 'E')
  if Ext == 'zip'
    Folder = ExtractFileDriveDirNameExt(FilePath; 'D:\P\N\')
    ExecuteProcess('explorer "' & Folder & '"')
  else
    OpenFile(FilePath)
  endif

Действие ТКД «Импортировать файл из кэша»

Берет из кэша файл и импортирует в документ. Для zip-архива каталог архива запаковывается.

Листинг действия

  FilePath = UDL_GetCacheFolderPath() & Object.SYSREQ_EDOC_NOTE
  if not Assigned(ExtractFileDriveDirNameExt(FilePath; 'E'))
    FilePath = FilePath & "." & Object.Info.Editor.Extension
  endif
  Ext = ExtractFileDriveDirNameExt(FilePath; 'E')
  if Ext == 'zip'
    Folder = ExtractFileDriveDirNameExt(FilePath; 'D:\P\N\')
    ZipContent = CreateStringList()
    
    FolderContent = CreateStringList()
    FolderContent.Delimiter = ";"
    FolderContent.DelimitedText = FindFile(Folder & "*") & ';' & FindFile(Folder & "*"; 'D')
    foreach Item in FolderContent
      if Assigned(Item) and Item <<>> '.' and Item <<>> '..' 
        ZipContent.Add(Folder & Item)
      endif
    endforeach
     
    UDL_ZIPArchive(ZipContent; FilePath)
  endif
  ImportDialog = SystemDialogs.GetImportEDocumentVersionFromFileDialog(Object; -1; FilePath; Object.Info.Editor.Code; false; false)
  ImportDialog.Show(false)
  
  if Ext == 'zip'
    Sleep(1)
    DeleteFile(FilePath)
  endif                                                                                       

Действие ТКД «Обновить кэш»

Принудительно обновляет кэш текущего документа. Отменяет все изменения текущего документа, произведенные в кэше.

Листинг действия

UDL_UpdateCache(true; Object.SYSREQ_ID)

Экспорт и Импорт документов

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

Листинг сценария UDL_ExportCacheFiles ("Экспорт кэшируемых файлов")

  Search = Searches.CreateNew(ckEDocument)
  Search.SearchCriteria.Add(SYSREQ_EDOC_TYPE_PSEUDO_REQUISITE_CODE).Add("UDL_CachedFiles")
  Search.Show(ssmMultiSelect; false)
  SelectedContents = Search.SelectedContents
  
  TargetFolder = IncludeTrailingPathDelimiter(SelectFolderDialog())
  PackageName = "CachedFiles" & FormatDate(' yymdhns'; Now())
  ZIPFolder = TargetFolder & PackageName & "\"
  ForceDirectories(ZIPFolder)
  ZIPContents = CreateStringList()
  
  try
    PackageInfoXML = CreateObject("MSXML.DOMDocument")
    RootElement = PackageInfoXML.CreateElement("CachedFiles")
    PackageInfoXML.AppendChild(RootElement)
    foreach DocInfo in SelectedContents
      Doc = DocInfo.Document
      LastActiveVersion = UDL_GetLastActiveVersion(Doc)
      FileName = ClearFileName(Doc.SYSREQ_EDOC_NAME & FormatDate(' yymdhns'; Doc.ISBEDocModifyDate); 'Ж') & '.' & LastActiveVersion.Editor.Extension
      FullFilePath = ZIPFolder & FileName
      ZIPContents.Add(FullFilePath)
      LastActiveVersion.Export(FullFilePath)
      
      FileElement = PackageInfoXML.CreateElement("File")
      Attribute = PackageInfoXML.createAttribute("Name")
      Attribute.Value = Doc.Requisites(SYSREQ_EDOC_NAME).AsString
      FileElement.setAttributeNode(Attribute)
      Attribute = PackageInfoXML.createAttribute("CachePath")
      Attribute.Value = Doc.Requisites(SYSREQ_EDOC_NOTE).AsString
      FileElement.setAttributeNode(Attribute)
      Attribute = PackageInfoXML.createAttribute("DocumentNote")
      Attribute.Value = Doc.Requisites("LongString").AsString
      FileElement.setAttributeNode(Attribute)
      Attribute = PackageInfoXML.createAttribute("VersionNote")
      Attribute.Value = LastActiveVersion.Note
      FileElement.setAttributeNode(Attribute)
      FileElement.text = FileName
      RootElement.AppendChild(FileElement)
      
      LastActiveVersion = nil
      Doc = nil
    endforeach
    PackageInfoFilePath = ZIPFolder & "PackageInfo.xml"
    PackageInfoXML.Save(PackageInfoFilePath)
    ZIPContents.Add(PackageInfoFilePath)
    
    UDL_ZIPArchive(ZIPContents; TargetFolder & PackageName & '.zip')
  finally
    DeleteFile(ZIPFolder)
  endfinally
  
  ExecuteProcess('explorer "' & TargetFolder & '"')

Листинг сценария UDL_ImportCacheFiles ("Импорт кэшируемых файлов")

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

  KindCode = SQL("
  select top 1
    MBAnalitSpr.Kod 
  from
    MBAnalitSpr
    join MBAnValR2 on MBAnalitSpr.Analit = MBAnValR2.Analit
  where 
    MBAnValR2.TipElDokT2 = (select Analit from MBAnalit where Prim like 'UDL_CachedFiles' and Vid = " & References.ТЭД.ID & ")
    and MBAnalitSpr.Vid = " & References.ВЭД.ID)
  if not Assigned(KindCode)
    Raise(CreateException(''; 'Нет доступных видов документов для типа карточки "UDL_CachedFiles"'; ecWarning))
  endif
  
  OpenDialog = CreateOpenDialog()
  OpenDialog.Filter = 'Cache files package (*.zip)|*.zip'
  OpenDialog.FilterIndex = 1
  OpenDialog.MultiSelect = false
  if OpenDialog.Execute
    PackageFilePath = OpenDialog.Result

    ZIPFolder = GetTempFolder() & ExtractFileDriveDirNameExt(PackageFilePath; 'N') & "\"
    DeleteFile(ZIPFolder)
    UDL_UNZIPArchive(PackageFilePath; ZIPFolder)
    
    try
      PackageInfoXML = CreateObject("MSXML.DOMDocument")
      PackageInfoXML.load(ZIPFolder & "PackageInfo.xml")
      Root = PackageInfoXML.documentElement
      Nodes = Root.childNodes
      Index = 0
      while Index < Nodes.length
        Node = Nodes.item(Index)
        FilePath = ZIPFolder & Node.text
        DocName = Node.getAttribute('Name')
        CachePath = Node.getAttribute('CachePath')
        DocumentNote = Node.getAttribute('DocumentNote')
        VersionNote = Node.getAttribute('VersionNote')
        
        DocumentID = SQL("
        select
          XRecID
        from
          SBEDoc
        where
          TypeID = (select TypeID from MBEDocType where kod like 'UDL_CachedFiles')
          and ltrim(rtrim(Note)) like '" & Trim(CachePath) & "'")
        
        if Assigned(DocumentID)
          Doc = EDocuments.GetObjectByID(DocumentID)
          Doc.ImportFromFile(-1; VersionNote; FilePath)
        else
          EditorCode = UDL_GetEditorCodeByFilePath(FilePath)
          Doc = EDocuments.CreateNewFromFile('UDL_CachedFiles'; KindCode; EditorCode; FilePath)
        endif
        Doc.Requisites(SYSREQ_EDOC_NAME).Value = DocName
        Doc.Requisites(SYSREQ_EDOC_NOTE).Value = CachePath
        Doc.Requisites("LongString").Value = DocumentNote
        Doc.Save
        Doc = nil
        
        Index = Index + 1
      endwhile
      ShowMessage("Успешно импортировано " & Index & " файлов")
    finally
      DeleteFile(ZIPFolder)
    endfinally
  endif

Пример использования

В качестве примера возьмем отображение на форме карточки справочника веб-контрола с календарем. Весь веб-контрол заархивировали, структура файлов как у обложки. Архив занесен в систему с ТКД "Кэшируемые файлы", значение реквизита документа Путь в папке кэша = calendar

Листинг события "Форма-карточка. Показ"

UpdateCache()

CalendarControl = Object.Form.Controls.FindControl("WebCalendar")
CalendarControl.Navigate(GetCacheFolderPath() & "calendar\index.html") 

Если содержимое веб-контрола динамическое его формирование занимает приличное время, то советую не формировать HTML-файл в коде ISBL и выгружать его на диск для загрузки в веб-контроле, а из самой страницы в коде JS вызывать сценарий Directum, который вернет набор данных для отображения. Такой подход позволяет открывать карточку вообще без задержек, так как JS выполняется в отдельном потоке, параллельно с открытием карточки.

 

Пакет разработки (лицензия MIT): CacheFile.zip

Пакет собран на версии 5.2.2, но решение должно работать на всех версиях системы.

Пока комментариев нет.

Авторизуйтесь, чтобы написать комментарий