Author: toad
Date: 2007-06-16 13:19:29 +0000 (Sat, 16 Jun 2007)
New Revision: 13622

Added:
   trunk/freenet/src/freenet/client/ArchiveExtractCallback.java
   trunk/freenet/src/freenet/support/io/MultiReaderBucket.java
Modified:
   trunk/freenet/src/freenet/client/ArchiveManager.java
   trunk/freenet/src/freenet/client/ArchiveStoreContext.java
   trunk/freenet/src/freenet/client/ArchiveStoreItem.java
   trunk/freenet/src/freenet/client/ErrorArchiveStoreItem.java
   trunk/freenet/src/freenet/client/RealArchiveStoreItem.java
   trunk/freenet/src/freenet/client/async/SingleFileFetcher.java
Log:
Solve various problems in archive extraction:
- Prevent infinite recursion.
- Prevent race conditions involving data being repeatedly extracted and deleted.
- Deal correctly with the extracted data not fitting in the cache (thus the 
cache can be tiny if necessary, and containers can be huge e.g. inserting a 
kernel tarball as an implicit container).
- Avoid unnecessary copying.

Added: trunk/freenet/src/freenet/client/ArchiveExtractCallback.java
===================================================================
--- trunk/freenet/src/freenet/client/ArchiveExtractCallback.java                
                (rev 0)
+++ trunk/freenet/src/freenet/client/ArchiveExtractCallback.java        
2007-06-16 13:19:29 UTC (rev 13622)
@@ -0,0 +1,15 @@
+package freenet.client;
+
+import freenet.support.api.Bucket;
+
+/** Called when we have extracted an archive, and a specified file either is
+ * or isn't in it. */
+public interface ArchiveExtractCallback {
+
+       /** Got the data */
+       public void gotBucket(Bucket data);
+       
+       /** Not in the archive */
+       public void notInArchive();
+       
+}

Modified: trunk/freenet/src/freenet/client/ArchiveManager.java
===================================================================
--- trunk/freenet/src/freenet/client/ArchiveManager.java        2007-06-15 
23:36:51 UTC (rev 13621)
+++ trunk/freenet/src/freenet/client/ArchiveManager.java        2007-06-16 
13:19:29 UTC (rev 13622)
@@ -34,6 +34,7 @@
  */
 public class ArchiveManager {

+       public static final String METADATA_NAME = ".metadata";
        private static boolean logMINOR;

        /**
@@ -145,7 +146,7 @@
                        storedData.push(k, asi);
                }
                if(logMINOR) Logger.minor(this, "Found data");
-               return asi.getDataOrThrow();
+               return asi.getReaderBucket();
        }

        /**
@@ -165,15 +166,18 @@
         * @param data The actual data fetched.
         * @param archiveContext The context for the whole fetch process.
         * @param ctx The ArchiveStoreContext for this key.
+        * @param element A particular element that the caller is especially 
interested in, or null.
+        * @param callback A callback to be called if we find that element, or 
if we don't.
         * @throws ArchiveFailureException If we could not extract the data, or 
it was too big, etc.
         * @throws ArchiveRestartException 
         * @throws ArchiveRestartException If the request needs to be restarted 
because the archive
         * changed.
         */
-       public void extractToCache(FreenetURI key, short archiveType, Bucket 
data, ArchiveContext archiveContext, ArchiveStoreContext ctx) throws 
ArchiveFailureException, ArchiveRestartException {
+       public void extractToCache(FreenetURI key, short archiveType, Bucket 
data, ArchiveContext archiveContext, ArchiveStoreContext ctx, String element, 
ArchiveExtractCallback callback) throws ArchiveFailureException, 
ArchiveRestartException {

                logMINOR = Logger.shouldLog(Logger.MINOR, this);

+               boolean gotElement = false;
                if(logMINOR) Logger.minor(this, "Extracting "+key);
                ctx.onExtract();
                ctx.removeAllCachedItems(); // flush cache anyway
@@ -249,16 +253,30 @@
                                        out.close();
                                        if(name.equals(".metadata"))
                                                gotMetadata = true;
-                                       addStoreElement(ctx, key, name, temp);
+                                       ArchiveStoreItem item = 
addStoreElement(ctx, key, name, temp);
+                                       if((!gotElement) && element != null && 
name.equals(element)) {
+                                               gotElement = true;
+                                               // Let it throw, if it does 
something is drastically wrong
+                                               
callback.gotBucket(item.getReaderBucket());
+                                       }
                                        names.add(name);
                                }
                        }

                        // If no metadata, generate some
                        if(!gotMetadata) {
-                               generateMetadata(ctx, key, names);
+                               ArchiveStoreItem item = generateMetadata(ctx, 
key, names);
+                               if(element != null && (!gotElement) && 
element.equals(METADATA_NAME)) {
+                                       gotElement = true;
+                                       // Let it throw, if it does something 
is drastically wrong
+                                       
callback.gotBucket(item.getReaderBucket());
+                               }
                        }
                        if(throwAtExit) throw new 
ArchiveRestartException("Archive changed on re-fetch");
+                       
+                       if((!gotElement) && element != null)
+                               callback.notInArchive();
+                       
                } catch (IOException e) {
                        throw new ArchiveFailureException("Error reading 
archive: "+e.getMessage(), e);
                } finally {
@@ -279,7 +297,7 @@
         * @param names Set of names in the archive.
         * @throws ArchiveFailureException 
         */
-       private void generateMetadata(ArchiveStoreContext ctx, FreenetURI key, 
HashSet names) throws ArchiveFailureException {
+       private ArchiveStoreItem generateMetadata(ArchiveStoreContext ctx, 
FreenetURI key, HashSet names) throws ArchiveFailureException {
                /* What we have to do is to:
                 * - Construct a filesystem tree of the names.
                 * - Turn each level of the tree into a Metadata object, 
including those below it, with
@@ -304,8 +322,7 @@
                                OutputStream os = 
element.bucket.getOutputStream();
                                os.write(buf);
                                os.close();
-                               addStoreElement(ctx, key, ".metadata", element);
-                               break;
+                               return addStoreElement(ctx, key, ".metadata", 
element);
                        } catch (MetadataUnresolvedException e) {
                                try {
                                        x = resolve(e, x, element, ctx, key);
@@ -383,13 +400,14 @@
        /**
         * Add a store element.
         */
-       private void addStoreElement(ArchiveStoreContext ctx, FreenetURI key, 
String name, TempStoreElement temp) {
+       private ArchiveStoreItem addStoreElement(ArchiveStoreContext ctx, 
FreenetURI key, String name, TempStoreElement temp) {
                RealArchiveStoreItem element = new RealArchiveStoreItem(this, 
ctx, key, name, temp);
                if(logMINOR) Logger.minor(this, "Adding store element: 
"+element+" ( "+key+ ' ' +name+" size "+element.spaceUsed()+" )");
                synchronized (storedData) {
                        storedData.push(element.key, element);
                }
                trimStoredData();
+               return element;
        }

        /**

Modified: trunk/freenet/src/freenet/client/ArchiveStoreContext.java
===================================================================
--- trunk/freenet/src/freenet/client/ArchiveStoreContext.java   2007-06-15 
23:36:51 UTC (rev 13621)
+++ trunk/freenet/src/freenet/client/ArchiveStoreContext.java   2007-06-16 
13:19:29 UTC (rev 13622)
@@ -46,6 +46,8 @@

        /**
         * Fetch a file in an archive.
+        * @return A Bucket containing the data. This will not be freed until 
the 
+        * client is finished with it i.e. calls free() or it is finalized.
         */
        public Bucket get(String internalName, ArchiveContext archiveContext, 
ClientMetadata dm, int recursionLevel, 
                        boolean dontEnterImplicitArchives) throws 
ArchiveFailureException, ArchiveRestartException, MetadataParseException, 
FetchException {
@@ -139,8 +141,8 @@
                return key;
        }

-       public void extractToCache(Bucket bucket, ArchiveContext actx) throws 
ArchiveFailureException, ArchiveRestartException {
-               manager.extractToCache(key, archiveType, bucket, actx, this);
+       public void extractToCache(Bucket bucket, ArchiveContext actx, String 
element, ArchiveExtractCallback callback) throws ArchiveFailureException, 
ArchiveRestartException {
+               manager.extractToCache(key, archiveType, bucket, actx, this, 
element, callback);
        }

        public void onExtract() {

Modified: trunk/freenet/src/freenet/client/ArchiveStoreItem.java
===================================================================
--- trunk/freenet/src/freenet/client/ArchiveStoreItem.java      2007-06-15 
23:36:51 UTC (rev 13621)
+++ trunk/freenet/src/freenet/client/ArchiveStoreItem.java      2007-06-16 
13:19:29 UTC (rev 13622)
@@ -39,4 +39,10 @@
         * Return the amount of cache space used by the item.
         */
        abstract long spaceUsed();
+       
+       /**
+        * Get the data as a Bucket, and guarantee that it won't be freed until 
the
+        * returned object is either finalized or freed.
+        */
+       abstract Bucket getReaderBucket() throws ArchiveFailureException;
 }

Modified: trunk/freenet/src/freenet/client/ErrorArchiveStoreItem.java
===================================================================
--- trunk/freenet/src/freenet/client/ErrorArchiveStoreItem.java 2007-06-15 
23:36:51 UTC (rev 13621)
+++ trunk/freenet/src/freenet/client/ErrorArchiveStoreItem.java 2007-06-16 
13:19:29 UTC (rev 13622)
@@ -35,5 +35,9 @@
        public long spaceUsed() {
                return 0;
        }
+
+       Bucket getReaderBucket() throws ArchiveFailureException {
+               throw new ArchiveFailureException(error);
+       }

 }

Modified: trunk/freenet/src/freenet/client/RealArchiveStoreItem.java
===================================================================
--- trunk/freenet/src/freenet/client/RealArchiveStoreItem.java  2007-06-15 
23:36:51 UTC (rev 13621)
+++ trunk/freenet/src/freenet/client/RealArchiveStoreItem.java  2007-06-16 
13:19:29 UTC (rev 13622)
@@ -7,16 +7,15 @@

 import freenet.keys.FreenetURI;
 import freenet.support.api.Bucket;
-import freenet.support.io.FileBucket;
 import freenet.support.io.FileUtil;
-import freenet.support.io.PaddedEphemerallyEncryptedBucket;
+import freenet.support.io.MultiReaderBucket;

 class RealArchiveStoreItem extends ArchiveStoreItem {

        private final ArchiveManager manager;
        private final File myFilename;
-       private final PaddedEphemerallyEncryptedBucket bucket;
-       private final FileBucket underBucket;
+       private final MultiReaderBucket mb;
+       private final Bucket bucket;
        private final long spaceUsed;

        /**
@@ -29,11 +28,11 @@
        RealArchiveStoreItem(ArchiveManager manager, ArchiveStoreContext ctx, 
FreenetURI key2, String realName, TempStoreElement temp) {
                super(new ArchiveKey(key2, realName), ctx);
                this.manager = manager;
-               this.bucket = temp.bucket;
-               this.underBucket = temp.underBucket;
-               underBucket.setReadOnly();
-               this.myFilename = underBucket.getFile();
-               spaceUsed = FileUtil.estimateUsage(myFilename, 
underBucket.size());
+               mb = new MultiReaderBucket(temp.bucket);
+               this.bucket = mb.getReaderBucket();
+               temp.underBucket.setReadOnly();
+               this.myFilename = temp.underBucket.getFile();
+               spaceUsed = FileUtil.estimateUsage(myFilename, 
temp.underBucket.size());
                this.manager.incrementSpace(spaceUsed);
        }

@@ -59,10 +58,14 @@
        }

        void innerClose() {
-               underBucket.free();
+               bucket.free();
        }

        Bucket getDataOrThrow() throws ArchiveFailureException {
                return dataAsBucket();
        }
+
+       Bucket getReaderBucket() throws ArchiveFailureException {
+               return mb.getReaderBucket();
+       }
 }

Modified: trunk/freenet/src/freenet/client/async/SingleFileFetcher.java
===================================================================
--- trunk/freenet/src/freenet/client/async/SingleFileFetcher.java       
2007-06-15 23:36:51 UTC (rev 13621)
+++ trunk/freenet/src/freenet/client/async/SingleFileFetcher.java       
2007-06-16 13:19:29 UTC (rev 13622)
@@ -8,7 +8,9 @@
 import java.util.LinkedList;

 import freenet.client.ArchiveContext;
+import freenet.client.ArchiveExtractCallback;
 import freenet.client.ArchiveFailureException;
+import freenet.client.ArchiveManager;
 import freenet.client.ArchiveRestartException;
 import freenet.client.ArchiveStoreContext;
 import freenet.client.ClientMetadata;
@@ -274,7 +276,31 @@
                                                throw new 
FetchException(FetchException.BUCKET_ERROR, e);
                                        }
                                } else {
-                                       fetchArchive(false, archiveMetadata); 
// will result in this function being called again
+                                       fetchArchive(false, archiveMetadata, 
ArchiveManager.METADATA_NAME, new ArchiveExtractCallback() {
+                                               public void gotBucket(Bucket 
data) {
+                                                       try {
+                                                               metadata = 
Metadata.construct(data);
+                                                       } catch (IOException e) 
{
+                                                               // Bucket error?
+                                                               onFailure(new 
FetchException(FetchException.BUCKET_ERROR, e));
+                                                       }
+                                                       try {
+                                                               
handleMetadata();
+                                                       } catch 
(MetadataParseException e) {
+                                                               
SingleFileFetcher.this.onFailure(new FetchException(e));
+                                                       } catch (FetchException 
e) {
+                                                               
e.setNotFinalizedSize();
+                                                               
SingleFileFetcher.this.onFailure(e);
+                                                       } catch 
(ArchiveFailureException e) {
+                                                               
SingleFileFetcher.this.onFailure(new FetchException(e));
+                                                       } catch 
(ArchiveRestartException e) {
+                                                               
SingleFileFetcher.this.onFailure(new FetchException(e));
+                                                       }
+                                               }
+                                               public void notInArchive() {
+                                                       onFailure(new 
FetchException(FetchException.INTERNAL_ERROR, "No metadata in container! Cannot 
happen as ArchiveManager should synthesise some!"));
+                                               }
+                                       }); // will result in this function 
being called again
                                        return;
                                }
                                continue;
@@ -289,17 +315,15 @@
                                Bucket dataBucket = ah.get(filename, actx, 
null, recursionLevel+1, true);
                                if(dataBucket != null) {
                                        if(logMINOR) Logger.minor(this, 
"Returning data");
-                                       // The client may free it, which is 
bad, or it may hang on to it for so long that it gets
-                                       // freed by us, which is also bad.
-                                       // So copy it.
-                                       // FIXME this is stupid, reconsider how 
we determine when to free buckets; refcounts maybe?
                                        Bucket out;
                                        try {
-                                               if(returnBucket != null)
+                                               // Data will not be freed until 
client is finished with it.
+                                               if(returnBucket != null) {
                                                        out = returnBucket;
-                                               else
-                                                       out = 
ctx.bucketFactory.makeBucket(dataBucket.size());
-                                               BucketTools.copy(dataBucket, 
out);
+                                                       
BucketTools.copy(dataBucket, out);
+                                               } else {
+                                                       out = dataBucket;
+                                               }
                                        } catch (IOException e) {
                                                onFailure(new 
FetchException(FetchException.BUCKET_ERROR));
                                                return;
@@ -312,7 +336,29 @@
                                        // Metadata cannot contain pointers to 
files which don't exist.
                                        // We enforce this in ArchiveHandler.
                                        // Therefore, the archive needs to be 
fetched.
-                                       fetchArchive(true, archiveMetadata);
+                                       fetchArchive(true, archiveMetadata, 
filename, new ArchiveExtractCallback() {
+                                               public void gotBucket(Bucket 
data) {
+                                                       if(logMINOR) 
Logger.minor(this, "Returning data");
+                                                       Bucket out;
+                                                       try {
+                                                               // Data will 
not be freed until client is finished with it.
+                                                               if(returnBucket 
!= null) {
+                                                                       out = 
returnBucket;
+                                                                       
BucketTools.copy(data, out);
+                                                               } else {
+                                                                       out = 
data;
+                                                               }
+                                                       } catch (IOException e) 
{
+                                                               onFailure(new 
FetchException(FetchException.BUCKET_ERROR));
+                                                               return;
+                                                       }
+                                                       // Return the data
+                                                       onSuccess(new 
FetchResult(clientMetadata, out));
+                                               }
+                                               public void notInArchive() {
+                                                       onFailure(new 
FetchException(FetchException.NOT_IN_ARCHIVE));
+                                               }
+                                       });
                                        // Will call back into this function 
when it has been fetched.
                                        return;
                                }
@@ -440,7 +486,7 @@
                decompressors.addLast(codec);
        }

-       private void fetchArchive(boolean forData, Metadata meta) throws 
FetchException, MetadataParseException, ArchiveFailureException, 
ArchiveRestartException {
+       private void fetchArchive(boolean forData, Metadata meta, String 
element, ArchiveExtractCallback callback) throws FetchException, 
MetadataParseException, ArchiveFailureException, ArchiveRestartException {
                if(logMINOR) Logger.minor(this, "fetchArchive()");
                // Fetch the archive
                // How?
@@ -451,7 +497,7 @@
                Metadata newMeta = (Metadata) meta.clone();
                newMeta.setSimpleRedirect();
                SingleFileFetcher f;
-               f = new SingleFileFetcher(this, newMeta, new 
ArchiveFetcherCallback(forData), new FetchContext(ctx, 
FetchContext.SET_RETURN_ARCHIVES, true));
+               f = new SingleFileFetcher(this, newMeta, new 
ArchiveFetcherCallback(forData, element, callback), new FetchContext(ctx, 
FetchContext.SET_RETURN_ARCHIVES, true));
                f.handleMetadata();
                // When it is done (if successful), the ArchiveCallback will 
re-call this function.
                // Which will then discover that the metadata *is* available.
@@ -461,14 +507,18 @@
        class ArchiveFetcherCallback implements GetCompletionCallback {

                private final boolean wasFetchingFinalData;
+               private final String element;
+               private final ArchiveExtractCallback callback;

-               ArchiveFetcherCallback(boolean wasFetchingFinalData) {
+               ArchiveFetcherCallback(boolean wasFetchingFinalData, String 
element, ArchiveExtractCallback cb) {
                        this.wasFetchingFinalData = wasFetchingFinalData;
+                       this.element = element;
+                       this.callback = cb;
                }

                public void onSuccess(FetchResult result, ClientGetState state) 
{
                        try {
-                               ah.extractToCache(result.asBucket(), actx);
+                               ah.extractToCache(result.asBucket(), actx, 
element, callback);
                        } catch (ArchiveFailureException e) {
                                SingleFileFetcher.this.onFailure(new 
FetchException(e));
                                return;

Added: trunk/freenet/src/freenet/support/io/MultiReaderBucket.java
===================================================================
--- trunk/freenet/src/freenet/support/io/MultiReaderBucket.java                 
        (rev 0)
+++ trunk/freenet/src/freenet/support/io/MultiReaderBucket.java 2007-06-16 
13:19:29 UTC (rev 13622)
@@ -0,0 +1,83 @@
+/* This code is part of Freenet. It is distributed under the GNU General
+ * Public License, version 2 (or at your option any later version). See
+ * http://www.gnu.org/ for further details of the GPL. */
+package freenet.support.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+import freenet.support.api.Bucket;
+
+/**
+ * A wrapper for a read-only bucket providing for multiple readers. The data 
is 
+ * only freed when all of the readers have freed it.
+ * @author toad
+ */
+public class MultiReaderBucket {
+       
+       private final Bucket bucket;
+       
+       // Assume there will be relatively few readers
+       private ArrayList readers;
+       
+       private boolean closed;
+       
+       public MultiReaderBucket(Bucket underlying) {
+               bucket = underlying;
+       }
+
+       /** Get a reader bucket */
+       public Bucket getReaderBucket() {
+               synchronized(this) {
+                       if(closed) return null;
+                       Bucket d = new ReaderBucket();
+                       readers.add(d);
+                       return d;
+               }
+       }
+
+       class ReaderBucket implements Bucket {
+
+               public void free() {
+                       synchronized(MultiReaderBucket.this) {
+                               readers.remove(this);
+                               if(!readers.isEmpty()) return;
+                               if(closed) return;
+                               closed = true;
+                       }
+                       bucket.free();
+               }
+
+               public InputStream getInputStream() throws IOException {
+                       return bucket.getInputStream();
+               }
+
+               public String getName() {
+                       return bucket.getName();
+               }
+
+               public OutputStream getOutputStream() throws IOException {
+                       throw new IOException("Read only");
+               }
+
+               public boolean isReadOnly() {
+                       return true;
+               }
+
+               public void setReadOnly() {
+                       // Already read only
+               }
+
+               public long size() {
+                       return bucket.size();
+               }
+               
+               public void finalize() {
+                       free();
+               }
+               
+       }
+       
+}


Reply via email to