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