001/** 002 * 003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved. 004 * 005 * This library is free software; you can redistribute it and/or 006 * modify it under the terms of the GNU Lesser General Public 007 * License as published by the Free Software Foundation; either 008 * version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 * Lesser General Public License for more details. 014 * 015 * You should have received a copy of the GNU Lesser General Public 016 * License along with this library. If not, see <http://www.gnu.org/licenses/>. 017 * 018 **/ 019package lucee.runtime.tag; 020 021import java.io.ByteArrayInputStream; 022import java.io.ByteArrayOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.util.ArrayList; 026import java.util.Enumeration; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Set; 030import java.util.zip.ZipEntry; 031import java.util.zip.ZipException; 032import java.util.zip.ZipFile; 033import java.util.zip.ZipInputStream; 034import java.util.zip.ZipOutputStream; 035 036import lucee.commons.io.IOUtil; 037import lucee.commons.io.compress.ZipUtil; 038import lucee.commons.io.res.Resource; 039import lucee.commons.io.res.filter.DirectoryResourceFilter; 040import lucee.commons.io.res.filter.FileResourceFilter; 041import lucee.commons.io.res.filter.OrResourceFilter; 042import lucee.commons.io.res.filter.ResourceFilter; 043import lucee.commons.io.res.util.FileWrapper; 044import lucee.commons.io.res.util.ResourceUtil; 045import lucee.commons.io.res.util.UDFFilter; 046import lucee.commons.io.res.util.WildcardPatternFilter; 047import lucee.commons.lang.StringUtil; 048import lucee.runtime.PageContextImpl; 049import lucee.runtime.exp.ApplicationException; 050import lucee.runtime.exp.ExpressionException; 051import lucee.runtime.exp.PageException; 052import lucee.runtime.ext.tag.BodyTagImpl; 053import lucee.runtime.op.Caster; 054import lucee.runtime.op.Decision; 055import lucee.runtime.type.QueryImpl; 056import lucee.runtime.type.UDF; 057import lucee.runtime.type.dt.DateTimeImpl; 058 059public final class Zip extends BodyTagImpl { 060 061 private String action="zip"; 062 private String charset; 063 private Resource destination; 064 private String entryPath; 065 private Resource file; 066 private ResourceFilter filter; 067 private String pattern; 068 private String patternDelimiters; 069 private String name; 070 private boolean overwrite; 071 private String prefix; 072 private boolean recurse=true; 073 private boolean showDirectory; 074 private boolean storePath=true; 075 private String variable; 076 private List<ZipParamAbstr> params; 077 private Set<String> alreadyUsed; 078 private Resource source; 079 private static int id=0; 080 081 082 @Override 083 public void release() { 084 super.release(); 085 action="zip"; 086 charset=null; 087 destination=null; 088 entryPath=null; 089 file=null; 090 filter=null; 091 name=null; 092 overwrite=false; 093 prefix=null; 094 recurse=true; 095 showDirectory=false; 096 source=null; 097 storePath=true; 098 variable=null; 099 pattern = null; 100 patternDelimiters = null; 101 102 if(params!=null)params.clear(); 103 if(alreadyUsed!=null)alreadyUsed.clear(); 104 } 105 106 107 108 /** 109 * @param action the action to set 110 */ 111 public void setAction(String action) { 112 this.action = action.trim().toLowerCase(); 113 } 114 115 116 117 /** 118 * @param charset the charset to set 119 */ 120 public void setCharset(String charset) { 121 this.charset = charset; 122 } 123 124 125 126 /** 127 * @param strDestination the destination to set 128 * @throws ExpressionException 129 * @throws PageException 130 */ 131 public void setDestination(String strDestination) throws PageException { 132 this.destination = ResourceUtil.toResourceExistingParent(pageContext, strDestination); 133 if(!destination.exists())destination.mkdirs(); 134 135 if(!destination.isDirectory()) 136 throw new ApplicationException("destination ["+strDestination+"] is not a existing directory"); 137 138 139 } 140 141 142 143 /** 144 * @param entryPath the entryPath to set 145 */ 146 public void setEntrypath(String entryPath) { 147 if(StringUtil.isEmpty(entryPath,true)) return; 148 149 entryPath=entryPath.trim(); 150 entryPath=entryPath.replace('\\','/'); 151 152 if(StringUtil.startsWith(entryPath,'/'))entryPath=entryPath.substring(1); 153 if(StringUtil.endsWith(entryPath,'/'))entryPath=entryPath.substring(0,entryPath.length()-1); 154 this.entryPath = entryPath; 155 } 156 157 158 159 /** 160 * @param file the file to set 161 * @throws ExpressionException 162 */ 163 public void setFile(String file) { 164 this.file = ResourceUtil.toResourceNotExisting(pageContext, file); 165 } 166 167 /** 168 * @param filter the filter to set 169 */ 170 public void setFilter(Object filter) throws PageException { 171 172 if (filter instanceof UDF) 173 this.setFilter((UDF)filter); 174 else if (filter instanceof String) 175 this.setFilter((String)filter); 176 } 177 178 public void setFilter(UDF filter) throws PageException { 179 180 this.filter = UDFFilter.createResourceAndResourceNameFilter(filter); 181 } 182 183 public void setFilter(String pattern) { 184 185 this.pattern = pattern; 186 } 187 188 public void setFilterdelimiters(String patternDelimiters) { 189 190 this.patternDelimiters = patternDelimiters; 191 } 192 193 194 /** 195 * @param name the name to set 196 */ 197 public void setName(String name) { 198 this.name = name; 199 } 200 201 202 203 /** 204 * @param overwrite the overwrite to set 205 */ 206 public void setOverwrite(boolean overwrite) { 207 this.overwrite = overwrite; 208 } 209 210 211 212 /** 213 * @param prefix the prefix to set 214 */ 215 public void setPrefix(String prefix) { 216 this.prefix = prefix; 217 } 218 219 220 221 /** 222 * @param recurse the recurse to set 223 */ 224 public void setRecurse(boolean recurse) { 225 this.recurse = recurse; 226 } 227 228 229 230 /** 231 * @param showDirectory the showDirectory to set 232 */ 233 public void setShowdirectory(boolean showDirectory) { 234 this.showDirectory = showDirectory; 235 } 236 237 238 239 /** 240 * @param strSource the source to set 241 * @throws PageException 242 */ 243 public void setSource(String strSource) throws PageException { 244 source = ResourceUtil.toResourceExisting(pageContext, strSource); 245 } 246 247 248 249 /** 250 * @param storePath the storePath to set 251 */ 252 public void setStorepath(boolean storePath) { 253 this.storePath = storePath; 254 } 255 256 257 258 /** 259 * @param variable the variable to set 260 */ 261 public void setVariable(String variable) { 262 this.variable = variable; 263 } 264 265 266 267 @Override 268 public int doStartTag() throws PageException { 269 return EVAL_BODY_INCLUDE; 270 } 271 272 private void actionDelete() throws ApplicationException, IOException { 273 required("file",file,true); 274 275 Resource existing = pageContext.getConfig().getTempDirectory().getRealResource(getTempName()); 276 IOUtil.copy(file, existing); 277 278 ZipInputStream zis = null; 279 ZipOutputStream zos = null; 280 try { 281 zis = new ZipInputStream( IOUtil.toBufferedInputStream(existing.getInputStream()) ); 282 zos = new ZipOutputStream(IOUtil.toBufferedOutputStream(file.getOutputStream())); 283 284 ZipEntry entry; 285 String path,name; 286 int index; 287 boolean accept; 288 289 if(filter==null && recurse && entryPath==null) 290 throw new ApplicationException("define at least one restriction, can't delete all the entries from a zip file"); 291 292 while ( ( entry = zis.getNextEntry()) != null ) { 293 accept=false; 294 path = entry.getName().replace('\\', '/'); 295 index=path.lastIndexOf('/'); 296 297 if(!recurse && index>0) accept=true; 298 299 //dir=index==-1?"":path.substring(0,index); 300 name=path.substring(index+1); 301 302 if(filter!=null && !filter.accept(file.getRealResource(name))) accept=true; 303 if(!entryPathMatch(path)) accept=true; 304 305 if(!accept) continue; 306 307 add(zos, entry, zis, false); 308 zis.closeEntry(); 309 } 310 } 311 finally { 312 IOUtil.closeEL(zis); 313 IOUtil.closeEL(zos); 314 existing.delete(); 315 } 316 317 } 318 319 320 321 private void actionList() throws PageException, IOException { 322 required("file",file,true); 323 required("name",name); 324 325 lucee.runtime.type.Query query=new QueryImpl( 326 new String[]{"name","size","type","dateLastModified","directory","crc","compressedSize","comment"}, 327 0,"query"); 328 pageContext.setVariable(name, query); 329 330 ZipFile zip = getZip(file); 331 Enumeration entries = zip.entries(); 332 333 try { 334 String path,name,dir; 335 ZipEntry ze; 336 int row=0,index; 337 while(entries.hasMoreElements()) { 338 ze = (ZipEntry)entries.nextElement(); 339 if(!showDirectory && ze.isDirectory()) continue; 340 341 path = ze.getName().replace('\\', '/'); 342 index=path.lastIndexOf('/'); 343 if(!recurse && index>0) continue; 344 345 dir=index==-1?"":path.substring(0,index); 346 name=path.substring(index+1); 347 348 if(filter!=null && !filter.accept( file.getRealResource(name) )) continue; 349 350 if(!entryPathMatch(dir)) continue; 351 //if(entryPath!=null && !(dir.equalsIgnoreCase(entryPath) || StringUtil.startsWithIgnoreCase(dir,entryPath+"/"))) ;///continue; 352 353 row++; 354 query.addRow(); 355 query.setAt("name", row, path); 356 query.setAt("size", row, Caster.toDouble(ze.getSize())); 357 query.setAt("type", row, ze.isDirectory()?"Directory":"File"); 358 query.setAt("dateLastModified", row, new DateTimeImpl(pageContext,ze.getTime(),false)); 359 query.setAt("crc", row, Caster.toDouble(ze.getCrc())); 360 query.setAt("compressedSize", row, Caster.toDouble(ze.getCompressedSize())); 361 query.setAt("comment", row, ze.getComment()); 362 query.setAt("directory", row, dir); 363 //zis.closeEntry(); 364 } 365 } 366 finally { 367 IOUtil.closeEL(zip); 368 } 369 } 370 371 private boolean entryPathMatch(String dir) { 372 if(entryPath==null) return true; 373 374 return dir.equalsIgnoreCase(entryPath) || StringUtil.startsWithIgnoreCase(dir,entryPath+"/"); 375 } 376 377 378 379 private void actionRead(boolean binary) throws ZipException, IOException, PageException { 380 required("file",file,true); 381 required("variable",variable); 382 required("entrypath",variable); 383 ZipFile zip = getZip(file); 384 385 try { 386 ZipEntry ze = zip.getEntry(entryPath); 387 if(ze==null)ze = zip.getEntry(entryPath+"/"); 388 if(ze==null) throw new ApplicationException("zip file ["+file+"] has no entry with name ["+entryPath+"]"); 389 390 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 391 392 InputStream is = zip.getInputStream(ze); 393 IOUtil.copy(is, baos,true,false); 394 zip.close(); 395 396 if(binary) 397 pageContext.setVariable(variable, baos.toByteArray()); 398 else { 399 if(charset==null)charset=((PageContextImpl)pageContext).getResourceCharset().name(); 400 pageContext.setVariable(variable, new String(baos.toByteArray(),charset)); 401 } 402 } 403 finally { 404 IOUtil.closeEL(zip); 405 } 406 407 } 408 409 410 411 private void actionUnzip() throws ApplicationException, IOException { 412 required("file",file,true); 413 required("destination",destination,false); 414 415 ZipInputStream zis=null; 416 String path; 417 Resource target,parent; 418 int index; 419 try { 420 421 zis = new ZipInputStream( IOUtil.toBufferedInputStream(file.getInputStream()) ) ; 422 ZipEntry entry; 423 while ( ( entry = zis.getNextEntry()) != null ) { 424 425 path = entry.getName().replace('\\', '/'); 426 index=path.lastIndexOf('/'); 427 428 // recurse 429 if(!recurse && index!=-1) { 430 zis.closeEntry(); 431 continue; 432 } 433 434 target=destination.getRealResource(entry.getName()); 435 436 // filter 437 if(filter!=null && !filter.accept(target)) { 438 zis.closeEntry(); 439 continue; 440 } 441 442 // entrypath 443 if(!entryPathMatch(path)) { 444 zis.closeEntry(); 445 continue; 446 } 447 if(!storePath) target=destination.getRealResource(target.getName()); 448 if(entry.isDirectory()) { 449 target.mkdirs(); 450 } 451 else { 452 if(storePath){ 453 parent=target.getParentResource(); 454 if(!parent.exists())parent.mkdirs(); 455 } 456 if(overwrite || !target.exists())IOUtil.copy(zis,target,false); 457 } 458 target.setLastModified(entry.getTime()); 459 zis.closeEntry() ; 460 } 461 } 462 finally { 463 IOUtil.closeEL(zis); 464 } 465 } 466 467 468 469 private void actionZip() throws PageException, IOException { 470 required("file",file,false); 471 Resource dir = file.getParentResource(); 472 473 if(!dir.exists()) { 474 throw new ApplicationException("directory ["+dir.toString()+"] doesn't exist"); 475 } 476 477 478 479 if((params==null || params.isEmpty()) && source!=null) { 480 setParam(new ZipParamSource(source,entryPath,filter,prefix,recurse)); 481 } 482 483 if((params==null || params.isEmpty())) { 484 throw new ApplicationException("No source/content specified"); 485 } 486 487 488 489 490 ZipOutputStream zos=null; 491 Resource existing=null; 492 try { 493 494 // existing 495 if(!overwrite && file.exists()) { 496 existing = pageContext.getConfig().getTempDirectory().getRealResource(getTempName()); 497 IOUtil.copy(file, existing); 498 } 499 500 zos = new ZipOutputStream(IOUtil.toBufferedOutputStream(file.getOutputStream())); 501 502 Object[] arr = params.toArray(); 503 for(int i=arr.length-1;i>=0;i--) { 504 if(arr[i] instanceof ZipParamSource) 505 actionZip(zos,(ZipParamSource)arr[i]); 506 else if(arr[i] instanceof ZipParamContent) 507 actionZip(zos,(ZipParamContent)arr[i]); 508 } 509 510 if(existing!=null) { 511 ZipInputStream zis = new ZipInputStream( IOUtil.toBufferedInputStream(existing.getInputStream()) ); 512 try { 513 ZipEntry entry; 514 while ( ( entry = zis.getNextEntry()) != null ) { 515 add(zos, entry, zis, false); 516 zis.closeEntry(); 517 } 518 } 519 finally { 520 zis.close(); 521 } 522 } 523 } 524 finally { 525 ZipUtil.close(zos); 526 if(existing!=null)existing.delete(); 527 528 } 529 530 } 531 532 533 534 private String getTempName() { 535 return "tmp-"+(id++)+".zip"; 536 } 537 538 539 540 private void actionZip(ZipOutputStream zos, ZipParamContent zpc) throws PageException, IOException { 541 Object content = zpc.getContent(); 542 if(Decision.isBinary(content)) { 543 add(zos, new ByteArrayInputStream(Caster.toBinary(content)), zpc.getEntryPath(), System.currentTimeMillis(), true); 544 545 } 546 else { 547 String charset=zpc.getCharset(); 548 if(StringUtil.isEmpty(charset))charset=((PageContextImpl)pageContext).getResourceCharset().name(); 549 add(zos, new ByteArrayInputStream(content.toString().getBytes(charset)), zpc.getEntryPath(), System.currentTimeMillis(), true); 550 } 551 } 552 553 554 555 private void actionZip(ZipOutputStream zos, ZipParamSource zps) throws IOException { 556 // prefix 557 String p=zps.getPrefix(); 558 if(StringUtil.isEmpty(p)) 559 p=this.prefix; 560 561 if(!StringUtil.isEmpty(p)){ 562 if(!StringUtil.endsWith(p, '/'))p+="/"; 563 } 564 else 565 p=""; 566 567 568 569 if(zps.getSource().isFile()){ 570 571 String ep = zps.getEntryPath(); 572 if(ep==null)ep=zps.getSource().getName(); 573 if(!StringUtil.isEmpty(p)) ep=p+ep; 574 575 add(zos,zps.getSource().getInputStream(),ep,zps.getSource().lastModified(),true); 576 } 577 else { 578 579 580 581 // filter 582 ResourceFilter f = zps.getFilter(); 583 if(f==null)f=this.filter; 584 if(zps.isRecurse()) { 585 if(f!=null)f=new OrResourceFilter(new ResourceFilter[]{DirectoryResourceFilter.FILTER,f}); 586 } 587 else { 588 if(f==null)f=FileResourceFilter.FILTER; 589 } 590 591 addDir(zos,zps.getSource(),p,f); 592 } 593 } 594 595 596 597 private void addDir(ZipOutputStream zos, Resource dir, String parent, ResourceFilter filter) throws IOException { 598 Resource[] children = filter==null?dir.listResources():dir.listResources(filter); 599 600 for(int i=0;i<children.length;i++) { 601 602 603 if(children[i].isDirectory()) addDir(zos, children[i], parent+children[i].getName()+"/",filter); 604 else { 605 add(zos, children[i].getInputStream(), parent+children[i].getName(), children[i].lastModified(), true); 606 } 607 } 608 } 609 610 611 612 private void add(ZipOutputStream zos, InputStream is, String path, long lastMod, boolean closeInput) throws IOException { 613 ZipEntry ze=new ZipEntry(path); 614 ze.setTime(lastMod); 615 add(zos, ze, is, closeInput); 616 } 617 618 private void add(ZipOutputStream zos, ZipEntry entry,InputStream is, boolean closeInput) throws IOException { 619 if(alreadyUsed==null)alreadyUsed=new HashSet<String>(); 620 else if(alreadyUsed.contains(entry.getName())) return; 621 zos.putNextEntry(entry); 622 try { 623 IOUtil.copy(is,zos,closeInput,false); 624 } 625 finally { 626 zos.closeEntry(); 627 } 628 alreadyUsed.add(entry.getName()); 629 } 630 631 632 633 @Override 634 public void doInitBody() { 635 636 } 637 638 @Override 639 public int doAfterBody() { 640 return SKIP_BODY; 641 } 642 643 @Override 644 public int doEndTag() throws PageException {//print.out("doEndTag"+doCaching+"-"+body); 645 try { 646 647 if (this.filter == null && !StringUtil.isEmpty(this.pattern)) 648 this.filter = new WildcardPatternFilter(pattern, patternDelimiters); 649 650 if(action.equals("delete")) actionDelete(); 651 else if(action.equals("list")) actionList(); 652 else if(action.equals("read")) actionRead(false); 653 else if(action.equals("readbinary")) actionRead(true); 654 else if(action.equals("unzip")) actionUnzip(); 655 else if(action.equals("zip")) actionZip(); 656 else 657 throw new ApplicationException("invalid value ["+action+"] for attribute action","values for attribute action are:info,move,rename,copy,delete,read,readbinary,write,append,upload"); 658 } 659 catch(IOException ioe) { 660 throw Caster.toPageException(ioe); 661 } 662 663 return EVAL_PAGE; 664 } 665 666 /** 667 * sets if tag has a body or not 668 * @param hasBody 669 */ 670 public void hasBody(boolean hasBody) { 671 ///this.hasBody=hasBody; 672 } 673 674 private ZipFile getZip(Resource file) throws ZipException, IOException { 675 return new ZipFile(FileWrapper.toFile(file)); 676 } 677 678 /** 679 * throw a error if the value is empty (null) 680 * @param attributeName 681 * @param attributValue 682 * @throws ApplicationException 683 */ 684 private void required(String attributeName, String attributValue) throws ApplicationException { 685 if(StringUtil.isEmpty(attributValue)) 686 throw new ApplicationException( 687 "invalid attribute constellation for the tag zip", 688 "attribute ["+attributeName+"] is required, if action is ["+action+"]"); 689 } 690 691 /** 692 * throw a error if the value is empty (null) 693 * @param attributeName 694 * @param attributValue 695 * @throws ApplicationException 696 */ 697 private void required(String attributeName, Resource attributValue, boolean exists) throws ApplicationException { 698 if(attributValue==null) 699 throw new ApplicationException( 700 "invalid attribute constellation for the tag zip", 701 "attribute ["+attributeName+"] is required, if action is ["+action+"]"); 702 703 if(exists && !attributValue.exists()) 704 throw new ApplicationException(attributeName+" resource ["+attributValue+"] doesn't exist"); 705 else if(exists && !attributValue.canRead()) 706 throw new ApplicationException("no access to "+attributeName+" resource ["+attributValue+"]"); 707 708 709 } 710 711 712 713 714 public void setParam(ZipParamAbstr param) { 715 if(params==null) { 716 params=new ArrayList<ZipParamAbstr>(); 717 alreadyUsed=new HashSet<String>(); 718 } 719 params.add(param); 720 } 721 722 723 724 /** 725 * @return the source 726 */ 727 public Resource getSource() { 728 return source; 729 } 730 731 732}