001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.commons.compress.archivers.examples; 020 021import java.io.BufferedInputStream; 022import java.io.File; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.nio.channels.Channels; 027import java.nio.channels.FileChannel; 028import java.nio.channels.SeekableByteChannel; 029import java.nio.file.Files; 030import java.nio.file.StandardOpenOption; 031 032import org.apache.commons.compress.archivers.ArchiveEntry; 033import org.apache.commons.compress.archivers.ArchiveException; 034import org.apache.commons.compress.archivers.ArchiveOutputStream; 035import org.apache.commons.compress.archivers.ArchiveStreamFactory; 036import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; 037import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; 038import org.apache.commons.compress.utils.IOUtils; 039 040/** 041 * Provides a high level API for creating archives. 042 * @since 1.17 043 */ 044public class Archiver { 045 046 private interface ArchiveEntryCreator { 047 ArchiveEntry create(File f, String entryName) throws IOException; 048 } 049 050 private interface ArchiveEntryConsumer { 051 void accept(File source, ArchiveEntry entry) throws IOException; 052 } 053 054 private interface Finisher { 055 void finish() throws IOException; 056 } 057 058 /** 059 * Creates an archive {@code target} using the format {@code 060 * format} by recursively including all files and directories in 061 * {@code directory}. 062 * 063 * @param format the archive format. This uses the same format as 064 * accepted by {@link ArchiveStreamFactory}. 065 * @param target the file to write the new archive to. 066 * @param directory the directory that contains the files to archive. 067 * @throws IOException if an I/O error occurs 068 * @throws ArchiveException if the archive cannot be created for other reasons 069 */ 070 public void create(String format, File target, File directory) throws IOException, ArchiveException { 071 if (prefersSeekableByteChannel(format)) { 072 try (SeekableByteChannel c = FileChannel.open(target.toPath(), StandardOpenOption.WRITE, 073 StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { 074 create(format, c, directory, CloseableConsumer.CLOSING_CONSUMER); 075 } 076 return; 077 } 078 try (OutputStream o = Files.newOutputStream(target.toPath())) { 079 create(format, o, directory, CloseableConsumer.CLOSING_CONSUMER); 080 } 081 } 082 083 /** 084 * Creates an archive {@code target} using the format {@code 085 * format} by recursively including all files and directories in 086 * {@code directory}. 087 * 088 * <p>This method creates a wrapper around the target stream 089 * which is never closed and thus leaks resources, please use 090 * {@link #create(String,OutputStream,File,CloseableConsumer)} 091 * instead.</p> 092 * 093 * @param format the archive format. This uses the same format as 094 * accepted by {@link ArchiveStreamFactory}. 095 * @param target the stream to write the new archive to. 096 * @param directory the directory that contains the files to archive. 097 * @throws IOException if an I/O error occurs 098 * @throws ArchiveException if the archive cannot be created for other reasons 099 * @deprecated this method leaks resources 100 */ 101 @Deprecated 102 public void create(String format, OutputStream target, File directory) throws IOException, ArchiveException { 103 create(format, target, directory, CloseableConsumer.NULL_CONSUMER); 104 } 105 106 /** 107 * Creates an archive {@code target} using the format {@code 108 * format} by recursively including all files and directories in 109 * {@code directory}. 110 * 111 * <p>This method creates a wrapper around the archive stream and 112 * the caller of this method is responsible for closing it - 113 * probably at the same time as closing the stream itself. The 114 * caller is informed about the wrapper object via the {@code 115 * closeableConsumer} callback as soon as it is no longer needed 116 * by this class.</p> 117 * 118 * @param format the archive format. This uses the same format as 119 * accepted by {@link ArchiveStreamFactory}. 120 * @param target the stream to write the new archive to. 121 * @param directory the directory that contains the files to archive. 122 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 123 * @throws IOException if an I/O error occurs 124 * @throws ArchiveException if the archive cannot be created for other reasons 125 * @since 1.19 126 */ 127 public void create(String format, OutputStream target, File directory, 128 CloseableConsumer closeableConsumer) throws IOException, ArchiveException { 129 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 130 create(c.track(new ArchiveStreamFactory().createArchiveOutputStream(format, target)), 131 directory); 132 } 133 } 134 135 /** 136 * Creates an archive {@code target} using the format {@code 137 * format} by recursively including all files and directories in 138 * {@code directory}. 139 * 140 * <p>This method creates a wrapper around the target channel 141 * which is never closed and thus leaks resources, please use 142 * {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} 143 * instead.</p> 144 * 145 * @param format the archive format. This uses the same format as 146 * accepted by {@link ArchiveStreamFactory}. 147 * @param target the channel to write the new archive to. 148 * @param directory the directory that contains the files to archive. 149 * @throws IOException if an I/O error occurs 150 * @throws ArchiveException if the archive cannot be created for other reasons 151 * @deprecated this method leaks resources 152 */ 153 @Deprecated 154 public void create(String format, SeekableByteChannel target, File directory) 155 throws IOException, ArchiveException { 156 create(format, target, directory, CloseableConsumer.NULL_CONSUMER); 157 } 158 159 /** 160 * Creates an archive {@code target} using the format {@code 161 * format} by recursively including all files and directories in 162 * {@code directory}. 163 * 164 * <p>This method creates a wrapper around the archive channel and 165 * the caller of this method is responsible for closing it - 166 * probably at the same time as closing the channel itself. The 167 * caller is informed about the wrapper object via the {@code 168 * closeableConsumer} callback as soon as it is no longer needed 169 * by this class.</p> 170 * 171 * @param format the archive format. This uses the same format as 172 * accepted by {@link ArchiveStreamFactory}. 173 * @param target the channel to write the new archive to. 174 * @param directory the directory that contains the files to archive. 175 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 176 * @throws IOException if an I/O error occurs 177 * @throws ArchiveException if the archive cannot be created for other reasons 178 * @since 1.19 179 */ 180 public void create(String format, SeekableByteChannel target, File directory, 181 CloseableConsumer closeableConsumer) 182 throws IOException, ArchiveException { 183 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 184 if (!prefersSeekableByteChannel(format)) { 185 create(format, c.track(Channels.newOutputStream(target)), directory); 186 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 187 create(c.track(new ZipArchiveOutputStream(target)), directory); 188 } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 189 create(c.track(new SevenZOutputFile(target)), directory); 190 } else { 191 // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z 192 throw new ArchiveException("Don't know how to handle format " + format); 193 } 194 } 195 } 196 197 /** 198 * Creates an archive {@code target} by recursively including all 199 * files and directories in {@code directory}. 200 * 201 * @param target the stream to write the new archive to. 202 * @param directory the directory that contains the files to archive. 203 * @throws IOException if an I/O error occurs 204 * @throws ArchiveException if the archive cannot be created for other reasons 205 */ 206 public void create(final ArchiveOutputStream target, File directory) 207 throws IOException, ArchiveException { 208 create(directory, new ArchiveEntryCreator() { 209 public ArchiveEntry create(File f, String entryName) throws IOException { 210 return target.createArchiveEntry(f, entryName); 211 } 212 }, new ArchiveEntryConsumer() { 213 public void accept(File source, ArchiveEntry e) throws IOException { 214 target.putArchiveEntry(e); 215 if (!e.isDirectory()) { 216 try (InputStream in = new BufferedInputStream(Files.newInputStream(source.toPath()))) { 217 IOUtils.copy(in, target); 218 } 219 } 220 target.closeArchiveEntry(); 221 } 222 }, new Finisher() { 223 public void finish() throws IOException { 224 target.finish(); 225 } 226 }); 227 } 228 229 /** 230 * Creates an archive {@code target} by recursively including all 231 * files and directories in {@code directory}. 232 * 233 * @param target the file to write the new archive to. 234 * @param directory the directory that contains the files to archive. 235 * @throws IOException if an I/O error occurs 236 */ 237 public void create(final SevenZOutputFile target, File directory) throws IOException { 238 create(directory, new ArchiveEntryCreator() { 239 public ArchiveEntry create(File f, String entryName) throws IOException { 240 return target.createArchiveEntry(f, entryName); 241 } 242 }, new ArchiveEntryConsumer() { 243 public void accept(File source, ArchiveEntry e) throws IOException { 244 target.putArchiveEntry(e); 245 if (!e.isDirectory()) { 246 final byte[] buffer = new byte[8024]; 247 int n = 0; 248 long count = 0; 249 try (InputStream in = new BufferedInputStream(Files.newInputStream(source.toPath()))) { 250 while (-1 != (n = in.read(buffer))) { 251 target.write(buffer, 0, n); 252 count += n; 253 } 254 } 255 } 256 target.closeArchiveEntry(); 257 } 258 }, new Finisher() { 259 public void finish() throws IOException { 260 target.finish(); 261 } 262 }); 263 } 264 265 private boolean prefersSeekableByteChannel(String format) { 266 return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); 267 } 268 269 private void create(File directory, ArchiveEntryCreator creator, ArchiveEntryConsumer consumer, 270 Finisher finisher) throws IOException { 271 create("", directory, creator, consumer); 272 finisher.finish(); 273 } 274 275 private void create(String prefix, File directory, ArchiveEntryCreator creator, ArchiveEntryConsumer consumer) 276 throws IOException { 277 File[] children = directory.listFiles(); 278 if (children == null) { 279 return; 280 } 281 for (File f : children) { 282 String entryName = prefix + f.getName() + (f.isDirectory() ? "/" : ""); 283 consumer.accept(f, creator.create(f, entryName)); 284 if (f.isDirectory()) { 285 create(entryName, f, creator, consumer); 286 } 287 } 288 } 289}