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.commons.io.res.type.s3; 020 021import java.io.ByteArrayInputStream; 022import java.io.ByteArrayOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.util.ArrayList; 027import java.util.Iterator; 028import java.util.LinkedHashSet; 029import java.util.Set; 030 031import lucee.commons.io.res.Resource; 032import lucee.commons.io.res.ResourceProvider; 033import lucee.commons.io.res.util.ResourceSupport; 034import lucee.commons.io.res.util.ResourceUtil; 035import lucee.commons.lang.StringUtil; 036import lucee.commons.net.http.httpclient3.HTTPEngine3Impl; 037import lucee.loader.util.Util; 038import lucee.runtime.exp.PageRuntimeException; 039import lucee.runtime.op.Caster; 040import lucee.runtime.type.Array; 041import lucee.runtime.type.util.ListUtil; 042 043import org.xml.sax.SAXException; 044 045public final class S3Resource extends ResourceSupport { 046 047 private static final long serialVersionUID = 2265457088552587701L; 048 049 private static final long FUTURE=50000000000000L; 050 051 private static final S3Info UNDEFINED=new Dummy("undefined",0,0,false,false,false); 052 private static final S3Info ROOT=new Dummy("root",0,0,true,false,true); 053 private static final S3Info LOCKED = new Dummy("locked",0,0,true,false,false); 054 private static final S3Info UNDEFINED_WITH_CHILDREN = new Dummy("undefined with children 1",0,0,true,false,true); 055 private static final S3Info UNDEFINED_WITH_CHILDREN2 = new Dummy("undefined with children 2",0,0,true,false,true); 056 057 058 private final S3ResourceProvider provider; 059 private final String bucketName; 060 private String objectName; 061 private final S3 s3; 062 long infoLastAccess=0; 063 private int storage=S3.STORAGE_UNKNOW; 064 private int acl=S3.ACL_PUBLIC_READ; 065 066 private boolean newPattern; 067 068 private S3Resource(S3 s3,int storage, S3ResourceProvider provider, String buckedName,String objectName, boolean newPattern) { 069 this.s3=s3; 070 this.provider=provider; 071 this.bucketName=buckedName; 072 this.objectName=objectName; 073 this.storage=storage; 074 this.newPattern=newPattern; 075 } 076 077 078 S3Resource(S3 s3,int storage, S3ResourceProvider provider, String path, boolean newPattern) { 079 this.s3=s3; 080 this.provider=provider; 081 this.newPattern=newPattern; 082 083 if(path.equals("/") || Util.isEmpty(path,true)) { 084 this.bucketName=null; 085 this.objectName=""; 086 } 087 else { 088 path=ResourceUtil.translatePath(path, true, false); 089 String[] arr = toStringArray( ListUtil.listToArrayRemoveEmpty(path,"/")); 090 bucketName=arr[0]; 091 for(int i=1;i<arr.length;i++) { 092 if(Util.isEmpty(objectName))objectName=arr[i]; 093 else objectName+="/"+arr[i]; 094 } 095 if(objectName==null)objectName=""; 096 } 097 this.storage=storage; 098 099 } 100 101 public static String[] toStringArray(Array array) { 102 String[] arr=new String[array.size()]; 103 for(int i=0;i<arr.length;i++) { 104 arr[i]=Caster.toString(array.get(i+1,""),""); 105 } 106 return arr; 107 } 108 109 110 public void createDirectory(boolean createParentWhenNotExists) throws IOException { 111 ResourceUtil.checkCreateDirectoryOK(this, createParentWhenNotExists); 112 try { 113 provider.lock(this); 114 if(isBucket()) { 115 s3.putBuckets(bucketName, acl,storage); 116 } 117 else s3.put(bucketName, objectName+"/", acl, HTTPEngine3Impl.getEmptyEntity("application")); 118 } 119 catch (IOException ioe) { 120 throw ioe; 121 } 122 catch (Exception e) { 123 e.printStackTrace(); 124 throw new IOException(e.getMessage()); 125 } 126 finally { 127 provider.unlock(this); 128 } 129 s3.releaseCache(getInnerPath()); 130 } 131 132 public void createFile(boolean createParentWhenNotExists) throws IOException { 133 ResourceUtil.checkCreateFileOK(this, createParentWhenNotExists); 134 if(isBucket()) throw new IOException("can't create file ["+getPath()+"], on this level (Bucket Level) you can only create directories"); 135 try { 136 provider.lock(this); 137 s3.put(bucketName, objectName, acl, HTTPEngine3Impl.getEmptyEntity("application")); 138 } 139 catch (Exception e) { 140 throw new IOException(e.getMessage()); 141 } 142 finally { 143 provider.unlock(this); 144 } 145 s3.releaseCache(getInnerPath()); 146 } 147 148 public boolean exists() { 149 150 return getInfo() 151 .exists(); 152 } 153 154 public InputStream getInputStream() throws IOException { 155 ResourceUtil.checkGetInputStreamOK(this); 156 provider.read(this); 157 try { 158 return Util.toBufferedInputStream(s3.getInputStream(bucketName, objectName)); 159 } 160 catch (Exception e) { 161 throw new IOException(e.getMessage()); 162 } 163 } 164 165 public int getMode() { 166 return 777; 167 } 168 169 @Override 170 public String getName() { 171 if(isRoot()) return ""; 172 if(isBucket()) return bucketName; 173 return objectName.substring(objectName.lastIndexOf('/')+1); 174 } 175 176 @Override 177 public boolean isAbsolute() { 178 return true; 179 } 180 181 182 @Override 183 public String getPath() { 184 return getPrefix().concat(getInnerPath()); 185 } 186 187 private String getPrefix() { 188 189 String aki=s3.getAccessKeyId(); 190 String sak=s3.getSecretAccessKey(); 191 192 StringBuilder sb=new StringBuilder(provider.getScheme()).append("://"); 193 194 if(!StringUtil.isEmpty(aki)){ 195 sb.append(aki); 196 if(!StringUtil.isEmpty(sak)){ 197 sb.append(":").append(sak); 198 if(storage!=S3.STORAGE_UNKNOW){ 199 sb.append(":").append(S3.toStringStorage(storage,"us")); 200 } 201 } 202 sb.append("@"); 203 } 204 if(!newPattern) 205 sb.append(s3.getHost()); 206 207 return sb.toString(); 208 } 209 210 211 @Override 212 public String getParent() { 213 if(isRoot()) return null; 214 return getPrefix().concat(getInnerParent()); 215 } 216 217 private String getInnerPath() { 218 if(isRoot()) return "/"; 219 return ResourceUtil.translatePath(bucketName+"/"+objectName, true, false); 220 } 221 222 private String getInnerParent() { 223 if(isRoot()) return null; 224 if(Util.isEmpty(objectName)) return "/"; 225 if(objectName.indexOf('/')==-1) return "/"+bucketName; 226 String tmp=objectName.substring(0,objectName.lastIndexOf('/')); 227 return ResourceUtil.translatePath(bucketName+"/"+tmp, true, false); 228 } 229 230 @Override 231 public Resource getParentResource() { 232 if(isRoot()) return null; 233 return new S3Resource(s3,isBucket()?S3.STORAGE_UNKNOW:storage,provider,getInnerParent(),newPattern);// MUST direkter machen 234 } 235 236 private boolean isRoot() { 237 return bucketName==null; 238 } 239 240 private boolean isBucket() { 241 return bucketName!=null && Util.isEmpty(objectName); 242 } 243 244 @Override 245 public String toString() { 246 return getPath(); 247 } 248 249 public OutputStream getOutputStream(boolean append) throws IOException { 250 251 ResourceUtil.checkGetOutputStreamOK(this); 252 //provider.lock(this); 253 254 try { 255 byte[] barr = null; 256 if(append){ 257 InputStream is=null; 258 OutputStream os=null; 259 try{ 260 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 261 os=baos; 262 Util.copy(is=getInputStream(), baos); 263 barr=baos.toByteArray(); 264 } 265 catch (Exception e) { 266 throw new PageRuntimeException(Caster.toPageException(e)); 267 } 268 finally{ 269 Util.closeEL(is); 270 Util.closeEL(os); 271 } 272 } 273 S3ResourceOutputStream os = new S3ResourceOutputStream(s3,bucketName,objectName,acl); 274 if(append && !(barr==null || barr.length==0)) 275 Util.copy(new ByteArrayInputStream(barr),os); 276 return os; 277 } 278 catch(IOException e) { 279 throw e; 280 } 281 catch (Exception e) { 282 throw new PageRuntimeException(Caster.toPageException(e)); 283 } 284 finally { 285 s3.releaseCache(getInnerPath()); 286 } 287 } 288 289 @Override 290 public Resource getRealResource(String relpath) { 291 relpath=ResourceUtil.merge(getInnerPath(), relpath); 292 if(relpath.startsWith("../"))return null; 293 return new S3Resource(s3,S3.STORAGE_UNKNOW,provider,relpath,newPattern); 294 } 295 296 @Override 297 public ResourceProvider getResourceProvider() { 298 return provider; 299 } 300 301 @Override 302 public boolean isDirectory() { 303 return getInfo().isDirectory(); 304 } 305 306 @Override 307 public boolean isFile() { 308 return getInfo().isFile(); 309 } 310 311 public boolean isReadable() { 312 return exists(); 313 } 314 315 public boolean isWriteable() { 316 return exists(); 317 } 318 319 @Override 320 public long lastModified() { 321 return getInfo().getLastModified(); 322 } 323 324 private S3Info getInfo() { 325 S3Info info = s3.getInfo(getInnerPath()); 326 327 if(info==null) {// || System.currentTimeMillis()>infoLastAccess 328 if(isRoot()) { 329 try { 330 s3.listBuckets(); 331 info=ROOT; 332 } 333 catch (Exception e) { 334 info=UNDEFINED; 335 } 336 infoLastAccess=FUTURE; 337 } 338 else { 339 try { 340 provider.read(this); 341 } catch (IOException e) { 342 return LOCKED; 343 } 344 try { 345 if(isBucket()) { 346 Bucket[] buckets = s3.listBuckets(); 347 String name=getName(); 348 for(int i=0;i<buckets.length;i++) { 349 if(buckets[i].getName().equals(name)) { 350 info=buckets[i]; 351 infoLastAccess=System.currentTimeMillis()+provider.getCache(); 352 break; 353 } 354 } 355 } 356 else { 357 try { 358 // first check if the bucket exists 359 // TODO not happy about this step 360 Bucket[] buckets = s3.listBuckets(); 361 boolean bucketExists=false; 362 for(int i=0;i<buckets.length;i++) { 363 if(buckets[i].getName().equals(bucketName)) { 364 bucketExists=true; 365 break; 366 } 367 } 368 369 if(bucketExists){ 370 String path = objectName; 371 Content[] contents = s3.listContents(bucketName, path); 372 if(contents.length>0) { 373 boolean has=false; 374 for(int i=0;i<contents.length;i++) { 375 if(ResourceUtil.translatePath(contents[i].getKey(),false,false).equals(path)) { 376 has=true; 377 info=contents[i]; 378 infoLastAccess=System.currentTimeMillis()+provider.getCache(); 379 break; 380 } 381 } 382 if(!has){ 383 for(int i=0;i<contents.length;i++) { 384 if(ResourceUtil.translatePath(contents[i].getKey(),false,false).startsWith(path)) { 385 info=UNDEFINED_WITH_CHILDREN; 386 infoLastAccess=System.currentTimeMillis()+provider.getCache(); 387 break; 388 } 389 } 390 } 391 } 392 } 393 } 394 catch(SAXException e) { 395 396 } 397 } 398 399 if(info==null){ 400 info=UNDEFINED; 401 infoLastAccess=System.currentTimeMillis()+provider.getCache(); 402 } 403 } 404 catch(Exception t) { 405 return UNDEFINED; 406 } 407 } 408 s3.setInfo(getInnerPath(), info); 409 } 410 return info; 411 } 412 413 @Override 414 public long length() { 415 return getInfo().getSize(); 416 } 417 418 public Resource[] listResources() { 419 S3Resource[] children=null; 420 try { 421 if(isRoot()) { 422 Bucket[] buckets = s3.listBuckets(); 423 children=new S3Resource[buckets.length]; 424 for(int i=0;i<children.length;i++) { 425 children[i]=new S3Resource(s3,storage,provider,buckets[i].getName(),"",newPattern); 426 s3.setInfo(children[i].getInnerPath(),buckets[i]); 427 } 428 } 429 else if(isDirectory()){ 430 Content[] contents = s3.listContents(bucketName, isBucket()?null:objectName+"/"); 431 ArrayList<S3Resource> tmp = new ArrayList<S3Resource>(); 432 String key,name,path; 433 int index; 434 Set<String> names=new LinkedHashSet<String>(); 435 Set<String> pathes=new LinkedHashSet<String>(); 436 S3Resource r; 437 boolean isb=isBucket(); 438 for(int i=0;i<contents.length;i++) { 439 key=ResourceUtil.translatePath(contents[i].getKey(), false, false); 440 if(!isb && !key.startsWith(objectName+"/")) continue; 441 if(Util.isEmpty(key)) continue; 442 index=key.indexOf('/',Util.length(objectName)+1); 443 if(index==-1) { 444 name=key; 445 path=null; 446 } 447 else { 448 name=key.substring(index+1); 449 path=key.substring(0,index); 450 } 451 452 //print.out("1:"+key); 453 //print.out("path:"+path); 454 //print.out("name:"+name); 455 if(path==null){ 456 names.add(name); 457 tmp.add(r=new S3Resource(s3,storage,provider,contents[i].getBucketName(),key,newPattern)); 458 s3.setInfo(r.getInnerPath(),contents[i]); 459 } 460 else { 461 pathes.add(path); 462 } 463 } 464 465 Iterator<String> it = pathes.iterator(); 466 while(it.hasNext()) { 467 path=it.next(); 468 if(names.contains(path)) continue; 469 tmp.add(r=new S3Resource(s3,storage,provider,bucketName,path,newPattern)); 470 s3.setInfo(r.getInnerPath(),UNDEFINED_WITH_CHILDREN2); 471 } 472 473 //if(tmp.size()==0 && !isDirectory()) return null; 474 475 children=tmp.toArray(new S3Resource[tmp.size()]); 476 } 477 } 478 catch(Exception t) { 479 t.printStackTrace(); 480 return null; 481 } 482 return children; 483 } 484 485 @Override 486 public void remove(boolean force) throws IOException { 487 if(isRoot()) throw new IOException("can not remove root of S3 Service"); 488 489 ResourceUtil.checkRemoveOK(this); 490 491 492 boolean isd=isDirectory(); 493 if(isd) { 494 Resource[] children = listResources(); 495 if(children.length>0) { 496 if(force) { 497 for(int i=0;i<children.length;i++) { 498 children[i].remove(force); 499 } 500 } 501 else { 502 throw new IOException("can not remove directory ["+this+"], directory is not empty"); 503 } 504 } 505 } 506 // delete res itself 507 provider.lock(this); 508 try { 509 s3.delete(bucketName, isd?objectName+"/":objectName); 510 } 511 catch (Exception e) { 512 throw new IOException(e.getMessage()); 513 } 514 finally { 515 s3.releaseCache(getInnerPath()); 516 provider.unlock(this); 517 } 518 519 520 } 521 522 public boolean setLastModified(long time) { 523 s3.releaseCache(getInnerPath()); 524 // TODO Auto-generated method stub 525 return false; 526 } 527 528 public void setMode(int mode) throws IOException { 529 s3.releaseCache(getInnerPath()); 530 // TODO Auto-generated method stub 531 532 } 533 534 public boolean setReadable(boolean readable) { 535 s3.releaseCache(getInnerPath()); 536 // TODO Auto-generated method stub 537 return false; 538 } 539 540 public boolean setWritable(boolean writable) { 541 s3.releaseCache(getInnerPath()); 542 // TODO Auto-generated method stub 543 return false; 544 } 545 546 547 public AccessControlPolicy getAccessControlPolicy() { 548 String p = getInnerPath(); 549 try { 550 AccessControlPolicy acp = s3.getACP(p); 551 if(acp==null){ 552 acp=s3.getAccessControlPolicy(bucketName, getObjectName()); 553 s3.setACP(p, acp); 554 } 555 556 557 return acp; 558 } 559 catch (Exception e) { 560 throw new PageRuntimeException(Caster.toPageException(e)); 561 } 562 } 563 564 public void setAccessControlPolicy(AccessControlPolicy acp) { 565 566 try { 567 s3.setAccessControlPolicy(bucketName, getObjectName(),acp); 568 } 569 catch (Exception e) { 570 throw new PageRuntimeException(Caster.toPageException(e)); 571 } 572 finally { 573 s3.releaseCache(getInnerPath()); 574 } 575 } 576 577 private String getObjectName() { 578 if(!StringUtil.isEmpty(objectName) && isDirectory()) { 579 return objectName+"/"; 580 } 581 return objectName; 582 } 583 584 585 public void setACL(int acl) { 586 this.acl=acl; 587 } 588 589 590 public void setStorage(int storage) { 591 this.storage=storage; 592 } 593 594 595 596} 597 598 599 class Dummy implements S3Info { 600 601 private long lastModified; 602 private long size; 603 private boolean exists; 604 private boolean file; 605 private boolean directory; 606 private String label; 607 608 609 public Dummy(String label,long lastModified, long size, boolean exists,boolean file, boolean directory) { 610 this.label = label; 611 this.lastModified = lastModified; 612 this.size = size; 613 this.exists = exists; 614 this.file = file; 615 this.directory = directory; 616 } 617 618 619 @Override 620 public long getLastModified() { 621 return lastModified; 622 } 623 624 @Override 625 public long getSize() { 626 return size; 627 } 628 629 @Override 630 public String toString() { 631 return "Dummy:"+getLabel(); 632 } 633 634 635 /** 636 * @return the label 637 */ 638 public String getLabel() { 639 return label; 640 } 641 642 643 @Override 644 public boolean exists() { 645 return exists; 646 } 647 648 @Override 649 public boolean isDirectory() { 650 return directory; 651 } 652 653 @Override 654 public boolean isFile() { 655 return file; 656 } 657 658}