Статьи

Распаковка 7-Zip файлов с помощью Groovy и 7-Zip-JBinding

В этом посте демонстрируется скрипт Groovy для распаковки файлов в формате архива 7-Zip . Две основные цели этого поста – продемонстрировать распаковку 7-Zip- файлов с помощью Groovy и удобного 7-Zip-JBinding, а также выявить и продемонстрировать некоторые ключевые характеристики Groovy как языка сценариев.

Страница 7-Zip описывает 7-Zip как «архиватор файлов с высокой степенью сжатия». Далее на странице добавлено: «7-Zip – это программное обеспечение с открытым исходным кодом. Большая часть исходного кода находится под лицензией GNU LGPL ». Более подробная информация о лицензии доступна на сайте вместе с информацией о формате 7z («LZMA по умолчанию и общий метод сжатия формата 7z»).

Страница 7-Zip описывает ее как «оболочку Java для библиотеки 7-Zip C ++», которая «позволяет извлекать многие форматы архивов, используя очень быструю нативную библиотеку напрямую из Java через JNI». Формат 7z основан на «сжатии LZMA и LZMA2 ». Хотя доступен LZMA SDK , проще использовать проект 7-Zip-JBinding с открытым исходным кодом ( SourceForge ) при манипулировании файлами 7-Zip с помощью Java.

Хороший пример использования Java с 7-Zip-JBinding для распаковки файлов 7z доступен в потоке StackOverflow. Распаковывает файлы с расширением .7z в java . Ответ Dark Knight показывает, как использовать Java с 7-Zip-JBinding для распаковки файла 7z. В этом посте я адаптирую Java-код Dark Knight в Groovy-скрипт.

Чтобы продемонстрировать адаптированный код Groovy для распаковки файлов 7z, сначала мне нужен файл 7z, из которого я могу извлечь содержимое. В следующей серии снимков экрана я показываю, как на моем ноутбуке установлена Windows 7-Zip, чтобы сжимать шесть PDF-файлов, доступных на странице загрузок Guava, в один файл 7z, который называется Guava.7z .

Шесть файлов гуавы в папке

guavaDownloadedPdfsToBeCompressedWith7Zip

Выбранное содержимое и контекстное меню для сжатия в формат 7z

savingSelectedFilesTo7zCompressedFile

Сжатый архивный файл Guava.7z создан

guava7zCompressedFileCreated

Имея файл 7z, я теперь Guava.7z к адаптированному скрипту Groovy, который будет извлекать содержимое этого файла Guava.7z . Как упоминалось ранее, этот скрипт Groovy представляет собой адаптацию Java-кода, предоставленного Dark Knight в потоке StackOverflow .

unzip7z.groovy

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//
// This Groovy script is adapted from Java code provided at
 
import static java.lang.System.err as error
 
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
import java.util.Arrays
 
import net.sf.sevenzipjbinding.ExtractOperationResult
import net.sf.sevenzipjbinding.ISequentialOutStream
import net.sf.sevenzipjbinding.ISevenZipInArchive
import net.sf.sevenzipjbinding.SevenZip
import net.sf.sevenzipjbinding.SevenZipException
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream
import net.sf.sevenzipjbinding.simple.ISimpleInArchive
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem
 
if (args.length < 1)
{
   println "USAGE:  unzip7z.groovy <fileToUnzip>.7z\n"
   System.exit(-1)
}
 
def fileToUnzip = args[0]
 
try
{
   RandomAccessFile randomAccessFile = new RandomAccessFile(fileToUnzip, "r")
   ISevenZipInArchive inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile))
 
   ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface()
 
   println "${'Hash'.center(10)}|${'Size'.center(12)}|${'Filename'.center(10)}"
   println "${'-'.multiply(10)}+${'-'.multiply(12)}+${'-'.multiply(10)}"
 
   simpleInArchive.getArchiveItems().each
   { item ->
      final int[] hash = new int[1]
      if (!item.isFolder())
      {
         final long[] sizeArray = new long[1]
         ExtractOperationResult result = item.extractSlow(
            new ISequentialOutStream()
            {
               public int write(byte[] data) throws SevenZipException
               {
                  //Write to file
                  try
                  {
                     File file = new File(item.getPath())
                     file.getParentFile()?.mkdirs()
                     FileOutputStream fos = new FileOutputStream(file)
                     fos.write(data)
                     fos.close()
                  }
                  catch (Exception e)
                  {
                     printExceptionStackTrace("Unable to write file", e)
                  }
 
                  hash[0] ^= Arrays.hashCode(data) // Consume data
                  sizeArray[0] += data.length
                  return data.length // Return amount of consumed data
               }
            })
         if (result == ExtractOperationResult.OK)
         {
            println(String.format("%9X | %10s | %s",
               hash[0], sizeArray[0], item.getPath()))
         }
         else
         {
            error.println("Error extracting item: " + result)
         }
      }
   }
}
catch (Exception e)
{
   printExceptionStackTrace("Error occurs", e)
   System.exit(1)
}
finally
{
   if (inArchive != null)
   {
      try
      {
         inArchive.close()
      }
      catch (SevenZipException e)
      {
         printExceptionStackTrace("Error closing archive", e)
      }
   }
   if (randomAccessFile != null)
   {
      try
      {
         randomAccessFile.close()
      }
      catch (IOException e)
      {
         printExceptionStackTrace("Error closing file", e)
      }
   }
}
 
/**
 * Prints the stack trace of the provided exception to standard error without
 * Groovy meta data trace elements.
 *
 * @param contextMessage String message to precede stack trace and provide context.
 * @param exceptionToBePrinted Exception whose Groovy-less stack trace should
 *    be printed to standard error.
 * @return Exception derived from the provided Exception but without Groovy
 *    meta data calls.
 */
def Exception printExceptionStackTrace(
   final String contextMessage, final Exception exceptionToBePrinted)
{
   error.print "${contextMessage}: ${org.codehaus.groovy.runtime.StackTraceUtils.sanitize(exceptionToBePrinted).printStackTrace()}"
}

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

Одна вещь, которую я изменил, заключалась в том, чтобы весь вывод, связанный с ошибкой, выводился на стандартную ошибку, а не на стандартный вывод. Это потребовало нескольких изменений. Во-первых, я использовал возможность Groovy переименовывать что-то статически импортированное (см. Мой связанный пост Groovier Static Imports ), чтобы ссылаться на «java.lang.System.err» как «error», чтобы я мог просто использовать «error» как дескриптор в сценарий, вместо того, чтобы использовать «System.err» для доступа к стандартной ошибке для вывода.

Поскольку Throwable.printStackTrace () уже пишет в стандартную ошибку, а не в стандартный вывод, я просто использовал ее напрямую. Однако я поместил вызовы к нему в новом методе, который сначала запустит StackTraceUtils.sanitize (Throwable), чтобы удалить специфичные для Groovy вызовы, связанные с динамическими возможностями Groovy во время выполнения, из трассировки стека.

Были некоторые другие незначительные изменения в скрипте, как часть создания Groovier. Я использовал итерацию Groovy для элементов в файле архива, а не for цикла Java for , удалил точки с запятой в концах операторов, использовал расширение String GDK Groovy для более контролируемых отчетов о выходе [для автоматического центрирования заголовков и умножения заданного символа на соответствующий символ количество раз, когда он должен существовать], и воспользовался неявным включением в Groovy аргументов, чтобы добавить проверку, чтобы гарантировать, что файл для извлечения был предоставлен сценарию.

Когда файл будет извлечен на месте, а скрипт Groovy готов к извлечению, пришло время извлечь содержимое файла Guava.7z который я продемонстрировал ранее в этом посте. Следующая команда запустит сценарий и поместит соответствующие файлы JAR 7-Zip-JBinding в путь к классам.

1
groovy -classpath "C:/sevenzipjbinding/lib/sevenzipjbinding.jar;C:/sevenzipjbinding/lib/sevenzipjbinding-Windows-x86.jar" unzip7z.groovy C:\Users\Dustin\Downloads\Guava\Guava.7z

Перед тем, как показать результаты выполнения вышеуказанного сценария для указанного файла Guava.7z, важно отметить сообщение об ошибке, которое произойдет, если 7-Zip-JBinding JAR для конкретной операционной системы (sevenzipjbinding-Windows-x86.jar в моем чехол для ноутбука) не включен в classpath скрипта.

sevenzipjbindingRequriesNativeJarErrorMsg

Как показывает последний снимок экрана, игнорирование включения собственного JAR-файла в путь к классу приводит к сообщению об ошибке: «Произошла ошибка: java.lang.RuntimeException: SevenZipJBinding не может быть инициализирован автоматически с помощью инициализации из зависимого от платформы JAR и временного каталога по умолчанию. , Пожалуйста, убедитесь, что правильно ‘sevenzipjbinding-

.jar ‘файл находится в пути к классу или рассмотрите возможность инициализации SevenZipJBinding вручную, используя один из предложенных методов инициализации:’ net.sf.sevenzipjbinding.SevenZip.init * () ‘”

Хотя я просто добавил C:/sevenzipjbinding/lib/sevenzipjbinding-Windows-x86.jar в classpath моего сценария, чтобы он работал на этом ноутбуке, более надежный сценарий может обнаружить операционную систему и применить соответствующий JAR-файл к classpath для этой операционной системы. система. Страница загрузки 7-Zip-JBinding содержит несколько загрузок для конкретной платформы (включая JAR-файлы для конкретной платформы), таких как sevenzipjbinding-4.65-1.06-rc-extr-only-Windows- amd64 .zip , sevenzipjbinding-4.65-1.06-rc-extr-only- Mac-x86_64 .zip , sevenzipjbinding-4.65-1.06-rc-extr-only- Mac-i386 .zip и sevenzipjbinding-4.65-1.06-rc-extr-only- Linux-i386 .zip .

Как только собственный JAR-файл 7-Zip-JBinding включен в classpath вместе с основным JAR-файлом sevenzipjbinding.jar, сценарий прекрасно работает, как показано на следующем снимке экрана.

outputRunningScriptToExtractGuavaPDFs

Сценарий извлекает содержимое файла 7z в тот же рабочий каталог, что и сценарий Groovy. Еще одним усовершенствованием было бы изменение сценария, чтобы он принимал каталог, в который записывать извлеченные файлы, или мог бы вместо этого записать их в тот же каталог, что и файл архива 7z по умолчанию. Использование встроенной в Groovy поддержки CLIBuilder также может улучшить сценарий.

Groovy – мой предпочтительный язык при написании сценариев, использующих JVM и / или библиотеки и инфраструктуры Java. Написание сценария, который является предметом этого поста, было еще одним напоминанием об этом.