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