001/*
002 *  $Source: /cvsroot2/open/projects/WebARTS/ca/bc/webarts/tools/Zipper.java,v $
003 *  Zipper.java - A zip compression tool.
004 *
005 *  $Revision: 563 $
006 *  $Date: 2012-11-03 19:28:37 -0700 (Sat, 03 Nov 2012) $
007 *  $Locker:  $
008 *
009 *
010 *  Written by Tom Gutwin - WebARTS Design.
011 *  Copyright (C) 2001 WebARTS Design, North Vancouver Canada
012 *  http://www.webarts.bc.ca
013 *
014 *  This program is free software; you can redistribute it and/or modify
015 *  it under the terms of the GNU General Public License as published by
016 *  the Free Software Foundation; either version 2 of the License, or
017 *  (at your option) any later version.
018 *
019 *  This program is distributed in the hope that it will be useful,
020 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
021 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
022 *  GNU General Public License for more details.
023 *
024 *  You should have received a copy of the GNU General Public License
025 *  along with this program; if not, write to the Free Software
026 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
027 */
028package ca.bc.webarts.tools;
029
030import ca.bc.webarts.widgets.Util;
031
032import java.io.File;
033import java.io.FileNotFoundException;
034import java.io.FileInputStream;
035import java.io.FileOutputStream;
036import java.io.IOException;
037import java.io.OutputStream;
038import java.io.PrintStream;
039
040import java.util.Date;
041import java.util.Vector;
042
043import java.util.zip.DataFormatException;
044import java.util.zip.DeflaterOutputStream;
045import java.util.zip.ZipEntry;
046import java.util.zip.ZipException;
047import java.util.zip.ZipOutputStream;
048
049/**
050 *  This class creates a zip file for specified directories or
051 *  files. The max size for the zip file can be specified.<P>Usage details:<pre>
052 *  Zipper <destination file with .zip extension> <source directory(s)/file(s)>
053 *  </pre>
054 *
055 * @author     Tom Gutwin tgutwin@webarts.bc.ca
056 * @created    May 19, 2002
057 */
058public class Zipper
059{
060
061  /**  Output stream for destination zip file. * */
062  protected ZipOutputStream zos_ = null;
063  protected File zipfile_ = null;
064  /** the Out Stream for any info **/
065  protected PrintStream printOutStream_ = System.out;
066  protected OutputStream infoOutStream_ = System.out;
067  protected boolean quiet_ = true;
068  protected boolean reallyQuiet_ = false;
069  private Vector zipWatchers_ = new Vector();
070  private String oldName_ = "";
071  int zipfilenameCounter_ = 0;
072  int compressionLevel_ = 9;
073  long maxZipFileSize_ = 630L * 1000L * 1024L;  //650 MB (630 to be safe for CDRW overhead issues)
074  String [] defaultExcludes_ = {"~","SWAPPER.DAT","#","OldHPFS","OldFatDisk","tmp","Trash"};
075  Vector excludes_ = new Vector();
076  long startTime_ = 0L;
077
078
079  /**  Constructor for the Zipper object */
080  public Zipper()
081  {
082    for (int i=0;i < defaultExcludes_.length;i++)
083      excludes_.add(defaultExcludes_[i]);
084  }
085
086
087  /**
088   *  Constructor for the Zipper object
089   *
090   * @param  src  An array of files to include in the zip.
091   * @param  des  Description of the Parameter
092   */
093  Zipper(File[] src, File des)
094  {
095    for (int i=0;i < defaultExcludes_.length;i++)
096      excludes_.add(defaultExcludes_[i]);
097    zipFiles(src, des);
098  }
099
100
101  /**
102   *  Constructor for the Zipper object
103   *
104   * @param  src  Description of the Parameter
105   * @param  des  Description of the Parameter
106   */
107  Zipper(File src, File des)
108  {
109    File[] srcArray = {src};
110    for (int i=0;i < defaultExcludes_.length;i++)
111      excludes_.add(defaultExcludes_[i]);
112    zipFiles(srcArray, des);
113  }
114
115
116  /**
117   *  Main Entry point to run this class as an application usage: <code>
118   * Zipper <destination file with .zip extension> <source directorys / files>
119   *</code>
120   *
121   * @param  args  1st arg is the destination file, 2nd arg is the source files
122   */
123  public static void main(String args[])
124  {
125    if (args.length >= 2)
126    {
127      try
128      {
129        File des = new File(args[0]);
130        if (!args[0].toLowerCase().substring(args[0].length() - 4,
131            args[0].length()).equalsIgnoreCase(".zip"))
132        {
133          throw new ArrayIndexOutOfBoundsException();
134        }
135
136        File[] src = new File[args.length - 1];
137        for (int i = 1; i < args.length; i++)
138        {
139          src[i - 1] = new File(args[i]);
140        }
141
142        new Zipper(src, des);
143      }
144      catch (ArrayIndexOutOfBoundsException aioobe)
145      {
146        System.out.println("Usage: java Zipper DestZipfile.zip SourceFile " +
147                           "SourceFile2 ...");
148        System.out.println(" OR");
149        System.out.println("Usage: java Zipper SourceFile");
150        System.out.println("      ...zips the SourceFile to SourceFile.zip");
151      }
152    }
153    else if (args.length == 1)
154    {
155      try
156      {
157        File src = new File(args[0]);
158        File des = new File(System.getProperty("user.dir") +
159            File.separator + args[0] + ".zip");
160        new Zipper(src, des);
161      }
162      catch (ArrayIndexOutOfBoundsException aioobe)
163      {
164        System.out.println("Usage: java Zipper DestZipfile.zip " +
165                           "SourceFile SourceFile2 ...");
166        System.out.println(" OR");
167        System.out.println("Usage: java Zipper SourceFile");
168        System.out.println("      ...zips the SourceFile to SourceFile.zip");
169      }
170    }
171    else
172    {
173      System.out.println("Usage: java Zipper DestZipfile.zip SourceFile " +
174                           "SourceFile2 ...");
175      System.out.println(" OR");
176      System.out.println("Usage: java Zipper SourceFile");
177      System.out.println("      ...zips the SourceFile to SourceFile.zip");
178    }
179
180  }
181
182
183  /**
184   * Gets the quiet_ field. Which specifies how much user feedback is provided
185   **/
186  public boolean getQuiet_()
187  {
188    return quiet_;
189  }
190
191
192  /**
193   * Sets the quiet_ field. Which specifies how much user feedback is provided
194   **/
195  public void setQuiet_(boolean quiet_)
196  {
197    this.quiet_ = quiet_;
198  }
199
200
201  /**
202   * Gets the infoOutStream_ field,Which specifies where to send feedback.
203   **/
204  public OutputStream getInfoOutStream()
205  {
206    return infoOutStream_;
207  }
208
209
210  /**
211   * Sets the infoOutStream_ field, Which specifies where to send feedback.
212   **/
213  public void setInfoOutStream(OutputStream ios)
214  {
215    infoOutStream_ = ios;
216    printOutStream_ = new PrintStream(ios);
217  }
218
219
220  /**
221   * Adds a String to the excludes Vecotr that gets checked to exclude files.
222   **/
223  public void addToExcludes(String ex)
224  {
225    if (ex!= null && !ex.equals(""))
226      excludes_.add(ex);
227  }
228
229
230  /**
231   *  Sets the maxZipFileSize_ attribute of the Zipper object
232   *
233   * @param  maxZipFileSize_  The new maxZipFileSize_ value
234   */
235  public void setMaxZipFileSize_(long maxZipFileSize)
236  {
237    this.maxZipFileSize_ = maxZipFileSize *1024L *1000L;
238  }
239
240
241  /**
242   *  Gets the maxZipFileSize_ attribute of the Zipper object
243   *
244   * @return    The maxZipFileSize_ value
245   */
246  public long getMaxZipFileSize_()
247  {
248    return maxZipFileSize_;
249  }
250
251
252  /**
253   *  Sets the compressionLevel_ attribute of the Zipper object
254   *
255   * @param  compressionLevel_  The new compressionLevel_ value
256   */
257  public void setCompressionLevel_(int compressionLevel_)
258  {
259    this.compressionLevel_ = compressionLevel_;
260  }
261
262
263  protected boolean excludedFile(File fileToCheck)
264  {
265    boolean retVal = false;
266
267    String filename = fileToCheck.getAbsolutePath();
268    for (int i=0; i< excludes_.size(); i++)
269      if (filename.endsWith((String)excludes_.get(i)))
270      {
271        retVal = true;
272        break;
273      }
274
275    return retVal;
276  }
277
278
279   protected boolean excludedFile(String fileToCheck)
280  {
281    boolean retVal = false;
282
283    for (int i=0; i< excludes_.size(); i++)
284      if (fileToCheck.endsWith((String)excludes_.get(i)))
285      {
286        retVal = true;
287        break;
288      }
289
290    return retVal;
291  }
292
293
294  /**
295   *  Gets the compressionLevel_ attribute of the Zipper object
296   *
297   * @return    The compressionLevel_ value
298   */
299  public int getCompressionLevel_()
300  {
301    return compressionLevel_;
302  }
303
304
305  protected void registerAsZipWatcher(ZipWatcher watcherClass)
306  {
307    zipWatchers_.add(watcherClass);
308  }
309
310
311  protected void notifyZipWatchers(String filenameJustCompleted, boolean done)
312  {
313    ZipWatcher currentWatcher = null;
314    for (int i=0; i< zipWatchers_.size(); i++)
315    {
316      currentWatcher = (ZipWatcher)zipWatchers_.get(i);
317      currentWatcher.nextZipDone(filenameJustCompleted, done);
318    }
319  }
320
321
322
323  protected ZipOutputStream createZipOutputStream(FileOutputStream fos)
324  {
325    return new ZipOutputStream(fos);
326  }
327
328
329  /**
330   *  Inits the Zip Output stream with the latest increment counter.
331   *
332   * @param  destZipFile  Description of the Parameter
333   * @return              Description of the Return Value
334   */
335  protected boolean initZipOutputStream(File destZipFile)
336  {
337    boolean retVal = (destZipFile != null);
338    // avoid nullPEx
339
340    if (retVal)
341    {
342      String newName = destZipFile.getPath().trim();
343      oldName_ = newName;
344      int endOffset = 6;
345
346      if (zipfilenameCounter_ > 0)
347      {
348        if (zipfilenameCounter_ == 1)
349        {
350          endOffset = 4;
351        }
352        else if (zipfilenameCounter_ > 10)
353        {
354          endOffset = 7;
355        }
356        else if (zipfilenameCounter_ > 100)
357        {
358          endOffset = 8;
359        }
360        newName = newName.substring(0, newName.length() - endOffset) + "_" +
361            zipfilenameCounter_ + ".zip";
362        try
363        {
364          if (zos_ != null)
365          {
366            zos_.close();
367            notifyZipWatchers(zipfile_.getAbsolutePath(), false);
368          }
369        }
370        catch (java.io.IOException ioe)
371        {
372          printOutStream_.println("Input/Output problem attempting to close the " +
373              "zipfile " + destZipFile.getPath().trim() +
374              ". \nDetails :\n" + ioe.getMessage());
375          retVal = false;
376        }
377      }
378
379      // Attempt to open the stream
380      try
381      {
382        zipfilenameCounter_++;
383        zipfile_ = new File(newName);
384        zos_ = createZipOutputStream(new FileOutputStream(zipfile_));
385        zos_.setLevel(compressionLevel_);
386      }
387      catch (java.lang.NullPointerException npe)
388      {
389        printOutStream_.println("Internal Zipper Error. Details :\n" + npe.getMessage());
390        retVal = false;
391      }
392      catch (java.io.FileNotFoundException fnfe)
393      {
394        printOutStream_.println("Cannot init File:" + newName + ". \nDetails :\n" +
395            fnfe.getMessage());
396        retVal = false;
397      }
398    }
399
400    return retVal;
401  }
402
403
404  /**
405   *  Encapsulates the zipping of file(s) into a zip archive.
406   *
407   * @param  source       The files to include in the zipped file.
408   * @param  destination  The resultant zipped archive.
409   */
410  public void zipFiles(File[] source, File destination)
411  {
412    boolean initOkay = false;
413    try
414    {
415      startTime_ = (new Date()).getTime();
416      printOutStream_.println("\nInitializing the ZipOutput at:  "+
417        Util.createCurrentDateTimeStamp());
418      initOkay = initZipOutputStream(destination);
419      // String srcFileNames[] = {source.getName()};
420      //File arrFile[] = {source};
421      if (initOkay && source.length > 0)
422      {
423        for (int i = 0; i < source.length; i++)
424        {
425          if (!reallyQuiet_ && source[i]!= null)
426          {
427            printOutStream_.println("Recursing "+source[i].getAbsolutePath());
428            //printOutStream_.print(" ..." );
429            //printOutStream_.println(
430            //    Util.countFilesInDir(source[i].getAbsolutePath(),true)+" files.");
431          }
432          recursiveZip(source[i]);
433        }
434        zos_.close();
435        notifyZipWatchers(zipfile_.getAbsolutePath(), true);
436
437        long duration = ((new Date()).getTime() - startTime_)/1000L;
438        long minutes = duration / 60L;
439        long seconds = duration - (60L*minutes);
440        if (!reallyQuiet_ && !quiet_)
441        {
442          printOutStream_.println(destination.getName() +
443                             " file created successfully.");
444          printOutStream_.print("Time to complete: ");
445          printOutStream_.println(""+minutes+":"+seconds);
446        }
447      }
448      else
449      {
450        if (initOkay)
451          printOutStream_.println("There is no file in the specified source.");
452        else
453          printOutStream_.println("Could NOT Init Zip Output: " + destination);
454      }
455    }
456    catch (java.util.zip.ZipException ze)
457    {
458      printOutStream_.println("Exception occured in zipping file. Details :\n" + ze.getMessage());
459    }
460    catch (java.io.FileNotFoundException fnfe)
461    {
462      printOutStream_.println("File is missing. Details :" + fnfe.getMessage());
463    }
464    catch (java.io.IOException ioe)
465    {
466      printOutStream_.println("Input / Output problem. Details :\n" + ioe.getMessage());
467    }
468  }
469
470
471  /**
472   *  Compresses and saves the passed file parameter into a zip stream. This
473   *  is a convienience class that wraps the srcFilename into an array and
474   *  calls the
475   *  <pre>writeToZipStream(String[] srcFilenameArray, ZipOutputStream zos, boolean recurseSubDirs)</pre>
476   *  method.
477   *
478   * @param  srcFilename                The File to zip up - it can be a dir.
479   * @param  zos                        The ZipOutputStream to comptress into.
480   * @param  recurseSubDirs             Flags if this method should decend into subdirs.
481   * @exception  DataFormatException    Description of the Exception
482   * @exception  ZipException           Description of the Exception
483   * @exception  FileNotFoundException  Description of the Exception
484   * @exception  IOException            Description of the Exception
485   */
486  public void writeToZipStream(String srcFilename, ZipOutputStream zos, boolean recurseSubDirs)
487    throws DataFormatException, ZipException, FileNotFoundException, IOException
488  {
489    String [] tmpStr = {srcFilename};
490    writeToZipStream(tmpStr, zos, recurseSubDirs);
491  }
492
493
494  /**
495   *  Compresses and saves the passed files into a zip stream.
496   *
497   * @param  srcFilename                The Files to zip up - they can be a dir names.
498   * @param  zos                        The ZipOutputStream to comptress into.
499   * @param  recurseSubDirs             Flags if this method should decend into subdirs.
500   * @exception  DataFormatException    Description of the Exception
501   * @exception  ZipException           Description of the Exception
502   * @exception  FileNotFoundException  Description of the Exception
503   * @exception  IOException            Description of the Exception
504   */
505  public void writeToZipStream(String[] srcFilenameArray, ZipOutputStream zos, boolean recurseSubDirs)
506    throws DataFormatException, ZipException, FileNotFoundException, IOException
507  {
508    boolean debugOutput = true;
509    if (zos !=null)
510    {
511      String srcFilename = "";
512      for (int arrayCounter = 0;
513          arrayCounter< srcFilenameArray.length;
514          arrayCounter++)
515      {
516        srcFilename = srcFilenameArray[arrayCounter];
517        // This function write s the data in to Zipoutput Stream.
518        byte fileBytes[] = new byte[2048];
519        File fileToGet = new File(srcFilename);
520        // clean off the drive letter and '/'
521        String entry = srcFilename.toString().trim();
522        if (entry.startsWith(":"+File.separator,1))
523          entry = srcFilename.toString().substring(3);
524        else if (entry.startsWith(File.separator))
525          entry = srcFilename.toString().substring(1);
526
527        // Make sure dirs hava a trailing separator char so it is represented
528        // in the Zip correctly
529        if (fileToGet.isDirectory() && !entry.endsWith(File.separator))
530          entry += File.separator;
531
532        entry = Util.tokenReplace(entry,"\\","/");
533        ZipEntry ze = null;
534
535        if (!fileToGet.isDirectory())
536        {
537          int len = 0;
538          ze = new ZipEntry(entry);
539          zos.putNextEntry(ze);
540          FileInputStream fileInStream = new FileInputStream(srcFilename);
541          len = fileInStream.read(fileBytes);
542          if (debugOutput)
543            System.out.println("Zipping on "+srcFilename+File.separator+srcFilename);
544          while (len != -1)
545          {
546            if (debugOutput)
547              System.out.print(".");
548            zos.write(fileBytes);
549            len = fileInStream.read(fileBytes);
550          }
551          if (debugOutput)
552              System.out.println("");
553          zos.closeEntry();
554        }
555        else // it is a dir
556        {
557          ze = new ZipEntry(entry);
558          zos.putNextEntry(ze);
559          zos.closeEntry();
560          String srcFileNames[] = fileToGet.list();
561          File tmpFile = null;
562          for (int i=0; i< srcFileNames.length; i++)
563          {
564            tmpFile = new File(srcFilename+File.separator+srcFileNames[i]);
565            if (tmpFile != null && tmpFile.isDirectory() && recurseSubDirs)
566            {
567              if (debugOutput)
568                System.out.println("Recursing on "+srcFilename+File.separator+srcFileNames[i]);
569              writeToZipStream(srcFilename+File.separator+srcFileNames[i],
570                               zos, recurseSubDirs);
571            }
572            else if (tmpFile != null && !tmpFile.isDirectory())
573            {
574              if (debugOutput)
575                System.out.println("Recursing on "+srcFilename+File.separator+srcFileNames[i]);
576              writeToZipStream(srcFilename+File.separator+srcFileNames[i],
577                               zos, recurseSubDirs);
578            }
579          }
580        }
581      }
582    }
583  }
584
585
586  /**
587   *  Compresses and saves the passed file parameter to a zip file.
588   *
589   * @param  src                        The File to zip up - it can be a dir.
590   * @exception  DataFormatException    Description of the Exception
591   * @exception  ZipException           Description of the Exception
592   * @exception  FileNotFoundException  Description of the Exception
593   * @exception  IOException            Description of the Exception
594   */
595  public void writeToZip(File src)
596    throws DataFormatException, ZipException, FileNotFoundException, IOException
597  {
598    // This function write s the data in to Zipoutput Stream.
599    byte b[] = new byte[512];
600    // clean off the drive letter and '/'
601    String entry = src.toString().trim();
602    if (entry.startsWith(":"+File.separator,1))
603      entry = src.toString().substring(3);
604    else if (entry.startsWith(File.separator))
605      entry = src.toString().substring(1);
606
607    // Make sure dirs hava a trailing separator char so it is represented
608    // in the Zip correctly
609    if (src.isDirectory() && !entry.endsWith(File.separator))
610      entry += File.separator;
611
612    entry = Util.tokenReplace(entry,"\\","/");
613    ZipEntry ze = new ZipEntry(entry);
614    zos_.putNextEntry(ze);
615    if (!src.isDirectory())
616    {
617      int len = 0;
618      FileInputStream is = new FileInputStream(src);
619      while ((len = is.read(b)) != -1)
620      {
621        zos_.write(b, 0, len);
622      }
623      is = null;
624    }
625
626    // Provide some user feedback
627    if (!reallyQuiet_ && !quiet_)
628      if (!src.isDirectory())
629        printOutStream_.println(" Adding " + entry);
630      else
631        printOutStream_.println(" Added directory " + entry);
632    zos_.closeEntry();
633  }
634
635
636  /**
637   *  Wrapper method to accept a dir or individual file in the passed in File
638   *  object AND then calls the method that writes the passed file/dir to this
639   *  Zipper instances zip output stream. This is a recursive function. If the
640   *  passed File object is a file then it calls the function to write to zip
641   *  output stream. If it is a directory it gets the list of file objects in
642   *  the child directory and recurses on them.
643   *
644   * @param  fo  The File object to zip up (can be an actual file or dir).
645   */
646  public void recursiveZip(File fo)
647  {
648    try
649    {
650      if (!fo.isDirectory())
651      {
652        //printOutStream_.println("Adding "+fo.getName());
653        if (zipfile_.length() + fo.length() > maxZipFileSize_)
654        {
655          printOutStream_.print("Max Zip File Size Reached! ");
656          printOutStream_.println("Incrementing ZipFile Name.");
657          System.out.print("Max Zip File Size Reached! ");
658          System.out.println("Incrementing ZipFile Name.");
659          initZipOutputStream(zipfile_);
660        }
661        try
662        {
663          if (!excludedFile(fo))
664            writeToZip(fo);
665          else if(!reallyQuiet_)
666            printOutStream_.println("Excluding File:"+fo.getAbsolutePath());
667        }
668        catch (java.io.FileNotFoundException fnfe)
669        {
670          if(fnfe.getMessage().endsWith("(Too many open files)"))
671          {
672            //wait a bit and try again
673            ca.bc.webarts.widgets.Util.sleep(2000);
674            recursiveZip(fo);
675          }
676          else  if (!reallyQuiet_)
677            printOutStream_.println("File is missing (Zipping will continue):\n" +
678                             fnfe.getMessage());
679        }
680      }
681      else
682      { // it IS a Directory
683        try
684        {
685          if (excludedFile(fo))
686          {
687            if(!reallyQuiet_)
688            {
689              printOutStream_.println("Excluding Directory:"+fo.getAbsolutePath());
690            }
691          }
692          else
693          {
694            writeToZip(fo);
695            if (!reallyQuiet_)
696              printOutStream_.println("Recursing " + fo.getPath());
697            String srcFileNames[] = fo.list();
698            if (srcFileNames != null)
699            {
700              for (int i = 0; i < srcFileNames.length; i++)
701              {
702                if (!excludedFile(srcFileNames[i]))
703                {
704                  recursiveZip(new File(fo.getPath() + File.separator +
705                    srcFileNames[i]));
706                }
707              }
708            }
709          }
710        }
711        catch (java.io.FileNotFoundException fnfe)
712        {
713          if(!reallyQuiet_)
714            printOutStream_.println("Directory is missing (Zipping will continue):\n" +
715                              fnfe.getMessage());
716        }
717      }
718    }
719    catch (java.util.zip.DataFormatException dfe)
720    {
721      printOutStream_.println("Input data is not in proper format. Details :\n" +
722                         dfe.getMessage());
723    }
724    catch (java.util.zip.ZipException ze)
725    {
726      printOutStream_.println("Exception occured in zipping file. Details :\n" +
727                          ze.getMessage());
728    }
729    /*catch (java.io.FileNotFoundException fnfe)
730    {
731      printOutStream_.println("File is missing. Details :\n" + fnfe.getMessage());
732    }*/
733    catch (java.io.IOException ioe)
734    {
735      printOutStream_.println("Input / Output problem. \nDetails : " +
736                          ioe.getMessage());
737      if ("no space left on device".equals(
738           ioe.getMessage().trim().toLowerCase()))
739      {
740        try
741        {
742          // close up zip and stop
743          zos_.closeEntry();
744          zos_.close();
745          notifyZipWatchers(zipfile_.getAbsolutePath(), true);
746          long duration = ((new Date()).getTime() - startTime_)/1000L;
747          long minutes = duration / 60L;
748          long seconds = duration - (60L*minutes);
749          if (!reallyQuiet_ && !quiet_)
750          {
751            printOutStream_.println(zipfile_.getAbsolutePath() +
752                               " file created successfully.");
753            printOutStream_.print("Time to complete: ");
754            printOutStream_.println(""+minutes+":"+seconds);
755          }
756        }
757        catch (java.io.IOException ioe2)
758        {
759          printOutStream_.println("Cannot Close zip output file. Details: \n" +
760                          ioe2.getMessage());
761        }
762      }
763    }
764  }
765}