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