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 */ 019 package org.apache.commons.compress.archivers.tar; 020 021 import java.io.File; 022 import java.io.IOException; 023 import java.io.OutputStream; 024 import org.apache.commons.compress.archivers.ArchiveEntry; 025 import org.apache.commons.compress.archivers.ArchiveOutputStream; 026 import org.apache.commons.compress.utils.ArchiveUtils; 027 import org.apache.commons.compress.utils.CountingOutputStream; 028 029 /** 030 * The TarOutputStream writes a UNIX tar archive as an OutputStream. 031 * Methods are provided to put entries, and then write their contents 032 * by writing to this stream using write(). 033 * @NotThreadSafe 034 */ 035 public class TarArchiveOutputStream extends ArchiveOutputStream { 036 /** Fail if a long file name is required in the archive. */ 037 public static final int LONGFILE_ERROR = 0; 038 039 /** Long paths will be truncated in the archive. */ 040 public static final int LONGFILE_TRUNCATE = 1; 041 042 /** GNU tar extensions are used to store long file names in the archive. */ 043 public static final int LONGFILE_GNU = 2; 044 045 private long currSize; 046 private String currName; 047 private long currBytes; 048 private final byte[] recordBuf; 049 private int assemLen; 050 private final byte[] assemBuf; 051 protected final TarBuffer buffer; 052 private int longFileMode = LONGFILE_ERROR; 053 054 private boolean closed = false; 055 056 /** Indicates if putArchiveEntry has been called without closeArchiveEntry */ 057 private boolean haveUnclosedEntry = false; 058 059 /** indicates if this archive is finished */ 060 private boolean finished = false; 061 062 private final OutputStream out; 063 064 /** 065 * Constructor for TarInputStream. 066 * @param os the output stream to use 067 */ 068 public TarArchiveOutputStream(OutputStream os) { 069 this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); 070 } 071 072 /** 073 * Constructor for TarInputStream. 074 * @param os the output stream to use 075 * @param blockSize the block size to use 076 */ 077 public TarArchiveOutputStream(OutputStream os, int blockSize) { 078 this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE); 079 } 080 081 /** 082 * Constructor for TarInputStream. 083 * @param os the output stream to use 084 * @param blockSize the block size to use 085 * @param recordSize the record size to use 086 */ 087 public TarArchiveOutputStream(OutputStream os, int blockSize, int recordSize) { 088 out = new CountingOutputStream(os); 089 090 this.buffer = new TarBuffer(out, blockSize, recordSize); 091 this.assemLen = 0; 092 this.assemBuf = new byte[recordSize]; 093 this.recordBuf = new byte[recordSize]; 094 } 095 096 /** 097 * Set the long file mode. 098 * This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or LONGFILE_GNU(2). 099 * This specifies the treatment of long file names (names >= TarConstants.NAMELEN). 100 * Default is LONGFILE_ERROR. 101 * @param longFileMode the mode to use 102 */ 103 public void setLongFileMode(int longFileMode) { 104 this.longFileMode = longFileMode; 105 } 106 107 108 @Deprecated 109 @Override 110 public int getCount() { 111 return (int) getBytesWritten(); 112 } 113 114 @Override 115 public long getBytesWritten() { 116 return ((CountingOutputStream) out).getBytesWritten(); 117 } 118 119 /** 120 * Ends the TAR archive without closing the underlying OutputStream. 121 * 122 * An archive consists of a series of file entries terminated by an 123 * end-of-archive entry, which consists of two 512 blocks of zero bytes. 124 * POSIX.1 requires two EOF records, like some other implementations. 125 * 126 * @throws IOException on error 127 */ 128 @Override 129 public void finish() throws IOException { 130 if (finished) { 131 throw new IOException("This archive has already been finished"); 132 } 133 134 if(haveUnclosedEntry) { 135 throw new IOException("This archives contains unclosed entries."); 136 } 137 writeEOFRecord(); 138 writeEOFRecord(); 139 buffer.flushBlock(); 140 finished = true; 141 } 142 143 /** 144 * Closes the underlying OutputStream. 145 * @throws IOException on error 146 */ 147 @Override 148 public void close() throws IOException { 149 if(!finished) { 150 finish(); 151 } 152 153 if (!closed) { 154 buffer.close(); 155 out.close(); 156 closed = true; 157 } 158 } 159 160 /** 161 * Get the record size being used by this stream's TarBuffer. 162 * 163 * @return The TarBuffer record size. 164 */ 165 public int getRecordSize() { 166 return buffer.getRecordSize(); 167 } 168 169 /** 170 * Put an entry on the output stream. This writes the entry's 171 * header record and positions the output stream for writing 172 * the contents of the entry. Once this method is called, the 173 * stream is ready for calls to write() to write the entry's 174 * contents. Once the contents are written, closeArchiveEntry() 175 * <B>MUST</B> be called to ensure that all buffered data 176 * is completely written to the output stream. 177 * 178 * @param archiveEntry The TarEntry to be written to the archive. 179 * @throws IOException on error 180 * @throws ClassCastException if archiveEntry is not an instance of TarArchiveEntry 181 */ 182 @Override 183 public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException { 184 if(finished) { 185 throw new IOException("Stream has already been finished"); 186 } 187 TarArchiveEntry entry = (TarArchiveEntry) archiveEntry; 188 if (entry.getName().length() >= TarConstants.NAMELEN) { 189 190 if (longFileMode == LONGFILE_GNU) { 191 // create a TarEntry for the LongLink, the contents 192 // of which are the entry's name 193 TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK, 194 TarConstants.LF_GNUTYPE_LONGNAME); 195 196 final byte[] nameBytes = ArchiveUtils.toAsciiBytes(entry.getName()); 197 longLinkEntry.setSize(nameBytes.length + 1); // +1 for NUL 198 putArchiveEntry(longLinkEntry); 199 write(nameBytes); 200 write(0); // NUL terminator 201 closeArchiveEntry(); 202 } else if (longFileMode != LONGFILE_TRUNCATE) { 203 throw new RuntimeException("file name '" + entry.getName() 204 + "' is too long ( > " 205 + TarConstants.NAMELEN + " bytes)"); 206 } 207 } 208 209 entry.writeEntryHeader(recordBuf); 210 buffer.writeRecord(recordBuf); 211 212 currBytes = 0; 213 214 if (entry.isDirectory()) { 215 currSize = 0; 216 } else { 217 currSize = entry.getSize(); 218 } 219 currName = entry.getName(); 220 haveUnclosedEntry = true; 221 } 222 223 /** 224 * Close an entry. This method MUST be called for all file 225 * entries that contain data. The reason is that we must 226 * buffer data written to the stream in order to satisfy 227 * the buffer's record based writes. Thus, there may be 228 * data fragments still being assembled that must be written 229 * to the output stream before this entry is closed and the 230 * next entry written. 231 * @throws IOException on error 232 */ 233 @Override 234 public void closeArchiveEntry() throws IOException { 235 if(finished) { 236 throw new IOException("Stream has already been finished"); 237 } 238 if (!haveUnclosedEntry){ 239 throw new IOException("No current entry to close"); 240 } 241 if (assemLen > 0) { 242 for (int i = assemLen; i < assemBuf.length; ++i) { 243 assemBuf[i] = 0; 244 } 245 246 buffer.writeRecord(assemBuf); 247 248 currBytes += assemLen; 249 assemLen = 0; 250 } 251 252 if (currBytes < currSize) { 253 throw new IOException("entry '" + currName + "' closed at '" 254 + currBytes 255 + "' before the '" + currSize 256 + "' bytes specified in the header were written"); 257 } 258 haveUnclosedEntry = false; 259 } 260 261 /** 262 * Writes bytes to the current tar archive entry. This method 263 * is aware of the current entry and will throw an exception if 264 * you attempt to write bytes past the length specified for the 265 * current entry. The method is also (painfully) aware of the 266 * record buffering required by TarBuffer, and manages buffers 267 * that are not a multiple of recordsize in length, including 268 * assembling records from small buffers. 269 * 270 * @param wBuf The buffer to write to the archive. 271 * @param wOffset The offset in the buffer from which to get bytes. 272 * @param numToWrite The number of bytes to write. 273 * @throws IOException on error 274 */ 275 @Override 276 public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { 277 if ((currBytes + numToWrite) > currSize) { 278 throw new IOException("request to write '" + numToWrite 279 + "' bytes exceeds size in header of '" 280 + currSize + "' bytes for entry '" 281 + currName + "'"); 282 283 // 284 // We have to deal with assembly!!! 285 // The programmer can be writing little 32 byte chunks for all 286 // we know, and we must assemble complete records for writing. 287 // REVIEW Maybe this should be in TarBuffer? Could that help to 288 // eliminate some of the buffer copying. 289 // 290 } 291 292 if (assemLen > 0) { 293 if ((assemLen + numToWrite) >= recordBuf.length) { 294 int aLen = recordBuf.length - assemLen; 295 296 System.arraycopy(assemBuf, 0, recordBuf, 0, 297 assemLen); 298 System.arraycopy(wBuf, wOffset, recordBuf, 299 assemLen, aLen); 300 buffer.writeRecord(recordBuf); 301 302 currBytes += recordBuf.length; 303 wOffset += aLen; 304 numToWrite -= aLen; 305 assemLen = 0; 306 } else { 307 System.arraycopy(wBuf, wOffset, assemBuf, assemLen, 308 numToWrite); 309 310 wOffset += numToWrite; 311 assemLen += numToWrite; 312 numToWrite = 0; 313 } 314 } 315 316 // 317 // When we get here we have EITHER: 318 // o An empty "assemble" buffer. 319 // o No bytes to write (numToWrite == 0) 320 // 321 while (numToWrite > 0) { 322 if (numToWrite < recordBuf.length) { 323 System.arraycopy(wBuf, wOffset, assemBuf, assemLen, 324 numToWrite); 325 326 assemLen += numToWrite; 327 328 break; 329 } 330 331 buffer.writeRecord(wBuf, wOffset); 332 333 int num = recordBuf.length; 334 335 currBytes += num; 336 numToWrite -= num; 337 wOffset += num; 338 } 339 } 340 341 /** 342 * Write an EOF (end of archive) record to the tar archive. 343 * An EOF record consists of a record of all zeros. 344 */ 345 private void writeEOFRecord() throws IOException { 346 for (int i = 0; i < recordBuf.length; ++i) { 347 recordBuf[i] = 0; 348 } 349 350 buffer.writeRecord(recordBuf); 351 } 352 353 @Override 354 public void flush() throws IOException { 355 out.flush(); 356 } 357 358 /** {@inheritDoc} */ 359 @Override 360 public ArchiveEntry createArchiveEntry(File inputFile, String entryName) 361 throws IOException { 362 if(finished) { 363 throw new IOException("Stream has already been finished"); 364 } 365 return new TarArchiveEntry(inputFile, entryName); 366 } 367 }