001/*
002** Authored by Timothy Gerard Endres
003** <mailto:time@gjt.org>  <http://www.trustice.com>
004** 
005** This work has been placed into the public domain.
006** You may use this work in any way and for any purpose you wish.
007**
008** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
009** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
010** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
011** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
012** REDISTRIBUTION OF THIS SOFTWARE. 
013** 
014*/
015
016package com.ice.tar;
017
018import java.io.*;
019import javax.activation.*;
020
021
022/**
023 * The TarArchive class implements the concept of a
024 * tar archive. A tar archive is a series of entries, each of
025 * which represents a file system object. Each entry in
026 * the archive consists of a header record. Directory entries
027 * consist only of the header record, and are followed by entries
028 * for the directory's contents. File entries consist of a
029 * header record followed by the number of records needed to
030 * contain the file's contents. All entries are written on
031 * record boundaries. Records are 512 bytes long.
032 *
033 * TarArchives are instantiated in either read or write mode,
034 * based upon whether they are instantiated with an InputStream
035 * or an OutputStream. Once instantiated TarArchives read/write
036 * mode can not be changed.
037 *
038 * There is currently no support for random access to tar archives.
039 * However, it seems that subclassing TarArchive, and using the
040 * TarBuffer.getCurrentRecordNum() and TarBuffer.getCurrentBlockNum()
041 * methods, this would be rather trvial.
042 *
043 * @version $Revision: 1.15 $
044 * @author Timothy Gerard Endres, <time@gjt.org>
045 * @see TarBuffer
046 * @see TarHeader
047 * @see TarEntry
048 */
049
050
051public class
052TarArchive extends Object
053        {
054        protected boolean                       verbose;
055        protected boolean                       debug;
056        protected boolean                       keepOldFiles;
057        protected boolean                       asciiTranslate;
058
059        protected int                           userId;
060        protected String                        userName;
061        protected int                           groupId;
062        protected String                        groupName;
063
064        protected String                        rootPath;
065        protected String                        tempPath;
066        protected String                        pathPrefix;
067
068        protected int                           recordSize;
069        protected byte[]                        recordBuf;
070
071        protected TarInputStream        tarIn;
072        protected TarOutputStream       tarOut;
073
074        protected TarTransFileTyper             transTyper;
075        protected TarProgressDisplay    progressDisplay;
076
077
078        /**
079         * The InputStream based constructors create a TarArchive for the
080         * purposes of e'x'tracting or lis't'ing a tar archive. Thus, use
081         * these constructors when you wish to extract files from or list
082         * the contents of an existing tar archive.
083         */
084
085        public
086        TarArchive( InputStream inStream )
087                {
088                this( inStream, TarBuffer.DEFAULT_BLKSIZE );
089                }
090
091        public
092        TarArchive( InputStream inStream, int blockSize )
093                {
094                this( inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE );
095                }
096
097        public
098        TarArchive( InputStream inStream, int blockSize, int recordSize )
099                {
100                this.tarIn = new TarInputStream( inStream, blockSize, recordSize );
101                this.initialize( recordSize );
102                }
103
104        /**
105         * The OutputStream based constructors create a TarArchive for the
106         * purposes of 'c'reating a tar archive. Thus, use these constructors
107         * when you wish to create a new tar archive and write files into it.
108         */
109
110        public
111        TarArchive( OutputStream outStream )
112                {
113                this( outStream, TarBuffer.DEFAULT_BLKSIZE );
114                }
115
116        public
117        TarArchive( OutputStream outStream, int blockSize )
118                {
119                this( outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE );
120                }
121
122        public
123        TarArchive( OutputStream outStream, int blockSize, int recordSize )
124                {
125                this.tarOut = new TarOutputStream( outStream, blockSize, recordSize );
126                this.initialize( recordSize );
127                }
128
129        /**
130         * Common constructor initialization code.
131         */
132
133        private void
134        initialize( int recordSize )
135                {
136                this.rootPath = null;
137                this.pathPrefix = null;
138                this.tempPath = System.getProperty( "user.dir" );
139
140                this.userId = 0;
141                this.userName = "";
142                this.groupId = 0;
143                this.groupName = "";
144
145                this.debug = false;
146                this.verbose = false;
147                this.keepOldFiles = false;
148                this.progressDisplay = null;
149
150                this.recordBuf =
151                        new byte[ this.getRecordSize() ];
152                }
153
154        /**
155         * Set the debugging flag.
156         *
157         * @param debugF The new debug setting.
158         */
159
160        public void
161        setDebug( boolean debugF )
162                {
163                this.debug = debugF;
164                if ( this.tarIn != null )
165                        this.tarIn.setDebug( debugF );
166                else if ( this.tarOut != null )
167                        this.tarOut.setDebug( debugF );
168                }
169
170        /**
171         * Returns the verbosity setting.
172         *
173         * @return The current verbosity setting.
174         */
175
176        public boolean
177        isVerbose()
178                {
179                return this.verbose;
180                }
181
182        /**
183         * Set the verbosity flag.
184         *
185         * @param verbose The new verbosity setting.
186         */
187
188        public void
189        setVerbose( boolean verbose )
190                {
191                this.verbose = verbose;
192                }
193
194        /**
195         * Set the current progress display interface. This allows the
196         * programmer to use a custom class to display the progress of
197         * the archive's processing.
198         *
199         * @param display The new progress display interface.
200         * @see TarProgressDisplay
201         */
202
203        public void
204        setTarProgressDisplay( TarProgressDisplay display )
205                {
206                this.progressDisplay = display;
207                }
208
209        /**
210         * Set the flag that determines whether existing files are
211         * kept, or overwritten during extraction.
212         *
213         * @param keepOldFiles If true, do not overwrite existing files.
214         */
215
216        public void
217        setKeepOldFiles( boolean keepOldFiles )
218                {
219                this.keepOldFiles = keepOldFiles;
220                }
221        
222        /**
223         * Set the ascii file translation flag. If ascii file translatio
224         * is true, then the MIME file type will be consulted to determine
225         * if the file is of type 'text/*'. If the MIME type is not found,
226         * then the TransFileTyper is consulted if it is not null. If
227         * either of these two checks indicates the file is an ascii text
228         * file, it will be translated. The translation converts the local
229         * operating system's concept of line ends into the UNIX line end,
230         * '\n', which is the defacto standard for a TAR archive. This makes
231         * text files compatible with UNIX, and since most tar implementations
232         * for other platforms, compatible with most other platforms.
233         *
234         * @param asciiTranslate If true, translate ascii text files.
235         */
236
237        public void
238        setAsciiTranslation( boolean asciiTranslate )
239                {
240                this.asciiTranslate = asciiTranslate;
241                }
242
243        /**
244         * Set the object that will determine if a file is of type
245         * ascii text for translation purposes.
246         *
247         * @param transTyper The new TransFileTyper object.
248         */
249
250        public void
251        setTransFileTyper( TarTransFileTyper transTyper )
252                {
253                this.transTyper = transTyper;
254                }
255
256        /**
257         * Set user and group information that will be used to fill in the
258         * tar archive's entry headers. Since Java currently provides no means
259         * of determining a user name, user id, group name, or group id for
260         * a given File, TarArchive allows the programmer to specify values
261         * to be used in their place.
262         *
263         * @param userId The user Id to use in the headers.
264         * @param userName The user name to use in the headers.
265         * @param groupId The group id to use in the headers.
266         * @param groupName The group name to use in the headers.
267         */
268
269        public void
270        setUserInfo(
271                        int userId, String userName,
272                        int groupId, String groupName )
273                {
274                this.userId = userId;
275                this.userName = userName;
276                this.groupId = groupId;
277                this.groupName = groupName;
278                }
279
280        /**
281         * Get the user id being used for archive entry headers.
282         *
283         * @return The current user id.
284         */
285
286        public int
287        getUserId()
288                {
289                return this.userId;
290                }
291
292        /**
293         * Get the user name being used for archive entry headers.
294         *
295         * @return The current user name.
296         */
297
298        public String
299        getUserName()
300                {
301                return this.userName;
302                }
303
304        /**
305         * Get the group id being used for archive entry headers.
306         *
307         * @return The current group id.
308         */
309
310        public int
311        getGroupId()
312                {
313                return this.groupId;
314                }
315
316        /**
317         * Get the group name being used for archive entry headers.
318         *
319         * @return The current group name.
320         */
321
322        public String
323        getGroupName()
324                {
325                return this.groupName;
326                }
327
328        /**
329         * Get the current temporary directory path. Because Java's
330         * File did not support temporary files until version 1.2,
331         * TarArchive manages its own concept of the temporary
332         * directory. The temporary directory defaults to the
333         * 'user.dir' System property.
334         *
335         * @return The current temporary directory path.
336         */
337
338        public String
339        getTempDirectory()
340                {
341                return this.tempPath;
342                }
343
344        /**
345         * Set the current temporary directory path.
346         *
347         * @param path The new temporary directory path.
348         */
349
350        public void
351        setTempDirectory( String path )
352                {
353                this.tempPath = path;
354                }
355
356        /**
357         * Get the archive's record size. Because of its history, tar
358         * supports the concept of buffered IO consisting of BLOCKS of
359         * RECORDS. This allowed tar to match the IO characteristics of
360         * the physical device being used. Of course, in the Java world,
361         * this makes no sense, WITH ONE EXCEPTION - archives are expected
362         * to be propertly "blocked". Thus, all of the horrible TarBuffer
363         * support boils down to simply getting the "boundaries" correct.
364         *
365         * @return The record size this archive is using.
366         */
367
368        public int
369        getRecordSize()
370                {
371                if ( this.tarIn != null )
372                        {
373                        return this.tarIn.getRecordSize();
374                        }
375                else if ( this.tarOut != null )
376                        {
377                        return this.tarOut.getRecordSize();
378                        }
379                
380                return TarBuffer.DEFAULT_RCDSIZE;
381                }
382
383        /**
384         * Get a path for a temporary file for a given File. The
385         * temporary file is NOT created. The algorithm attempts
386         * to handle filename collisions so that the name is
387         * unique.
388         *
389         * @return The temporary file's path.
390         */
391
392        private String
393        getTempFilePath( File eFile )
394                {
395                String pathStr =
396                        this.tempPath + File.separator
397                                + eFile.getName() + ".tmp";
398
399                for ( int i = 1 ; i < 5 ; ++i )
400                        {
401                        File f = new File( pathStr );
402
403                        if ( ! f.exists() )
404                                break;
405
406                        pathStr =
407                                this.tempPath + File.separator
408                                        + eFile.getName() + "-" + i + ".tmp";
409                        }
410
411                return pathStr;
412                }
413
414        /**
415         * Close the archive. This simply calls the underlying
416         * tar stream's close() method.
417         */
418
419        public void
420        closeArchive()
421                throws IOException
422                {
423                if ( this.tarIn != null )
424                        {
425                        this.tarIn.close();
426                        }
427                else if ( this.tarOut != null )
428                        {
429                        this.tarOut.close();
430                        }
431                }
432
433        /**
434         * Perform the "list" command and list the contents of the archive.
435         * NOTE That this method uses the progress display to actually list
436         * the conents. If the progress display is not set, nothing will be
437         * listed!
438         */
439
440        public void
441        listContents()
442                throws IOException, InvalidHeaderException
443                {
444                for ( ; ; )
445                        {
446                        TarEntry entry = this.tarIn.getNextEntry();
447
448
449                        if ( entry == null )
450                                {
451                                if ( this.debug )
452                                        {
453                                        System.err.println( "READ EOF RECORD" );
454                                        }
455                                break;
456                                }
457
458                        if ( this.progressDisplay != null )
459                                this.progressDisplay.showTarProgressMessage
460                                        ( entry.getName() );
461                        }
462                }
463
464        /**
465         * Perform the "extract" command and extract the contents of the archive.
466         *
467         * @param destDir The destination directory into which to extract.
468         */
469
470        public void
471        extractContents( File destDir )
472                throws IOException, InvalidHeaderException
473                {
474                for ( ; ; )
475                        {
476                        TarEntry entry = this.tarIn.getNextEntry();
477
478                        if ( entry == null )
479                                {
480                                if ( this.debug )
481                                        {
482                                        System.err.println( "READ EOF RECORD" );
483                                        }
484                                break;
485                                }
486
487                        this.extractEntry( destDir, entry );
488                        }
489                }
490
491        /**
492         * Extract an entry from the archive. This method assumes that the
493         * tarIn stream has been properly set with a call to getNextEntry().
494         *
495         * @param destDir The destination directory into which to extract.
496         * @param entry The TarEntry returned by tarIn.getNextEntry().
497         */
498
499        private void
500        extractEntry( File destDir, TarEntry entry )
501                throws IOException
502                {
503                if ( this.verbose )
504                        {
505                        if ( this.progressDisplay != null )
506                                this.progressDisplay.showTarProgressMessage
507                                        ( entry.getName() );
508                        }
509
510                String name = entry.getName();
511                name = name.replace( '/', File.separatorChar );
512
513                File destFile = new File( destDir, name );
514
515                if ( entry.isDirectory() )
516                        {
517                        if ( ! destFile.exists() )
518                                {
519                                if ( ! destFile.mkdirs() )
520                                        {
521                                        throw new IOException
522                                                ( "error making directory path '"
523                                                        + destFile.getPath() + "'" );
524                                        }
525                                }
526                        }
527                else
528                        {
529                        File subDir = new File( destFile.getParent() );
530
531                        if ( ! subDir.exists() )
532                                {
533                                if ( ! subDir.mkdirs() )
534                                        {
535                                        throw new IOException
536                                                ( "error making directory path '"
537                                                        + subDir.getPath() + "'" );
538                                        }
539                                }
540
541                        if ( this.keepOldFiles && destFile.exists() )
542                                {
543                                if ( this.verbose )
544                                        {
545                                        if ( this.progressDisplay != null )
546                                                this.progressDisplay.showTarProgressMessage
547                                                        ( "not overwriting " + entry.getName() );
548                                        }
549                                }
550                        else
551                                {
552                                boolean asciiTrans = false;
553
554                                FileOutputStream out =
555                                        new FileOutputStream( destFile );
556
557                                if ( this.asciiTranslate )
558                                        {
559                                        MimeType mime = null;
560                                        String contentType = null;
561
562                                        try {
563                                                contentType =
564                                                        FileTypeMap.getDefaultFileTypeMap().
565                                                                getContentType( destFile );
566
567                                                mime = new MimeType( contentType );
568
569                                                if ( mime.getPrimaryType().
570                                                                equalsIgnoreCase( "text" ) )
571                                                        {
572                                                        asciiTrans = true;
573                                                        }
574                                                else if ( this.transTyper != null )
575                                                        {
576                                                        if ( this.transTyper.isAsciiFile( entry.getName() ) )
577                                                                {
578                                                                asciiTrans = true;
579                                                                }
580                                                        }
581                                                }
582                                        catch ( MimeTypeParseException ex )
583                                                { }
584
585                                        if ( this.debug )
586                                                {
587                                                System.err.println
588                                                        ( "EXTRACT TRANS? '" + asciiTrans
589                                                                + "'  ContentType='" + contentType
590                                                                + "'  PrimaryType='" + mime.getPrimaryType()
591                                                                + "'" );
592                                                }
593                                        }
594
595                                PrintWriter outw = null;
596                                if ( asciiTrans )
597                                        {
598                                        outw = new PrintWriter( out );
599                                        }
600
601                                byte[] rdbuf = new byte[32 * 1024];
602
603                                for ( ; ; )
604                                        {
605                                        int numRead = this.tarIn.read( rdbuf );
606
607                                        if ( numRead == -1 )
608                                                break;
609                                        
610                                        if ( asciiTrans )
611                                                {
612                                                for ( int off = 0, b = 0 ; b < numRead ; ++b )
613                                                        {
614                                                        if ( rdbuf[ b ] == 10 )
615                                                                {
616                                                                String s = new String
617                                                                        ( rdbuf, off, (b - off) );
618
619                                                                outw.println( s );
620
621                                                                off = b + 1;
622                                                                }
623                                                        }
624                                                }
625                                        else
626                                                {
627                                                out.write( rdbuf, 0, numRead );
628                                                }
629                                        }
630
631                                if ( asciiTrans )
632                                        outw.close();
633                                else
634                                        out.close();
635                                }
636                        }
637                }
638
639        /**
640         * Write an entry to the archive. This method will call the putNextEntry()
641         * and then write the contents of the entry, and finally call closeEntry()
642         * for entries that are files. For directories, it will call putNextEntry(),
643         * and then, if the recurse flag is true, process each entry that is a
644         * child of the directory.
645         *
646         * @param entry The TarEntry representing the entry to write to the archive.
647         * @param recurse If true, process the children of directory entries.
648         */
649
650        public void
651        writeEntry( TarEntry oldEntry, boolean recurse )
652                throws IOException
653                {
654                boolean asciiTrans = false;
655                boolean unixArchiveFormat = oldEntry.isUnixTarFormat();
656
657                File tFile = null;
658                File eFile = oldEntry.getFile();
659
660                // Work on a copy of the entry so we can manipulate it.
661                // Note that we must distinguish how the entry was constructed.
662                //
663                TarEntry entry = (TarEntry) oldEntry.clone();
664
665                if ( this.verbose )
666                        {
667                        if ( this.progressDisplay != null )
668                                this.progressDisplay.showTarProgressMessage
669                                        ( entry.getName() );
670                        }
671
672                if ( this.asciiTranslate
673                                 && ! entry.isDirectory() )
674                        {
675                        MimeType mime = null;
676                        String contentType = null;
677
678                        try {
679                                contentType =
680                                        FileTypeMap.getDefaultFileTypeMap().
681                                                getContentType( eFile );
682
683                                mime = new MimeType( contentType );
684
685                                if ( mime.getPrimaryType().
686                                                equalsIgnoreCase( "text" ) )
687                                        {
688                                        asciiTrans = true;
689                                        }
690                                else if ( this.transTyper != null )
691                                        {
692                                        if ( this.transTyper.isAsciiFile( eFile ) )
693                                                {
694                                                asciiTrans = true;
695                                                }
696                                        }
697                                }
698                        catch ( MimeTypeParseException ex )
699                                {
700                                // IGNORE THIS ERROR...
701                                }
702
703                        if ( this.debug )
704                                {
705                                System.err.println
706                                        ( "CREATE TRANS? '" + asciiTrans
707                                                + "'  ContentType='" + contentType
708                                                + "'  PrimaryType='" + mime.getPrimaryType()
709                                                + "'" );
710                                }
711
712                        if ( asciiTrans )
713                                {
714                                String tempFileName =
715                                        this.getTempFilePath( eFile );
716
717                                tFile = new File( tempFileName );
718
719                                BufferedReader in =
720                                        new BufferedReader
721                                                ( new InputStreamReader
722                                                        ( new FileInputStream( eFile ) ) );
723
724                                BufferedOutputStream out =
725                                        new BufferedOutputStream
726                                                ( new FileOutputStream( tFile ) );
727
728                                for ( ; ; )
729                                        {
730                                        String line = in.readLine();
731                                        if ( line == null )
732                                                break;
733
734                                        out.write( line.getBytes() );
735                                        out.write( (byte)'\n' );
736                                        }
737
738                                in.close();
739                                out.flush();
740                                out.close();
741
742                                entry.setSize( tFile.length() );
743
744                                eFile = tFile;
745                                }
746                        }
747
748                String newName = null;
749
750                if ( this.rootPath != null )
751                        {
752                        if ( entry.getName().startsWith( this.rootPath ) )
753                                {
754                                newName =
755                                        entry.getName().substring
756                                                ( this.rootPath.length() + 1 );
757                                }
758                        }
759
760                if ( this.pathPrefix != null )
761                        {
762                        newName = (newName == null)
763                                ? this.pathPrefix + "/" + entry.getName()
764                                : this.pathPrefix + "/" + newName;
765                        }
766
767                if ( newName != null )
768                        {
769                        entry.setName( newName );
770                        }
771
772                this.tarOut.putNextEntry( entry );
773
774                if ( entry.isDirectory() )
775                        {
776                        if ( recurse )
777                                {
778                                TarEntry[] list = entry.getDirectoryEntries();
779
780                                for ( int i = 0 ; i < list.length ; ++i )
781                                        {
782                                        TarEntry dirEntry = list[i];
783
784                                        if ( unixArchiveFormat )
785                                                dirEntry.setUnixTarFormat();
786
787                                        this.writeEntry( dirEntry, recurse );
788                                        }
789                                }
790                        }
791                else
792                        {
793                        FileInputStream in =
794                                new FileInputStream( eFile );
795
796                        byte[] eBuf = new byte[ 32 * 1024 ];
797                        for ( ; ; )
798                                {
799                                int numRead = in.read( eBuf, 0, eBuf.length );
800
801                                if ( numRead == -1 )
802                                        break;
803
804                                this.tarOut.write( eBuf, 0, numRead );
805                                }
806
807                        in.close();
808
809                        if ( tFile != null )
810                                {
811                                tFile.delete();
812                                }
813                        
814                        this.tarOut.closeEntry();
815                        }
816                }
817
818        }
819
820