001/*
002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id$
020 */
021
022package org.nuxeo.ecm.platform.ui.web.util.files;
023
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.lang.reflect.Field;
028import java.lang.reflect.Method;
029import java.nio.file.Files;
030import java.nio.file.StandardCopyOption;
031
032import javax.servlet.http.Part;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.ecm.core.api.Blob;
037import org.nuxeo.ecm.core.api.Blobs;
038import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException;
039import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
040import org.nuxeo.runtime.api.Framework;
041
042public class FileUtils {
043
044    private static final Log log = LogFactory.getLog(FileUtils.class);
045
046    private FileUtils() {
047    }
048
049    /**
050     * Creates a serializable blob from a stream, with filename and mimetype detection.
051     * <p>
052     * Creates a serializable FileBlob which stores data in a temporary file on the hard disk.
053     *
054     * @param in the input stream holding data
055     * @param filename the file name. Will be set on the blob and will used for mimetype detection.
056     * @param mimeType the detected mimetype at upload. Can be null. Will be verified by the mimetype service.
057     */
058    public static Blob createSerializableBlob(InputStream in, String filename, String mimeType) {
059        Blob blob = null;
060        try {
061            // persisting the blob makes it possible to read the binary content
062            // of the request stream several times (mimetype sniffing, digest
063            // computation, core binary storage)
064            blob = Blobs.createBlob(in, mimeType);
065            // filename
066            if (filename != null) {
067                filename = getCleanFileName(filename);
068            }
069            blob.setFilename(filename);
070            // mimetype detection
071            MimetypeRegistry mimeService = Framework.getService(MimetypeRegistry.class);
072            String detectedMimeType = mimeService.getMimetypeFromFilenameAndBlobWithDefault(filename, blob, null);
073            if (detectedMimeType == null) {
074                if (mimeType != null) {
075                    detectedMimeType = mimeType;
076                } else {
077                    // default
078                    detectedMimeType = "application/octet-stream";
079                }
080            }
081            blob.setMimeType(detectedMimeType);
082        } catch (MimetypeDetectionException e) {
083            log.error("Could not fetch mimetype for file " + filename, e);
084        } catch (IOException e) {
085            log.error(e);
086        }
087        return blob;
088    }
089
090    /**
091     * Creates a Blob from a {@link Part}.
092     * <p>
093     * Attempts to capture the underlying temporary file, if one exists. This needs to use reflection to avoid having
094     * dependencies on the application server.
095     *
096     * @param part the servlet part
097     * @return the blob
098     * @since 7.2
099     */
100    public static Blob createBlob(Part part) throws IOException {
101        Blob blob = null;
102        try {
103            // part : org.apache.catalina.core.ApplicationPart
104            // part.fileItem : org.apache.tomcat.util.http.fileupload.disk.DiskFileItem
105            // part.fileItem.isInMemory() : false if on disk
106            // part.fileItem.getStoreLocation() : java.io.File
107            Field fileItemField = part.getClass().getDeclaredField("fileItem");
108            fileItemField.setAccessible(true);
109            Object fileItem = fileItemField.get(part);
110            if (fileItem != null) {
111                Method isInMemoryMethod = fileItem.getClass().getDeclaredMethod("isInMemory");
112                boolean inMemory = ((Boolean) isInMemoryMethod.invoke(fileItem)).booleanValue();
113                if (!inMemory) {
114                    Method getStoreLocationMethod = fileItem.getClass().getDeclaredMethod("getStoreLocation");
115                    File file = (File) getStoreLocationMethod.invoke(fileItem);
116                    if (file != null) {
117                        // move the file to a temporary blob we own
118                        blob = Blobs.createBlobWithExtension(null);
119                        Files.move(file.toPath(), blob.getFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
120                    }
121                }
122            }
123        } catch (ReflectiveOperationException | SecurityException | IllegalArgumentException e) {
124            // unknown Part implementation
125        }
126        if (blob == null) {
127            // if we couldn't get to the file, use the InputStream
128            blob = Blobs.createBlob(part.getInputStream());
129        }
130        blob.setMimeType(part.getContentType());
131        blob.setFilename(retrieveFilename(part));
132        return blob;
133    }
134
135    /**
136     * Helper method to retrieve filename from part, waiting for servlet-api related improvements.
137     * <p>
138     * Filename is cleaned before being returned.
139     *
140     * @see #getCleanFileName(String)
141     * @since 7.1
142     */
143    public static String retrieveFilename(Part part) {
144        for (String cd : part.getHeader("content-disposition").split(";")) {
145            if (cd.trim().startsWith("filename")) {
146                String filename = cd.substring(cd.indexOf('=') + 1).trim().replace("\"", "");
147                return getCleanFileName(filename);
148            }
149        }
150        return null;
151    }
152
153    /**
154     * Returns a clean filename, stripping upload path on client side.
155     * <p>
156     * Fixes NXP-544
157     */
158    public static String getCleanFileName(String filename) {
159        String res = null;
160        int lastWinSeparator = filename.lastIndexOf('\\');
161        int lastUnixSeparator = filename.lastIndexOf('/');
162        int lastSeparator = Math.max(lastWinSeparator, lastUnixSeparator);
163        if (lastSeparator != -1) {
164            res = filename.substring(lastSeparator + 1, filename.length());
165        } else {
166            res = filename;
167        }
168        return res;
169    }
170
171    public static void configureFileBlob(Blob blob) {
172        // mimetype detection
173        MimetypeRegistry mimeService = Framework.getService(MimetypeRegistry.class);
174        String detectedMimeType;
175        try {
176            detectedMimeType = mimeService.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob, null);
177        } catch (MimetypeDetectionException e) {
178            log.error("could not fetch mimetype for file " + blob.getFilename(), e);
179            return;
180        }
181        if (detectedMimeType == null) {
182            if (blob.getMimeType() != null) {
183                return;
184            }
185            // default
186            detectedMimeType = "application/octet-stream";
187        }
188        blob.setMimeType(detectedMimeType);
189        return;
190    }
191
192}