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}