package org.dcache.webdav; import com.google.common.collect.ImmutableMap; import io.milton.http.Auth; import io.milton.http.Range; import io.milton.http.Request; import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.ConflictException; import io.milton.http.exceptions.NotAuthorizedException; import io.milton.property.PropertySource.PropertyMetaData; import io.milton.property.PropertySource.PropertySetException; import io.milton.resource.DeletableResource; import io.milton.resource.GetableResource; import io.milton.resource.MultiNamespaceCustomPropertyResource; import io.milton.servlet.ServletRequest; import io.milton.servlet.ServletResponse; import org.eclipse.jetty.io.EofException; import javax.xml.namespace.QName; import java.io.IOException; import java.io.OutputStream; import java.net.FileNameMap; import java.net.URISyntaxException; import java.net.URLConnection; import java.util.List; import java.util.Map; import diskCacheV111.util.AccessLatency; import diskCacheV111.util.CacheException; import diskCacheV111.util.FileNotFoundCacheException; import diskCacheV111.util.FsPath; import diskCacheV111.util.NotInTrashCacheException; import diskCacheV111.util.PermissionDeniedCacheException; import diskCacheV111.util.RetentionPolicy; import diskCacheV111.vehicles.HttpProtocolInfo; import org.dcache.vehicles.FileAttributes; import static io.milton.property.PropertySource.PropertyAccessibility.READ_ONLY; import static org.dcache.util.Checksums.TO_RFC3230; /** * Exposes regular dCache files as resources in the Milton WebDAV * framework. */ public class DcacheFileResource extends DcacheResource implements GetableResource, DeletableResource, MultiNamespaceCustomPropertyResource { private static final FileNameMap MIME_TYPE_MAP = URLConnection.getFileNameMap(); private static final String DCACHE_NAMESPACE_URI = "http://www.dcache.org/2013/webdav"; private static final String PARAM_ACTION = "action"; // We use the SRM 2.2 WSDL's TargetNamespace for the WebDAV properties // associated with SRM concepts. private static final String SRM_NAMESPACE_URI = "http://srm.lbl.gov/StorageResourceManager"; /* * Our dCache WebDAV properties. */ private static final String PROPERTY_CHECKSUMS = "Checksums"; /* * Our SRM WebDAV properties. */ private static final String PROPERTY_ACCESS_LATENCY = "AccessLatency"; private static final String PROPERTY_RETENTION_POLICY = "RetentionPolicy"; private static final String PROPERTY_FILE_LOCALITY = "FileLocality"; private static final ImmutableMap<QName,PropertyMetaData> PROPERTY_METADATA = new ImmutableMap.Builder<QName,PropertyMetaData>() .put(new QName(SRM_NAMESPACE_URI, PROPERTY_ACCESS_LATENCY), new PropertyMetaData(READ_ONLY, AccessLatency.class)) .put(new QName(SRM_NAMESPACE_URI, PROPERTY_RETENTION_POLICY), new PropertyMetaData(READ_ONLY, RetentionPolicy.class)) .put(new QName(DCACHE_NAMESPACE_URI, PROPERTY_CHECKSUMS), new PropertyMetaData(READ_ONLY, String.class)) .put(new QName(SRM_NAMESPACE_URI, PROPERTY_FILE_LOCALITY), new PropertyMetaData(READ_ONLY, String.class)) .build(); public DcacheFileResource(DcacheResourceFactory factory, FsPath path, FileAttributes attributes) { super(factory, path, attributes); } @Override public void sendContent(OutputStream out, Range range, Map<String,String> params, String contentType) throws IOException, NotAuthorizedException { /** * We set the Content-Disposition to reflect the users choice whether * or not to download the file. This is crazy, but necessary until * HTML supports some mechanism to do this purely in the browser. The * 'download' attribute for the 'a' tag seems promising; but, adoption * is low: * * http://caniuse.com/#feat=download * * As of 07-2014, above website estimates 54% support. Until IE * supports the attribute, we can't relying on it. */ ServletResponse.getResponse().addHeader("Content-Disposition", dispositionFor(params.get(PARAM_ACTION)).toString().toLowerCase()); try { _factory.readFile(_path, _attributes.getPnfsId(), out, range); } catch (EofException e) { // Milton reacts badly to receiving any IOException and wraps the // IOException in a RuntimeException. Here, we translate this to // an internal error exception, although this shouldn't matter as // the client has already disconnected. throw new WebDavException("Failed to send entity: client closed connection", e, this); } catch (PermissionDeniedCacheException e) { throw WebDavExceptions.permissionDenied(this); } catch (FileNotFoundCacheException | NotInTrashCacheException e) { throw new ForbiddenException(e.getMessage(), e, this); } catch (CacheException e) { throw new WebDavException(e.getMessage(), e, this); } catch (InterruptedException e) { throw new WebDavException("Transfer was interrupted", e, this); } catch (URISyntaxException e) { throw new WebDavException("Invalid request URI: " + e.getMessage(), e, this); } } @Override public Long getMaxAgeSeconds(Auth auth) { return null; } @Override public String getContentType(String accepts) { return MIME_TYPE_MAP.getContentTypeFor(_path.toString()); } @Override public Long getContentLength() { return _attributes.getSizeIfPresent().orNull(); } public static HttpProtocolInfo.Disposition dispositionFor(String action) { if (action != null) { switch (action) { case "download": return HttpProtocolInfo.Disposition.ATTACHMENT; case "show": return HttpProtocolInfo.Disposition.INLINE; } } return HttpProtocolInfo.Disposition.ATTACHMENT; } @Override public String checkRedirect(Request request) { try { if (_factory.shouldRedirect(request)) { return _factory.getReadUrl(_path, _attributes.getPnfsId(), dispositionFor(request.getParams().get(PARAM_ACTION))); } return null; } catch (PermissionDeniedCacheException e) { throw WebDavExceptions.permissionDenied(e.getMessage(), e, this); } catch (CacheException | InterruptedException e) { throw new WebDavException(e.getMessage(), e, this); } catch (URISyntaxException e) { throw new WebDavException("Invalid request URI: " + e.getMessage(), e, this); } } @Override public void delete() throws NotAuthorizedException, ConflictException, BadRequestException { try { _factory.deleteFile(_attributes, _path); } catch (PermissionDeniedCacheException e) { throw WebDavExceptions.permissionDenied(this); } catch (CacheException e) { throw new WebDavException(e.getMessage(), e, this); } } public String getRfc3230Digest() { return _attributes.getChecksumsIfPresent().transform(TO_RFC3230).or(""); } @Override public Object getProperty(QName qname) { switch (qname.getNamespaceURI()) { case DCACHE_NAMESPACE_URI: return getDcacheProperty(qname.getLocalPart()); case SRM_NAMESPACE_URI: return getSrmProperty(qname.getLocalPart()); } // Milton filters out unknown properties by checking with the // PropertyMetaData, so if we get here then it's a bug. throw new RuntimeException("unknown property " + qname); } private Object getDcacheProperty(String localPart) { switch(localPart) { case PROPERTY_CHECKSUMS: return _attributes.getChecksumsIfPresent().transform(TO_RFC3230).orNull(); } throw new RuntimeException("unknown dCache property " + localPart); } private Object getSrmProperty(String localPart) { switch(localPart) { case PROPERTY_ACCESS_LATENCY: return _attributes.getAccessLatencyIfPresent().orNull(); case PROPERTY_RETENTION_POLICY: return _attributes.getRetentionPolicyIfPresent().orNull(); case PROPERTY_FILE_LOCALITY: String clientIP = ServletRequest.getRequest().getRemoteAddr(); return _factory.calculateLocality(_attributes, clientIP).name(); } throw new RuntimeException("unknown SRM property " + localPart); } @Override public void setProperty(QName qname, Object o) throws PropertySetException, NotAuthorizedException { // Handle any updates here. // We should not see any read-only or unknown properties as Milton // discovers them from PropertyMetaData and filters out any attempt by // end-users. throw new RuntimeException("Attempt to update " + (PROPERTY_METADATA.containsKey(qname) ? "read-only" : "unknown") + "property " + qname); } @Override public PropertyMetaData getPropertyMetaData(QName qname) { // Milton accepts null and PropertyMetaData.UNKNOWN to mean the // property is unknown. return PROPERTY_METADATA.get(qname); } @Override public List<QName> getAllPropertyNames() { return PROPERTY_METADATA.keySet().asList(); } }