001/* 002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Florent Guillaume 016 */ 017package org.nuxeo.ecm.core.blob; 018 019import java.io.FileNotFoundException; 020import java.io.IOException; 021import java.io.InputStream; 022import java.nio.file.Files; 023import java.nio.file.Path; 024import java.nio.file.Paths; 025import java.util.Map; 026 027import org.apache.commons.codec.digest.DigestUtils; 028import org.apache.commons.lang.StringUtils; 029import org.nuxeo.ecm.core.api.Blob; 030import org.nuxeo.ecm.core.api.NuxeoException; 031import org.nuxeo.ecm.core.blob.BlobManager.BlobInfo; 032import org.nuxeo.ecm.core.model.Document; 033 034/** 035 * Blob provider that can reference files on the filesystem. 036 * <p> 037 * This blob provider MUST be configured with a "root" property that specifies the minimum root path for all files: 038 * 039 * <pre> 040 * <code> 041 * <blobprovider name="myfsblobprovider"> 042 * <class>org.nuxeo.ecm.core.blob.FilesystemBlobProvider</class> 043 * <property name="root">/base/directory/for/files</property> 044 * </blobprovider> 045 * </code> 046 * </pre> 047 * <p> 048 * A root of {@code /} may be used to allow any path. 049 * <p> 050 * Blobs are constructed through {@link FilesystemBlobProvider#createBlob}. The constructed blob's key, which will be 051 * stored in the document database, contains a path relative to the root. 052 * 053 * @since 7.10 054 */ 055public class FilesystemBlobProvider extends AbstractBlobProvider { 056 057 public static final String ROOT_PROP = "root"; 058 059 /** The root ending with /, or an empty string. */ 060 protected String root; 061 062 @Override 063 public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { 064 super.initialize(blobProviderId, properties); 065 root = properties.get(ROOT_PROP); 066 if (StringUtils.isBlank(root)) { 067 throw new NuxeoException( 068 "Missing property '" + ROOT_PROP + "' for " + getClass().getSimpleName() + ": " + blobProviderId); 069 } 070 if ("/".equals(root)) { 071 root = ""; 072 } else if (!root.endsWith("/")) { 073 root = root + "/"; 074 } 075 } 076 077 @Override 078 public void close() { 079 } 080 081 @Override 082 public Blob readBlob(BlobInfo blobInfo) throws IOException { 083 return new SimpleManagedBlob(blobInfo); 084 } 085 086 @Override 087 public InputStream getStream(ManagedBlob blob) throws IOException { 088 String key = blob.getKey(); 089 // strip prefix 090 int colon = key.indexOf(':'); 091 if (colon >= 0 && key.substring(0, colon).equals(blobProviderId)) { 092 key = key.substring(colon + 1); 093 } 094 // final sanity checks 095 if (key.contains("..")) { 096 throw new FileNotFoundException("Illegal path: " + key); 097 } 098 return Files.newInputStream(Paths.get(root + key)); 099 } 100 101 @Override 102 public boolean supportsUserUpdate() { 103 return supportsUserUpdateDefaultFalse(); 104 } 105 106 @Override 107 public String writeBlob(Blob blob, Document doc) throws IOException { 108 throw new UnsupportedOperationException("Writing a blob is not supported"); 109 } 110 111 /** 112 * Creates a filesystem blob with the given information. 113 * <p> 114 * The passed {@link BlobInfo} contains information about the blob, and the key is a file path. 115 * 116 * @param blobInfo the blob info where the key is a file path 117 * @return the blob 118 */ 119 public ManagedBlob createBlob(BlobInfo blobInfo) throws IOException { 120 String filePath = blobInfo.key; 121 if (filePath.contains("..")) { 122 throw new FileNotFoundException("Illegal path: " + filePath); 123 } 124 if (!filePath.startsWith(root)) { 125 throw new FileNotFoundException("Path is not under configured root: " + filePath); 126 } 127 Path path = Paths.get(filePath); 128 if (!Files.exists(path)) { 129 throw new FileNotFoundException(filePath); 130 } 131 // dereference links 132 while (Files.isSymbolicLink(path)) { 133 // dereference if link 134 path = Files.readSymbolicLink(path); 135 if (!Files.exists(path)) { 136 throw new FileNotFoundException(filePath); 137 } 138 } 139 String relativePath = filePath.substring(root.length()); 140 long length = Files.size(path); 141 blobInfo = new BlobInfo(blobInfo); // copy 142 blobInfo.key = blobProviderId + ":" + relativePath; 143 blobInfo.length = Long.valueOf(length); 144 if (blobInfo.filename == null) { 145 blobInfo.filename = Paths.get(filePath).getFileName().toString(); 146 } 147 if (blobInfo.digest == null) { 148 try (InputStream in = Files.newInputStream(path)) { 149 blobInfo.digest = DigestUtils.md5Hex(in); 150 } 151 } 152 return new SimpleManagedBlob(blobInfo); 153 } 154 155}