View Javadoc
1   /**
2    *
3    * Copyright 2004 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.codehaus.plexus.archiver.tar;
18  
19  import javax.inject.Named;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.util.zip.GZIPOutputStream;
26  
27  import io.airlift.compress.snappy.SnappyFramedOutputStream;
28  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
29  import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
30  import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
31  import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
32  import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
33  import org.codehaus.plexus.archiver.AbstractArchiver;
34  import org.codehaus.plexus.archiver.ArchiveEntry;
35  import org.codehaus.plexus.archiver.ArchiverException;
36  import org.codehaus.plexus.archiver.ResourceIterator;
37  import org.codehaus.plexus.archiver.exceptions.EmptyArchiveException;
38  import org.codehaus.plexus.archiver.util.ResourceUtils;
39  import org.codehaus.plexus.archiver.util.Streams;
40  import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributes;
41  import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
42  import org.codehaus.plexus.components.io.resources.PlexusIoResource;
43  import org.codehaus.plexus.util.IOUtil;
44  import org.codehaus.plexus.util.StringUtils;
45  
46  import static org.codehaus.plexus.archiver.util.Streams.bufferedOutputStream;
47  
48  /**
49   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
50   */
51  @Named("tar")
52  public class TarArchiver extends AbstractArchiver {
53  
54      /**
55       * Indicates whether the user has been warned about long files already.
56       */
57      private boolean longWarningGiven = false;
58  
59      private TarLongFileMode longFileMode = TarLongFileMode.warn;
60  
61      private TarCompressionMethod compression = TarCompressionMethod.none;
62  
63      private final TarOptions options = new TarOptions();
64  
65      private TarArchiveOutputStream tOut;
66  
67      /**
68       * Set how to handle long files, those with a path&gt;100 chars.
69       * Optional, default=warn.
70       * <p>
71       * Allowable values are </p>
72       * <ul>
73       * <li> truncate - paths are truncated to the maximum length </li>
74       * <li> fail - paths greater than the maximum cause a build exception </li>
75       * <li> warn - paths greater than the maximum cause a warning and GNU is used </li>
76       * <li> gnu - GNU extensions are used for any paths greater than the maximum. </li>
77       * <li> posix - posix extensions are used for any paths greater than the maximum. </li>
78       * <li> posixwarn - posix extensions are used (with warning) for any paths greater than the maximum. </li>
79       * <li> omit - paths greater than the maximum are omitted from the archive </li>
80       * </ul>
81       *
82       * @param mode the mode to handle long file names.
83       */
84      public void setLongfile(TarLongFileMode mode) {
85          this.longFileMode = mode;
86      }
87  
88      /**
89       * Set compression method.
90       * Allowable values are
91       * <ul>
92       * <li> none - no compression
93       * <li> gzip - Gzip compression
94       * <li> bzip2 - Bzip2 compression
95       * </ul>
96       *
97       * @param mode the compression method.
98       */
99      public void setCompression(TarCompressionMethod mode) {
100         this.compression = mode;
101     }
102 
103     @Override
104     protected void execute() throws ArchiverException, IOException {
105         if (!checkForced()) {
106             return;
107         }
108 
109         ResourceIterator iter = getResources();
110         if (!iter.hasNext()) {
111             throw new EmptyArchiveException("archive cannot be empty");
112         }
113 
114         File tarFile = getDestFile();
115 
116         if (tarFile == null) {
117             throw new ArchiverException("You must set the destination tar file.");
118         }
119         if (tarFile.exists() && !tarFile.isFile()) {
120             throw new ArchiverException(tarFile + " isn't a file.");
121         }
122         if (tarFile.exists() && !tarFile.canWrite()) {
123             throw new ArchiverException(tarFile + " is read-only.");
124         }
125 
126         getLogger().info("Building tar: " + tarFile.getAbsolutePath());
127 
128         try {
129             tOut = new TarArchiveOutputStream(compress(compression, Streams.fileOutputStream(tarFile)), "UTF8");
130             if (longFileMode.isTruncateMode()) {
131                 tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_TRUNCATE);
132             } else if (longFileMode.isPosixMode() || longFileMode.isPosixWarnMode()) {
133                 tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
134                 // Todo: Patch 2.5.1   for this fix. Also make closeable fix on 2.5.1
135                 tOut.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
136             } else if (longFileMode.isFailMode() || longFileMode.isOmitMode()) {
137                 tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_ERROR);
138             } else {
139                 // warn or GNU
140                 tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
141             }
142 
143             longWarningGiven = false;
144             while (iter.hasNext()) {
145                 ArchiveEntry entry = iter.next();
146                 // Check if we don't add tar file in itself
147                 if (ResourceUtils.isSame(entry.getResource(), tarFile)) {
148                     throw new ArchiverException("A tar file cannot include itself.");
149                 }
150                 String fileName = entry.getName();
151                 String name = StringUtils.replace(fileName, File.separatorChar, '/');
152 
153                 tarFile(entry, tOut, name);
154             }
155 
156             tOut.close();
157         } finally {
158             IOUtil.close(tOut);
159         }
160     }
161 
162     /**
163      * tar a file
164      *
165      * @param entry the file to tar
166      * @param tOut the output stream
167      * @param vPath the path name of the file to tar
168      *
169      * @throws IOException on error
170      */
171     protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String vPath)
172             throws ArchiverException, IOException {
173 
174         // don't add "" to the archive
175         if (vPath.length() <= 0) {
176             return;
177         }
178 
179         if (entry.getResource().isDirectory() && !vPath.endsWith("/")) {
180             vPath += "/";
181         }
182 
183         if (vPath.startsWith("/") && !options.getPreserveLeadingSlashes()) {
184             int l = vPath.length();
185             if (l <= 1) {
186                 // we would end up adding "" to the archive
187                 return;
188             }
189             vPath = vPath.substring(1, l);
190         }
191 
192         int pathLength = vPath.length();
193         InputStream fIn = null;
194 
195         try {
196             TarArchiveEntry te;
197             if (!longFileMode.isGnuMode()
198                     && pathLength >= org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN) {
199                 int maxPosixPathLen = org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN
200                         + org.apache.commons.compress.archivers.tar.TarConstants.PREFIXLEN;
201                 if (longFileMode.isPosixMode()) {
202                 } else if (longFileMode.isPosixWarnMode()) {
203                     if (pathLength > maxPosixPathLen) {
204                         getLogger().warn("Entry: " + vPath + " longer than " + maxPosixPathLen + " characters.");
205                         if (!longWarningGiven) {
206                             getLogger()
207                                     .warn("Resulting tar file can only be processed "
208                                             + "successfully by GNU compatible tar commands");
209                             longWarningGiven = true;
210                         }
211                     }
212                 } else if (longFileMode.isOmitMode()) {
213                     getLogger().info("Omitting: " + vPath);
214                     return;
215                 } else if (longFileMode.isWarnMode()) {
216                     getLogger()
217                             .warn("Entry: " + vPath + " longer than "
218                                     + org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN
219                                     + " characters.");
220                     if (!longWarningGiven) {
221                         getLogger()
222                                 .warn("Resulting tar file can only be processed "
223                                         + "successfully by GNU compatible tar commands");
224                         longWarningGiven = true;
225                     }
226                 } else if (longFileMode.isFailMode()) {
227                     throw new ArchiverException("Entry: " + vPath + " longer than "
228                             + org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN
229                             + " characters.");
230                 } else {
231                     throw new IllegalStateException("Non gnu mode should never get here?");
232                 }
233             }
234 
235             if (entry.getType() == ArchiveEntry.SYMLINK) {
236                 final SymlinkDestinationSupplier plexusIoSymlinkResource =
237                         (SymlinkDestinationSupplier) entry.getResource();
238 
239                 te = new TarArchiveEntry(vPath, TarArchiveEntry.LF_SYMLINK);
240                 te.setLinkName(plexusIoSymlinkResource.getSymlinkDestination());
241             } else {
242                 te = new TarArchiveEntry(vPath);
243             }
244 
245             if (getLastModifiedTime() == null) {
246                 long teLastModified = entry.getResource().getLastModified();
247                 te.setModTime(
248                         teLastModified == PlexusIoResource.UNKNOWN_MODIFICATION_DATE
249                                 ? System.currentTimeMillis()
250                                 : teLastModified);
251             } else {
252                 te.setModTime(getLastModifiedTime().toMillis());
253             }
254 
255             if (entry.getType() == ArchiveEntry.SYMLINK) {
256                 te.setSize(0);
257 
258             } else if (!entry.getResource().isDirectory()) {
259                 final long size = entry.getResource().getSize();
260                 te.setSize(size == PlexusIoResource.UNKNOWN_RESOURCE_SIZE ? 0 : size);
261             }
262             te.setMode(entry.getMode());
263 
264             PlexusIoResourceAttributes attributes = entry.getResourceAttributes();
265 
266             te.setUserName(
267                     (attributes != null && attributes.getUserName() != null)
268                             ? attributes.getUserName()
269                             : options.getUserName());
270             te.setGroupName(
271                     (attributes != null && attributes.getGroupName() != null)
272                             ? attributes.getGroupName()
273                             : options.getGroup());
274 
275             final int userId =
276                     (attributes != null && attributes.getUserId() != null) ? attributes.getUserId() : options.getUid();
277             if (userId >= 0) {
278                 te.setUserId(userId);
279             }
280 
281             final int groupId = (attributes != null && attributes.getGroupId() != null)
282                     ? attributes.getGroupId()
283                     : options.getGid();
284             if (groupId >= 0) {
285                 te.setGroupId(groupId);
286             }
287 
288             tOut.putArchiveEntry(te);
289 
290             try {
291                 if (entry.getResource().isFile() && !(entry.getType() == ArchiveEntry.SYMLINK)) {
292                     fIn = entry.getInputStream();
293 
294                     Streams.copyFullyDontCloseOutput(fIn, tOut, "xAR");
295                 }
296 
297             } catch (Throwable e) {
298                 getLogger().warn("When creating tar entry", e);
299             } finally {
300                 tOut.closeArchiveEntry();
301             }
302         } finally {
303             IOUtil.close(fIn);
304         }
305     }
306 
307     /**
308      * Valid Modes for Compression attribute to Tar Task
309      */
310     public class TarOptions {
311 
312         private String userName = "";
313 
314         private String groupName = "";
315 
316         private int uid;
317 
318         private int gid;
319 
320         private boolean preserveLeadingSlashes = false;
321 
322         /**
323          * The username for the tar entry
324          * This is not the same as the UID.
325          *
326          * @param userName the user name for the tar entry.
327          */
328         public void setUserName(String userName) {
329             this.userName = userName;
330         }
331 
332         /**
333          * @return the user name for the tar entry
334          */
335         public String getUserName() {
336             return userName;
337         }
338 
339         /**
340          * The uid for the tar entry
341          * This is not the same as the User name.
342          *
343          * @param uid the id of the user for the tar entry.
344          */
345         public void setUid(int uid) {
346             this.uid = uid;
347         }
348 
349         /**
350          * @return the uid for the tar entry
351          */
352         public int getUid() {
353             return uid;
354         }
355 
356         /**
357          * The groupname for the tar entry; optional, default=""
358          * This is not the same as the GID.
359          *
360          * @param groupName the group name string.
361          */
362         public void setGroup(String groupName) {
363             this.groupName = groupName;
364         }
365 
366         /**
367          * @return the group name string.
368          */
369         public String getGroup() {
370             return groupName;
371         }
372 
373         /**
374          * The GID for the tar entry; optional, default="0"
375          * This is not the same as the group name.
376          *
377          * @param gid the group id.
378          */
379         public void setGid(int gid) {
380             this.gid = gid;
381         }
382 
383         /**
384          * @return the group identifier.
385          */
386         public int getGid() {
387             return gid;
388         }
389 
390         /**
391          * @return the leading slashes flag.
392          */
393         public boolean getPreserveLeadingSlashes() {
394             return preserveLeadingSlashes;
395         }
396 
397         /**
398          * Flag to indicates whether leading `/'s should
399          * be preserved in the file names.
400          * Optional, default is <code>false</code>.
401          *
402          * @param preserveLeadingSlashes the leading slashes flag.
403          */
404         public void setPreserveLeadingSlashes(boolean preserveLeadingSlashes) {
405             this.preserveLeadingSlashes = preserveLeadingSlashes;
406         }
407     }
408 
409     /**
410      * Valid Modes for Compression attribute to Tar Task
411      */
412     public enum TarCompressionMethod {
413         none,
414         gzip,
415         bzip2,
416         snappy,
417         xz,
418         zstd
419     }
420 
421     private OutputStream compress(TarCompressionMethod tarCompressionMethod, final OutputStream ostream)
422             throws IOException {
423         if (TarCompressionMethod.gzip.equals(tarCompressionMethod)) {
424             return bufferedOutputStream(new GZIPOutputStream(ostream));
425         } else if (TarCompressionMethod.bzip2.equals(tarCompressionMethod)) {
426             return new BZip2CompressorOutputStream(bufferedOutputStream(ostream));
427         } else if (TarCompressionMethod.snappy.equals(tarCompressionMethod)) {
428             return new SnappyFramedOutputStream(bufferedOutputStream(ostream));
429         } else if (TarCompressionMethod.xz.equals(tarCompressionMethod)) {
430             return new XZCompressorOutputStream(bufferedOutputStream(ostream));
431         } else if (TarCompressionMethod.zstd.equals(tarCompressionMethod)) {
432             return new ZstdCompressorOutputStream(bufferedOutputStream(ostream));
433         }
434 
435         return ostream;
436     }
437 
438     @Override
439     public boolean isSupportingForced() {
440         return true;
441     }
442 
443     @Override
444     protected void cleanUp() throws IOException {
445         super.cleanUp();
446         if (this.tOut != null) {
447             this.tOut.close();
448         }
449     }
450 
451     @Override
452     protected void close() throws IOException {
453         if (this.tOut != null) {
454             this.tOut.close();
455         }
456     }
457 
458     @Override
459     protected String getArchiveType() {
460         return "TAR";
461     }
462 }