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 }