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