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]>