001/*
002 * Copyright (c) 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Bogdan Stefanescu
011 *     Florent Guillaume
012 */
013package org.nuxeo.ecm.core.api.impl.blob;
014
015import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
016import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.io.Serializable;
025import java.nio.file.AtomicMoveNotSupportedException;
026import java.nio.file.Files;
027import java.nio.file.Path;
028
029import org.apache.commons.io.IOUtils;
030import org.nuxeo.ecm.core.api.Blob;
031import org.nuxeo.runtime.api.Framework;
032
033/**
034 * A {@link Blob} backed by a {@link File}.
035 * <p>
036 * The backing file may be in a temporary location, which is the case if this {@link FileBlob} was constructed from an
037 * {@link InputStream} or from a file which was explicitly marked as temporary. In this case, the file may be renamed,
038 * or the file location may be changed to a non-temporary one.
039 */
040public class FileBlob extends AbstractBlob implements Serializable {
041
042    private static final long serialVersionUID = 1L;
043
044    protected File file;
045
046    protected boolean isTemporary;
047
048    public FileBlob(File file) {
049        this(file, null, null, null, null);
050    }
051
052    public FileBlob(File file, String mimeType) {
053        this(file, mimeType, null, null, null);
054    }
055
056    public FileBlob(File file, String mimeType, String encoding) {
057        this(file, mimeType, encoding, null, null);
058    }
059
060    public FileBlob(File file, String mimeType, String encoding, String filename, String digest) {
061        if (file == null) {
062            throw new NullPointerException("null file");
063        }
064        this.file = file;
065        this.mimeType = mimeType;
066        this.encoding = encoding;
067        this.digest = digest;
068        this.filename = filename != null ? filename : file.getName();
069    }
070
071    /**
072     * Creates a {@link FileBlob} from an {@link InputStream}, by saving it to a temporary file.
073     * <p>
074     * The input stream is closed.
075     *
076     * @param in the input stream, which is closed after use
077     */
078    public FileBlob(InputStream in) throws IOException {
079        this(in, null, null);
080    }
081
082    /**
083     * Creates a {@link FileBlob} from an {@link InputStream}, by saving it to a temporary file.
084     * <p>
085     * The input stream is closed.
086     *
087     * @param in the input stream, which is closed after use
088     * @param mimeType the MIME type
089     */
090    public FileBlob(InputStream in, String mimeType) throws IOException {
091        this(in, mimeType, null);
092    }
093
094    /**
095     * Creates a {@link FileBlob} from an {@link InputStream}, by saving it to a temporary file.
096     * <p>
097     * The input stream is closed.
098     *
099     * @param in the input stream, which is closed after use
100     * @param mimeType the MIME type
101     * @param encoding the encoding
102     */
103    public FileBlob(InputStream in, String mimeType, String encoding) throws IOException {
104        this(in, mimeType, encoding, null);
105    }
106
107    /**
108     * Creates a {@link FileBlob} from an {@link InputStream}, by saving it to a temporary file.
109     * <p>
110     * The input stream is closed.
111     *
112     * @param in the input stream, which is closed after use
113     * @param mimeType the MIME type
114     * @param encoding the encoding
115     * @param tmpDir the temporary directory for file creation
116     */
117    public FileBlob(InputStream in, String mimeType, String encoding, File tmpDir) throws IOException {
118        if (in == null) {
119            throw new NullPointerException("null inputstream");
120        }
121        this.mimeType = mimeType;
122        this.encoding = encoding;
123        isTemporary = true;
124        try {
125            file = File.createTempFile("nxblob-", ".tmp", tmpDir);
126            Framework.trackFile(file, file);
127            filename = file.getName();
128            try (OutputStream out = new FileOutputStream(file)) {
129                IOUtils.copy(in, out);
130            }
131        } finally {
132            IOUtils.closeQuietly(in);
133        }
134    }
135
136    /**
137     * Creates a {@link FileBlob} with an empty temporary file with the given extension.
138     *
139     * @param ext the temporary file extension
140     * @return a file blob
141     * @since 7.2
142     */
143    public FileBlob(String ext) throws IOException {
144        isTemporary = true;
145        file = File.createTempFile("nxblob-", ext);
146        Framework.trackFile(file, file);
147        filename = file.getName();
148    }
149
150    @Override
151    public File getFile() {
152        return file;
153    }
154
155    @Override
156    public long getLength() {
157        return file.length();
158    }
159
160    @Override
161    public InputStream getStream() throws IOException {
162        return new FileInputStream(file);
163    }
164
165    /**
166     * Checks whether this {@link FileBlob} is backed by a temporary file.
167     *
168     * @since 7.2
169     */
170    public boolean isTemporary() {
171        return isTemporary;
172    }
173
174    /**
175     * Moves this blob's temporary file to a new non-temporary location.
176     * <p>
177     * The move is done as atomically as possible.
178     *
179     * @since 7.2
180     */
181    public void moveTo(File dest) throws IOException {
182        if (!isTemporary) {
183            throw new IOException("Cannot move non-temporary file: " + file);
184        }
185        Path path = file.toPath();
186        Path destPath = dest.toPath();
187        try {
188            Files.move(path, destPath, ATOMIC_MOVE);
189            file = dest;
190        } catch (AtomicMoveNotSupportedException e) {
191            // Do a copy through a tmp file on the same filesystem then atomic rename
192            Path tmp = Files.createTempFile(destPath.getParent(), null, null);
193            try {
194                Files.copy(path, tmp, REPLACE_EXISTING);
195                Files.delete(path);
196                Files.move(tmp, destPath, ATOMIC_MOVE);
197                file = dest;
198            } catch (IOException ioe) {
199                // don't leave tmp file in case of error
200                Files.deleteIfExists(tmp);
201                throw ioe;
202            }
203        }
204        isTemporary = false;
205    }
206
207}