Here is an updated version of the patch that uses the
"Content-Range" header instead of the "Range" header.  Let me know if you
see any other problems/issues.

-----Original Message-----
From: Remy Maucherat [mailto:[EMAIL PROTECTED]]
Sent: Wednesday, November 28, 2001 1:04 PM
To: Slide Developers List
Subject: Re: Plans for supporting HTTP PUT resume?


> See http://curl.haxx.se/libcurl/competitors.html for a list of some
> open source HTTP client libraries/tools.  It appears that some of these
> tools support HTTP resume, although I'm uncertain about the state of PUT
> resume support in these tools.

Ok, I reviewed the patch, and there's a problem.
The header that should be used here is not "Range", but "Content-Range"
(which is great, since it specifies the final length of the resource).
"Range" is used only for retrieval operations.

Modifying the patch to parse "Content-Range" instead is not too hard. I plan
to experiment with TC 4 first (the new HTTP/1.1 features go there first
usually).

Remy

> > I tested this patch with a custom client that I wrote to support
> > partial PUT - unfortunately, I can't post the source.
>
> No problem.
>
> > According to the cURL web site (http://curl.haxx.se/), cURL supports
> > PUT resume.  However, I have not tested or used cURL before.
> >
> > Do you have a pointer to Getright?  I haven't used this tool before.
>
> http://www.getright.com/
> The downloading features are quite impressive.
>
> I think there are equivalents in OSS, but I'm not too sure. I'll have to
> check SF ...
>
> Remy
>
>
> --
> To unsubscribe, e-mail:
<mailto:[EMAIL PROTECTED]>
> For additional commands, e-mail:
<mailto:[EMAIL PROTECTED]>
>
> --
> To unsubscribe, e-mail:
<mailto:[EMAIL PROTECTED]>
> For additional commands, e-mail:
<mailto:[EMAIL PROTECTED]>
>


--
To unsubscribe, e-mail:   <mailto:[EMAIL PROTECTED]>
For additional commands, e-mail: <mailto:[EMAIL PROTECTED]>

--- PutMethod.java.orig Tue Nov 27 15:52:30 2001
+++ PutMethod.java      Wed Nov 28 15:42:51 2001
@@ -1,5 +1,5 @@
 /*
- * $Header: 
/home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/PutMethod.java,v
 1.20 2001/10/11 08:24:52 remm Exp $
+ * $Header: 
+/home/cvs/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/PutMethod.java,v
+ 1.20 2001/10/11 08:24:52 remm Exp $
  * $Revision: 1.20 $
  * $Date: 2001/10/11 08:24:52 $
  *
@@ -68,6 +68,7 @@
 import java.util.*;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.text.ParseException;
 import javax.servlet.*;
 import javax.servlet.http.*;
 import org.apache.util.WebdavStatus;
@@ -88,15 +89,24 @@
     
     // -------------------------------------------------------------- Constants
     
+    /**
+     * The set of SimpleDateFormat formats to use in getDateHeader().
+     */
+    protected static final SimpleDateFormat formats[] = {
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
+            new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
+            new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
+    };
     
     // ----------------------------------------------------- Instance Variables
     
-    
     /**
      * Resource to be written.
      */
     private String resourcePath;
     
+    // Size of file transfer buffer in bytes
+    private static int BUFFER_SIZE = 4096;
     
     // ----------------------------------------------------------- Constructors
     
@@ -142,19 +152,30 @@
         slideToken.setForceStoreEnlistment(true);
         
         try {
+            // Temp. content file used to support partial PUT
+            File contentFile = null;
+            
+            // Input stream for temp. content file used to support partial PUT
+            FileInputStream contentFileInStream = null;
+            
+            // Flag to indicate if a partial PUT (PUT with "Content-Range" header) has
+            // been specified for this request
+            boolean usingPartialPut = false;
             
             try {
-                
                 NodeRevisionDescriptors revisionDescriptors =
                     content.retrieve(slideToken, resourcePath);
                 
                 NodeRevisionNumber revisionNumber =
                     revisionDescriptors.getLatestRevision();
                 NodeRevisionDescriptor oldRevisionDescriptor = null;
+                NodeRevisionContent oldRevisionContent = null;
                 if (revisionNumber != null) {
                     try {
                         oldRevisionDescriptor = content.retrieve
                             (slideToken, revisionDescriptors);
+                        oldRevisionContent = content.retrieve(slideToken, 
+                            resourcePath, oldRevisionDescriptor);
                     } catch (RevisionDescriptorNotFoundException e) {
                     }
                 }
@@ -170,17 +191,45 @@
                     revisionDescriptor = oldRevisionDescriptor;
                     revisionDescriptor.setContentLength(-1);
                 }
+                
+                // ResourceInfo for this resource
+                ResourceInfo resourceInfo =
+                        new ResourceInfo(resourcePath, revisionDescriptor);
+                
                 NodeRevisionContent revisionContent =
                     new NodeRevisionContent();
-                //revisionContent.setContent(req.getReader());
-                revisionContent.setContent(req.getInputStream());
+                
+                // Add support for "Content-Range:" header - partial PUTs:
+                Range range = parseRange(req, resp, resourceInfo);
+                
+                // Append data specified in ranges to existing content for this
+                // resource - create a temp. file on the local filesystem to
+                // perform this operation
+                if (range != null) {
+                    contentFile = executePartial(range, oldRevisionContent);
+                    usingPartialPut = true;
+                    
+                    contentFileInStream = new FileInputStream(contentFile);
+
+                    revisionContent.setContent(contentFileInStream);
+                }
+                else {
+                    revisionContent.setContent(req.getInputStream());
+                }
                 
                 NodeProperty property = null;
                 
                 // Get content length
-                property = new NodeProperty
+                if (usingPartialPut) {
+                    property = new NodeProperty
+                        ("getcontentlength", new Long(contentFile.length()),
+                         true);
+                }
+                else {
+                    property = new NodeProperty
                     ("getcontentlength", new Long(req.getContentLength()),
                      true);
+                }
                 revisionDescriptor.setProperty(property);
                 
                 // Last modification date
@@ -235,15 +284,27 @@
                 // the transaction wants to be aborted
                 //
                 throw new WebdavException(WebdavStatus.SC_ACCEPTED, false);
-            } catch (ObjectNotFoundException e) {
-                
+            } catch (ObjectNotFoundException e) { 
+                // A file with a unique resourcePath is being uploaded:
                 // Todo : Check to see if parent exists
                 SubjectNode subject = new SubjectNode();
                 // Creating an object
                 structure.create(slideToken, subject, resourcePath);
                 
-                NodeRevisionDescriptor revisionDescriptor =
-                    new NodeRevisionDescriptor(req.getContentLength());
+                NodeRevisionDescriptor revisionDescriptor = new 
+NodeRevisionDescriptor();
+                
+                // ResourceInfo for this resource
+                ResourceInfo resourceInfo =
+                        new ResourceInfo(resourcePath, revisionDescriptor);
+                
+                 // Add support for "COntent-Range:" header - partial PUTs:
+                Range range = parseRange(req, resp, resourceInfo);
+                if (range != null) {
+                    contentFile = executePartial(range, null);
+                    usingPartialPut = true;
+                    
+                    contentFileInStream = new FileInputStream(contentFile);
+                }
                 
                 NodeProperty property = null;
                 
@@ -262,9 +323,16 @@
                 revisionDescriptor.setProperty(property);
                 
                 // Get content length
-                property = new NodeProperty
-                    ("getcontentlength",
-                     new Long(req.getContentLength()), true);
+                if (usingPartialPut) {
+                    property = new NodeProperty
+                        ("getcontentlength", new Long(contentFile.length()),
+                         true);
+                }
+                else {
+                    property = new NodeProperty
+                    ("getcontentlength", new Long(req.getContentLength()),
+                     true);
+                }
                 revisionDescriptor.setProperty(property);
                 
                 // Get content type
@@ -323,7 +391,13 @@
                 // Creating revisionDescriptor associated with the object
                 NodeRevisionContent revisionContent =
                     new NodeRevisionContent();
-                revisionContent.setContent(req.getInputStream());
+                
+                if (usingPartialPut) {
+                    revisionContent.setContent(contentFileInStream);
+                }
+                else {
+                    revisionContent.setContent(req.getInputStream()); 
+                }
                 
                 content.create(slideToken, resourcePath, revisionDescriptor,
                                revisionContent);
@@ -371,5 +445,234 @@
         return true;
     }
     
+    // Handle a partial PUT.  New content specified in request is appended to 
+    // existing content in oldRevisionContent (if present).
+    private File executePartial(Range currentRange, NodeRevisionContent 
+oldRevisionContent) 
+        throws IOException {
+        // Append data specified in currentRange to existing content for this
+        // resource - create a temp. file on the local filesystem to
+        // perform this operation
+        File tempDir = (File) 
+this.getConfig().getServletContext().getAttribute("javax.servlet.context.tempdir");
+        // Convert all '/' characters to '.' in resourcePath
+        String convertedResourcePath = resourcePath.replace('/', '.');
+        File contentFile = new File(tempDir, convertedResourcePath);
+        if (contentFile.createNewFile()) {
+            // Clean up contentFile when Slide is terminated
+            contentFile.deleteOnExit();
+        }
+        
+        RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, 
+"rw");
+        
+        // Copy data in oldRevisionContent to contentFile
+        if (oldRevisionContent != null) {
+            BufferedInputStream bufOldRevStream = 
+                new BufferedInputStream(oldRevisionContent.streamContent(), 
+BUFFER_SIZE);
+            
+            int numBytesRead;
+            byte[] copyBuffer = new byte[BUFFER_SIZE];
+            while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
+                randAccessContentFile.write(copyBuffer, 0, numBytesRead);
+            }
+            
+            bufOldRevStream.close();
+        }
+        
+        // Append data in request input stream to contentFile
+        randAccessContentFile.seek(currentRange.start);
+        int numBytesRead;
+        byte[] transferBuffer = new byte[BUFFER_SIZE];
+        BufferedInputStream requestBufInStream =
+            new BufferedInputStream(req.getInputStream(), BUFFER_SIZE);
+        while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
+            randAccessContentFile.write(transferBuffer, 0, numBytesRead);
+        }
+        randAccessContentFile.close();
+        requestBufInStream.close();
+        
+        return contentFile;
+    }
+    
+    /**
+     * Parse the content range header.
+     *
+     * @param request The servlet request we are processing
+     * @param response The servlet response we are creating
+     * @return Vector of ranges
+     */
+    private Range parseRange(HttpServletRequest request,
+                              HttpServletResponse response,
+                              ResourceInfo resourceInfo)
+        throws IOException {
+        
+        long fileLength = resourceInfo.length;
+        
+        if (fileLength == 0)
+            return null;
+        
+        // Retrieving the content range header (if any is specified)
+        String rangeHeader = request.getHeader("Content-Range");
+        
+        if (rangeHeader == null)
+            return null;
+        // bytes is the only range unit supported (and I don't see the point
+        // of adding new ones).
+        if (!rangeHeader.startsWith("bytes")) {
+            response.sendError
+                (HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+            return null;
+        }
+        
+        rangeHeader = rangeHeader.substring(6);
+        
+        StringTokenizer rangeTokenizer = new StringTokenizer(rangeHeader, "/");
+        
+        // Parsing the range header
+        if (rangeTokenizer.hasMoreTokens()) {
+            String rangeDefinition = rangeTokenizer.nextToken();
+            
+            Range currentRange = new Range();
+            currentRange.length = fileLength;
+            
+            int dashPos = rangeDefinition.indexOf('-');
+            
+            if (dashPos == -1) {
+                response.sendError
+                    (HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+                return null;
+            }
+            
+            if (dashPos == 0) {        
+                try {
+                    long offset = Long.parseLong(rangeDefinition);
+                    currentRange.start = fileLength + offset;
+                    currentRange.end = fileLength - 1;
+                } catch (NumberFormatException e) {
+                    response.sendError
+                        (HttpServletResponse
+                             .SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+                    return null;
+                }
+                
+            } else {
+                try {
+                    currentRange.start = Long.parseLong
+                        (rangeDefinition.substring(0, dashPos));
+                    if (dashPos < rangeDefinition.length() - 1)
+                        currentRange.end = Long.parseLong
+                            (rangeDefinition.substring
+                                 (dashPos + 1, rangeDefinition.length()));
+                    else
+                        currentRange.end = fileLength - 1;
+                } catch (NumberFormatException e) {
+                    response.sendError
+                        (HttpServletResponse
+                             .SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+                    return null;
+                }
+                
+            }
+            
+            if (!currentRange.validate()) {
+                response.sendError
+                    (HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+                return null;
+            }
+            
+            return currentRange;
+        }
+        else {
+            response.sendError(
+                HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+            return null;
+        }
+    }
+    
+    /**
+     * Get the ETag value associated with a file.
+     *
+     * @param resourceInfo File object
+     * @param strong True if we want a strong ETag, in which case a checksum
+     * of the file has to be calculated
+     */
+    private String getETagValue(ResourceInfo resourceInfo, boolean strong) {
+        // FIXME : Compute a strong ETag if requested, using an MD5 digest
+        // of the file contents
+        return resourceInfo.length + "-" + resourceInfo.date;
+    }
+    
     
+    /**
+     * Get the ETag associated with a file.
+     *
+     * @param resourceInfo File object
+     * @param strong True if we want a strong ETag, in which case a checksum
+     * of the file has to be calculated
+     */
+    private String getETag(ResourceInfo resourceInfo, boolean strong) {
+        if (strong)
+            return "\"" + getETagValue(resourceInfo, strong) + "\"";
+        else
+            return "W/\"" + getETagValue(resourceInfo, strong) + "\"";
+    }
+    
+    
+    // ------------------------------------------------------ Range Inner Class
+    private class Range {
+        
+        public long start;
+        public long end;
+        public long length;
+        
+        /**
+         * Validate range.
+         */
+        public boolean validate() {
+            return ((start >= 0) && (end >= 0) && (start <= end));
+        }
+    }
+    
+    // ----------------------------------------------  ResourceInfo Inner Class
+    private class ResourceInfo {
+        
+        /**
+         * Constructor.
+         *
+         * @param pathname Path name of the file
+         */
+        public ResourceInfo(String path, NodeRevisionDescriptor properties) {
+            
+            this.path = path;
+            this.exists = true;
+            this.creationDate = properties.getCreationDateAsDate().getTime();
+            this.date = properties.getLastModifiedAsDate().getTime();
+            this.httpDate = properties.getLastModified();
+            this.length = properties.getContentLength();
+            
+        }
+        
+        
+        public String path;
+        public long creationDate;
+        public String httpDate;
+        public long date;
+        public long length;
+        //public boolean collection;
+        public boolean exists;
+        
+        
+        /**
+         * Test if the associated resource exists.
+         */
+        public boolean exists() {
+            return exists;
+        }
+        
+        
+        /**
+         * String representation.
+         */
+        public String toString() {
+            return path;
+        }
+    }
 }

--
To unsubscribe, e-mail:   <mailto:[EMAIL PROTECTED]>
For additional commands, e-mail: <mailto:[EMAIL PROTECTED]>

Reply via email to