Attached is a patch for the org.apache.slide.webdav.method.PutMethod
class from Slide 1.0.16 implementing support for partial PUT. There is an
assumption that only one byte range is specified in the "Range" header.
-----Original Message-----
From: Remy Maucherat [mailto:[EMAIL PROTECTED]]
Sent: Monday, November 19, 2001 10:22 AM
To: Slide Developers List
Subject: Re: Plans for supporting HTTP PUT resume?
> As of version 1.0.0-1.3.6 of mod_dav for Apache, it appears that
> Content-Range support for PUT has been implemented (see
> http://www.webdav.org/mod_dav/). Can you provide further details about
why
> this would be complex/difficult to implement in Slide?
- It is not supported by any of the underlying objects, and adding it would
require changing a lot of objects / helper / stores
- Many stores would have trouble implementing the functionality, so it's
really a problem to require that
Maybe what could be done is having the WebdavServlet hide this. It could
work on the file in the work directory, and only actually store it in Slide
when it's complete. That's the only realistic solution I can think of.
If you're interested in contributing to Slide, that could be a nice
subproject to start with, as it's mostly independent from the rest (so you
won't have to learn the whole Slide at once).
Tomcat 4 uses a JNDI directory context abstrcation, so it's about as much a
problem to implement partial PUT.
Remy
--
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 Tue Nov 27 15:38:29 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 "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,46 @@
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 "Range:" header - partial PUTs:
+ Vector ranges = 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
+ // Assume just one range is specified for now
+ if ((ranges != null) && (!ranges.isEmpty())) {
+ contentFile = executePartial(ranges, 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 +285,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 "Range:" header - partial PUTs:
+ Vector ranges = parseRange(req, resp, resourceInfo);
+ if ((ranges != null) && (!ranges.isEmpty())) {
+ contentFile = executePartial(ranges, null);
+ usingPartialPut = true;
+
+ contentFileInStream = new FileInputStream(contentFile);
+ }
NodeProperty property = null;
@@ -262,9 +324,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 +392,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 +446,277 @@
return true;
}
+ // Handle a partial PUT. New content specified in request is appended to
+ // existing content in oldRevisionContent (if present).
+ private File executePartial(Vector ranges, NodeRevisionContent
+oldRevisionContent)
+ throws IOException {
+ // Append data specified in ranges to existing content for this
+ // resource - create a temp. file on the local filesystem to
+ // perform this operation
+ // Assume just one range is specified for now
+ Range currentRange = (Range) ranges.get(0);
+ 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 range header.
+ *
+ * @param request The servlet request we are processing
+ * @param response The servlet response we are creating
+ * @return Vector of ranges
+ */
+ private Vector parseRange(HttpServletRequest request,
+ HttpServletResponse response,
+ ResourceInfo resourceInfo)
+ throws IOException {
+
+ // Checking If-Range
+ String headerValue = request.getHeader("If-Range");
+ if (headerValue != null) {
+
+ String eTag = getETag(resourceInfo, true);
+ long lastModified = resourceInfo.date;
+
+ Date date = null;
+
+ // Parsing the HTTP Date
+ for (int i = 0; (date == null) && (i < formats.length); i++) {
+ try {
+ synchronized (formats[i]) {
+ date = formats[i].parse(headerValue);
+ }
+ } catch (ParseException e) {
+ ;
+ }
+ }
+
+ if (date == null) {
+
+ // If the ETag the client gave does not match the entity
+ // etag, then the entire entity is returned.
+ if (!eTag.equals(headerValue.trim()))
+ return null;
+
+ } else {
+
+ // If the timestamp of the entity the client got is older than
+ // the last modification date of the entity, the entire entity
+ // is returned.
+ if (lastModified > (date.getTime() + 1000))
+ return null;
+
+ }
+
+ }
+
+ long fileLength = resourceInfo.length;
+
+ if (fileLength == 0)
+ return null;
+
+ // Retrieving the range header (if any is specified
+ String rangeHeader = request.getHeader("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);
+
+ // Vector which will contain all the ranges which are successfully
+ // parsed.
+ Vector result = new Vector();
+ StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
+
+ // Parsing the range list
+ while (commaTokenizer.hasMoreTokens()) {
+ String rangeDefinition = commaTokenizer.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;
+ }
+
+ result.addElement(currentRange);
+ }
+
+ return result;
+ }
+
+ /**
+ * 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]>