All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fm.common.SevenZipUtils.scala Maven / Gradle / Ivy

/*
 * Copyright 2019 Frugal Mechanic (http://frugalmechanic.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fm.common

import java.io.{File, FileFilter, FileNotFoundException, IOException, InputStream}
import org.apache.commons.compress.archivers.sevenz.{SevenZArchiveEntry, SevenZFile}

object SevenZipUtils {
  /**
   * Open a 7-Zip File with support for multi-part archives (with names like file.7z.001)
   */
  def open(file: File): SevenZFile = {
    if (file.extension.exists{ _.isInt }) {
      // Check for multi-part - stripping off the numeric extension for the passed in file
      openMultiPart(findMultiPartFiles(file.getParentFile, file.nameWithoutExtension))
    } else if (file.isFile) {
      // Try opening normally
      new SevenZFile(file)
    } else {
      // Check for multi-part
      openMultiPart(findMultiPartFiles(file.getParentFile, file.getName))
    }
  }

  def openMultiPart(files: Seq[File]): SevenZFile = {
    new SevenZFile(MultiReadOnlySeekableByteChannel.forFiles(files))
  }

  private def findMultiPartFiles(dir: File, baseName: String): Seq[File] = {
    require(dir.isDirectory, s"Expected a directory: $dir")

    dir.listFiles(new FileFilter {
      override def accept(file: File): Boolean = file.isFile && file.getName.startsWith(baseName) && file.extension.exists{ _.isInt }
    }).toIndexedSeq.sortBy{ _.extension.map{ _.toInt }.get }
  }

  def inputStreamResourceForEntry(seven7File: File, entryName: String): InputStreamResource = {
    val resource: MultiUseResource[InputStream] = MultiUseResource{ inputStreamForEntry(seven7File, entryName) }
    InputStreamResource(resource = resource, fileName = entryName)
  }

  def getInputStreamResourceForEntry(seven7File: File, entryName: String): Option[InputStreamResource] = {
    if (!entryExists(seven7File, entryName)) return None

    val resource: MultiUseResource[InputStream] = MultiUseResource{ inputStreamForEntry(seven7File, entryName) }
    Some(InputStreamResource(resource = resource, fileName = entryName))
  }

  /**
   * Return an InputStream given an 7Zip file and entry name within the file to read.
   *
   * The caller is responsible for closing the returned InputStream
   */
  def inputStreamForEntry(seven7File: File, entryName: String): InputStream = {
    getInputStreamForEntry(seven7File, entryName).getOrElse{ throw new FileNotFoundException(s"Missing $seven7File or $entryName within $seven7File") }
  }

  /**
   * Return an optional InputStream given an 7Zip file and entry name within the file to read.
   *
   * The caller is responsible for closing the returned InputStream
   */
  def getInputStreamForEntry(seven7File: File, entryName: String): Option[InputStream] = {
    val file: SevenZFile = try {
      new SevenZFile(seven7File)
    } catch {
      case _: IOException => return None
    }

    try {
      var entry: SevenZArchiveEntry = file.getNextEntry()

      while (entry =!= null) {
        if (!entry.isDirectory && entry.getName === entryName) {
          // Note: File is NOT closed.  The caller is responsible for closing.
          return Some(new InputStreamWrapper(file, closeUnderlying = true))
        }

        entry = file.getNextEntry()
      }

      // Close the file if no matching entry
      file.close()
      None
    } catch {
      // Make sure the file gets closed on an exception
      case ex: Throwable =>
        file.close()
        throw ex
    }
  }

  def entryExists(seven7File: File, entryName: String): Boolean = {
    val file: SevenZFile = try {
      new SevenZFile(seven7File)
    } catch {
      case _: IOException => return false
    }

    try {
      var entry: SevenZArchiveEntry = file.getNextEntry()

      while (entry =!= null) {
        if (!entry.isDirectory && entry.getName === entryName) {
          return true
        }

        entry = file.getNextEntry()
      }

      false
    } finally {
      file.close()
    }
  }

  /**
   * This is to support the various inputStreamForEntry, getInputStreamForEntry, etc. methods that work
   * on a single entry within a 7-Zip file.  This class just exposes the SevenZArchiveEntry in the
   * SevenZFile as an InputStream.  Calling close() will close the SevenZFile.
   *
   * @param file The SevenZFile to wrap
   * @param closeUnderlying Should calling close() close the underlying SevenZFile?
   */
  private class InputStreamWrapper(file: SevenZFile, closeUnderlying: Boolean) extends InputStream {
    override def read(): Int = file.read()
    override def read(b: Array[Byte]): Int = file.read(b)
    override def read(b: Array[Byte], off: Int, len: Int): Int = file.read(b, off, len)
    override def close(): Unit = if (closeUnderlying) file.close()
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy