All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.milton.http.http11.PutHandler Maven / Gradle / Ivy

Go to download

Milton Community Edition: Supports DAV level 1 and is available on Apache2 license

There is a newer version: 4.0.3.2215
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.milton.http.http11;

import io.milton.common.FileUtils;
import io.milton.common.LogUtils;
import io.milton.common.Path;
import io.milton.common.RandomFileOutputStream;
import io.milton.event.NewFolderEvent;
import io.milton.event.PutEvent;
import io.milton.http.FileItem;
import io.milton.http.Handler;
import io.milton.http.HandlerHelper;
import io.milton.http.HttpManager;
import io.milton.http.Range;
import io.milton.http.Request;
import io.milton.http.Request.Method;
import io.milton.http.RequestParseException;
import static io.milton.http.ResourceHandlerHelper.ATT_NAME_FILES;
import static io.milton.http.ResourceHandlerHelper.ATT_NAME_PARAMS;
import io.milton.http.Response;
import io.milton.http.Response.Status;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.ConflictException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.http.exceptions.NotFoundException;
import io.milton.http.quota.StorageChecker.StorageErrorReason;
import io.milton.http.webdav.WebDavResponseHandler;
import io.milton.resource.CalendarResource;
import io.milton.resource.CollectionResource;
import io.milton.resource.GetableResource;
import io.milton.resource.MakeCollectionableResource;
import io.milton.resource.PutableResource;
import io.milton.resource.ReplaceableResource;
import io.milton.resource.Resource;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PutHandler implements Handler {

	private static final Logger log = LoggerFactory.getLogger(PutHandler.class);
	private final Http11ResponseHandler responseHandler;
	private final HandlerHelper handlerHelper;
	private final PutHelper putHelper;
	private final MatchHelper matchHelper;

	public PutHandler(Http11ResponseHandler responseHandler, HandlerHelper handlerHelper, PutHelper putHelper, MatchHelper matchHelper) {
		this.responseHandler = responseHandler;
		this.handlerHelper = handlerHelper;
		this.putHelper = putHelper;
		this.matchHelper = matchHelper;
		checkResponseHandler();
	}

	private void checkResponseHandler() {
		if (!(responseHandler instanceof WebDavResponseHandler)) {
			log.warn("response handler is not a WebDavResponseHandler, so locking and quota checking will not be enabled");
		}
	}

	@Override
	public String[] getMethods() {
		return new String[]{Method.PUT.code};
	}

	@Override
	public boolean isCompatible(Resource handler) {
		return (handler instanceof PutableResource);
	}

	@Override
	public void process(HttpManager manager, Request request, Response response) throws NotAuthorizedException, ConflictException, BadRequestException, NotFoundException {
		// need a linked hash map to preserve ordering of params
		Map params = new LinkedHashMap<>();

		Map files = new HashMap<>();

		try {
			request.parseRequestParameters(params, files);
		} catch (RequestParseException ex) {
			if (log.isTraceEnabled()) {
				log.warn("failed to parse request parameters: {}", ex.getMessage(), ex);
			} else {
				log.warn("failed to parse request parameters: {}", ex.getMessage());
			}
			return;
		}
		request.getAttributes().put(ATT_NAME_PARAMS, params);
		request.getAttributes().put(ATT_NAME_FILES, files);

		if (!handlerHelper.checkExpects(responseHandler, request, response)) {
			return;
		}

		String host = request.getHostHeader();
		String urlToCreateOrUpdate = HttpManager.decodeUrl(request.getAbsolutePath());

		LogUtils.debug(log, "PUT request. Host:", host, " Url:", urlToCreateOrUpdate, " content length header:", request.getContentLengthHeader());

		Path path = Path.path(urlToCreateOrUpdate);
		urlToCreateOrUpdate = path.toString();

		Resource existingResource = manager.getResourceFactory().getResource(host, urlToCreateOrUpdate);
		StorageErrorReason storageErr = null;
		if (existingResource != null) {
			if (!handlerHelper.checkAuthorisation(manager, existingResource, request)) {
				responseHandler.respondUnauthorised(existingResource, response, request);
				return;
			}
			//Make sure the parent collection is not locked by someone else
			if (handlerHelper.isLockedOut(request, existingResource)) {
				log.warn("resource is locked, but not by the current user");
				respondLocked(request, response, existingResource);
				return;
			}
			// Check if the resource has been modified based on etags
			if (!matchHelper.checkIfMatch(existingResource, request)) {
				log.info("if-match comparison failed, aborting PUT request");
				responseHandler.respondPreconditionFailed(request, response, existingResource);
				return;
			}
			if (matchHelper.checkIfNoneMatch(existingResource, request)) {
				log.info("if-none-match comparison failed, aborting PUT request");
				responseHandler.respondPreconditionFailed(request, response, existingResource);
				return;
			}

			Resource parent = manager.getResourceFactory().getResource(host, path.getParent().toString());
			if (parent instanceof CollectionResource) {
				CollectionResource parentCol = (CollectionResource) parent;
				storageErr = handlerHelper.checkStorageOnReplace(request, parentCol, existingResource, host);
			} else {
				log.warn("parent exists but is not a collection resource: " + path.getParent());
			}
		} else {
			if (!matchHelper.checkIfMatch(null, request)) {
				// Special case: we will get a PUT on a calendar to create a resource with an etag check match
				// when no resource exists, when a user accepts a calendar invitation from lightning
				// So have a special case here that says to allow if the parent is a calendar
				Resource parent = manager.getResourceFactory().getResource(host, path.getParent().toString());
				if (parent instanceof CalendarResource) {
					//  https://bugzilla.mozilla.org/show_bug.cgi?id=540398
					log.info("if-match comparison failed on null resource, but parent is a calendar, so allow to proceed");
				} else {
					log.info("if-match comparison failed on null resource, aborting PUT request");
					responseHandler.respondPreconditionFailed(request, response, existingResource);
					return;
				}
			}
			if (matchHelper.checkIfNoneMatch(null, request)) {
				log.info("if-none-match comparison failed on null resource, aborting PUT request");
				responseHandler.respondPreconditionFailed(request, response, existingResource);
				return;
			}

			CollectionResource parentCol = putHelper.findNearestParent(manager, host, path);
			if (!handlerHelper.checkAuthorisation(manager, parentCol, request)) {
				responseHandler.respondUnauthorised(parentCol, response, request);
				return;
			}
			storageErr = handlerHelper.checkStorageOnAdd(request, parentCol, path.getParent(), host);
		}

		if (storageErr != null) {
			respondInsufficientStorage(request, response, storageErr);
			return;
		}

		ReplaceableResource replacee;
		if (existingResource instanceof ReplaceableResource) {
			replacee = (ReplaceableResource) existingResource;
		} else {
			replacee = null;
		}

		if (replacee != null) {
			if (log.isTraceEnabled()) {
				log.trace("replacing content in: " + replacee.getName() + " - " + replacee.getClass());
			}
			long t = System.currentTimeMillis();
			try {
				manager.onProcessResourceStart(request, response, replacee);
				processReplace(manager, request, response, replacee);
				manager.getEventManager().fireEvent(new PutEvent(replacee));
			} finally {
				t = System.currentTimeMillis() - t;
				manager.onProcessResourceFinish(request, response, replacee, t);
			}
		} else {
			// either no existing resource, or its not replaceable. check for folder
			String nameToCreate = path.getName();
			CollectionResource folderResource = findOrCreateFolders(manager, host, path.getParent(), request);
			if (folderResource != null) {
				long t = System.currentTimeMillis();
				try {
					if (folderResource instanceof PutableResource) {

						//Make sure the parent collection is not locked by someone else
						if (handlerHelper.isLockedOut(request, folderResource)) {
							respondLocked(request, response, folderResource);
							return;
						}

						PutableResource putableResource = (PutableResource) folderResource;
						processCreate(manager, request, response, putableResource, nameToCreate);
						files.forEach((key, file) -> org.apache.commons.io.FileUtils.deleteQuietly(new File(file.getPath())));
					} else {
						LogUtils.debug(log, "method not implemented: PUT on class: ", folderResource.getClass(), folderResource.getName());
						manager.getResponseHandler().respondMethodNotImplemented(folderResource, response, request);
					}
				} finally {
					t = System.currentTimeMillis() - t;
					manager.onProcessResourceFinish(request, response, folderResource, t);
				}
			} else {
				responseHandler.respondNotFound(response, request);
			}
		}
	}

	private void processCreate(HttpManager manager, Request request, Response response, PutableResource folder, String newName) throws ConflictException, BadRequestException, NotAuthorizedException {

		LogUtils.debug(log, "process: putting to: ", folder.getName());
		try {
			Long l = putHelper.getContentLength(request);
			String ct = putHelper.findContentTypes(request, newName);
			LogUtils.debug(log, "PutHandler: creating resource of type: ", ct);
			InputStream inputStream = request.getInputStream();
			if (MapUtils.isNotEmpty(request.getFiles())) {
				inputStream = request.getFiles().entrySet().stream().findFirst().get().getValue().getInputStream();
			}
			Resource newlyCreated = folder.createNew(newName, inputStream, l, ct);
			if (newlyCreated != null) {
				if (newName != null && !newName.equals(newlyCreated.getName())) {
					log.warn("getName on the created resource does not match the name requested by the client! requested: " + newName + " - created: " + newlyCreated.getName());
				}
				manager.getEventManager().fireEvent(new PutEvent(newlyCreated));
				manager.getResponseHandler().respondCreated(newlyCreated, response, request);
			} else {
				throw new RuntimeException("createNew method on: " + folder.getClass() + " returned a null resource. Must return a reference to the newly created or modified resource");
			}
		} catch (IOException ex) {
			throw new RuntimeException("IOException reading input stream. Probably interrupted upload", ex);
		}
	}

	private CollectionResource findOrCreateFolders(HttpManager manager, String host, Path path, Request request) throws NotAuthorizedException, ConflictException, BadRequestException {
		if (path == null) {
			return null;
		}

		Resource thisResource = manager.getResourceFactory().getResource(host, path.toString());
		if (thisResource != null) {
			// Defensive programming test for a common problem where resource factories
			// return the wrong resource for a given path
			if (thisResource.getName() != null && !thisResource.getName().equals(path.getName())) {
				log.warn("Your resource factory returned a resource with a different name to that requested!!! Requested: " + path.getName() + " returned: " + thisResource.getName() + " - resource factory: " + manager.getResourceFactory().getClass());
			}
			if (thisResource instanceof CollectionResource) {
				return (CollectionResource) thisResource;
			} else {
				log.warn("parent is not a collection: " + path);
				return null;
			}
		}

		CollectionResource parent = findOrCreateFolders(manager, host, path.getParent(), request);
		if (parent == null) {
			log.warn("couldnt find parent: " + path);
			return null;
		}

		Resource r = parent.child(path.getName());

		if (r == null) {
			log.info("Could not find child: " + path.getName() + " in parent: " + parent.getName() + " - " + parent.getClass());
			if (parent instanceof MakeCollectionableResource) {
				MakeCollectionableResource mkcol = (MakeCollectionableResource) parent;
				if (!handlerHelper.checkAuthorisation(manager, mkcol, request)) {
					throw new NotAuthorizedException(mkcol);
				}
				log.info("autocreating new folder: " + path.getName());
				CollectionResource newCol = mkcol.createCollection(path.getName());
				manager.getEventManager().fireEvent(new NewFolderEvent(newCol));
				return newCol;
			} else {
				log.info("parent folder isnt a MakeCollectionableResource: " + parent.getName() + " - " + parent.getClass());
				return null;
			}
		} else if (r instanceof CollectionResource) {
			return (CollectionResource) r;
		} else {
			log.info("parent in URL is not a collection: " + r.getName());
			return null;
		}
	}

	/**
	 * "If an existing resource is modified, either the 200 (OK) or 204 (No
	 * Content) response codes SHOULD be sent to indicate successful completion
	 * of the request."
	 *
	 * @param request
	 * @param response
	 * @param replacee
	 */
	private void processReplace(HttpManager manager, Request request, Response response, ReplaceableResource replacee) throws BadRequestException, NotAuthorizedException, ConflictException, NotFoundException {
		if (!handlerHelper.checkAuthorisation(manager, replacee, request)) {
			responseHandler.respondUnauthorised(replacee, response, request);
			return;
		}
		try {
			Range range = putHelper.parseContentRange(replacee, request);
			if (range != null) {
				log.debug("partial put: " + range);
				if (replacee instanceof PartialllyUpdateableResource) {
					log.debug("doing partial put on a PartialllyUpdateableResource");
					PartialllyUpdateableResource partialllyUpdateableResource = (PartialllyUpdateableResource) replacee;
					partialllyUpdateableResource.replacePartialContent(range, request.getInputStream());
				} else if (replacee instanceof GetableResource) {
					log.debug("doing partial put on a GetableResource");
					File tempFile = File.createTempFile("milton-partial", null);
					RandomAccessFile randomAccessFile = null;

					// The new length of the resource
					long length;
					try {
						randomAccessFile = new RandomAccessFile(tempFile, "rw");
						RandomFileOutputStream tempOut = new RandomFileOutputStream(tempFile);
						GetableResource gr = (GetableResource) replacee;
						// Update the content with the supplied partial content, and get the result as an inputstream
						gr.sendContent(tempOut, null, null, null);

						// Calculate new length, if the partial put is extending it
						length = randomAccessFile.length();
						if (range.getFinish() + 1 > length) {
							length = range.getFinish() + 1;
						}

						randomAccessFile.setLength(length);
						randomAccessFile.seek(range.getStart());

						int numBytesRead;
						byte[] copyBuffer = new byte[1024];
						InputStream newContent = request.getInputStream();

						while ((numBytesRead = newContent.read(copyBuffer)) != -1) {
							randomAccessFile.write(copyBuffer, 0, numBytesRead);
						}
					} finally {
						FileUtils.close(randomAccessFile);
					}

					InputStream updatedContent = new FileInputStream(tempFile);
					BufferedInputStream bufin = new BufferedInputStream(updatedContent);

					// Now, finally, we can just do a normal update
					replacee.replaceContent(bufin, length);
				} else {
					throw new BadRequestException(replacee, "Cant apply partial update. Resource does not support PartialllyUpdateableResource or GetableResource");
				}
			} else {
				// Not a partial update, but resource implements Replaceable, so give it the new data
				Long l = request.getContentLengthHeader();
				replacee.replaceContent(request.getInputStream(), l);
			}
		} catch (IOException ex) {
			log.warn("IOException reading input stream. Probably interrupted upload: " + ex.getMessage());
			return;
		}
		// Respond with a 204
		responseHandler.respondNoContent(replacee, response, request);

		log.debug("process: finished");
	}

	public void processExistingResource(HttpManager manager, Request request, Response response, Resource resource) throws NotAuthorizedException, BadRequestException, ConflictException, NotFoundException {
		String host = request.getHostHeader();
		String urlToCreateOrUpdate = HttpManager.decodeUrl(request.getAbsolutePath());
		log.debug("process request: host: " + host + " url: " + urlToCreateOrUpdate);

		Path path = Path.path(urlToCreateOrUpdate);
		urlToCreateOrUpdate = path.toString();

		Resource existingResource = manager.getResourceFactory().getResource(host, urlToCreateOrUpdate);
		ReplaceableResource replacee;

		if (existingResource != null) {
			//Make sure the parent collection is not locked by someone else
			if (handlerHelper.isLockedOut(request, existingResource)) {
				log.warn("resource is locked, but not by the current user");
				response.setStatus(Status.SC_LOCKED); //423
				return;
			}

		}
		if (existingResource instanceof ReplaceableResource) {
			replacee = (ReplaceableResource) existingResource;
		} else {
			replacee = null;
		}

		if (replacee != null) {
			processReplace(manager, request, response, (ReplaceableResource) existingResource);
		} else {
			// either no existing resource, or its not replaceable. check for folder
			String urlFolder = path.getParent().toString();
			String nameToCreate = path.getName();
			CollectionResource folderResource = findOrCreateFolders(manager, host, path.getParent(), request);
			if (folderResource != null) {
				if (log.isDebugEnabled()) {
					log.debug("found folder: " + urlFolder + " - " + folderResource.getClass());
				}
				if (folderResource instanceof PutableResource) {

					//Make sure the parent collection is not locked by someone else
					if (handlerHelper.isLockedOut(request, folderResource)) {
						response.setStatus(Status.SC_LOCKED); //423
						return;
					}

					PutableResource putableResource = (PutableResource) folderResource;
					processCreate(manager, request, response, putableResource, nameToCreate);
				} else {
					responseHandler.respondMethodNotImplemented(folderResource, response, request);
				}
			} else {
				responseHandler.respondNotFound(response, request);
			}
		}

	}

	private void respondLocked(Request request, Response response, Resource existingResource) {
		if (responseHandler instanceof WebDavResponseHandler) {
			WebDavResponseHandler rh = (WebDavResponseHandler) responseHandler;
			rh.respondLocked(request, response, existingResource);
		} else {
			response.setStatus(Status.SC_LOCKED); //423
		}
	}

	private void respondInsufficientStorage(Request request, Response response, StorageErrorReason storageErrorReason) {
		if (responseHandler instanceof WebDavResponseHandler) {
			WebDavResponseHandler rh = (WebDavResponseHandler) responseHandler;
			rh.respondInsufficientStorage(request, response, storageErrorReason);
		} else {
			response.setStatus(Status.SC_INSUFFICIENT_STORAGE);
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy