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     * @return a file blob
148     * @since 7.2
149     */
150    public FileBlob(String ext) throws IOException {
151        isTemporary = true;
152        file = Framework.createTempFile("nxblob-", ext);
153        Framework.trackFile(file, file);
154        filename = file.getName();
155    }
156
157    @Override
158    public File getFile() {
159        return file;
160    }
161
162    @Override
163    public long getLength() {
164        return file.length();
165    }
166
167    @Override
168    public InputStream getStream() throws IOException {
169        return new FileInputStream(file);
170    }
171
172    /**
173     * Checks whether this {@link FileBlob} is backed by a temporary file.
174     *
175     * @since 7.2
176     */
177    public boolean isTemporary() {
178        return isTemporary;
179    }
180
181    /**
182     * Moves this blob's temporary file to a new non-temporary location.
183     * <p>
184     * The move is done as atomically as possible.
185     *
186     * @since 7.2
187     */
188    public void moveTo(File dest) throws IOException {
189        if (!isTemporary) {
190            throw new IOException("Cannot move non-temporary file: " + file);
191        }
192        Path path = file.toPath();
193        Path destPath = dest.toPath();
194        try {
195            Files.move(path, destPath, ATOMIC_MOVE);
196            file = dest;
197        } catch (AtomicMoveNotSupportedException e) {
198            // Do a copy through a tmp file on the same filesystem then atomic rename
199            Path tmp = Files.createTempFile(destPath.getParent(), null, null);
200            try {
201                Files.copy(path, tmp, REPLACE_EXISTING);
202                Files.delete(path);
203                Files.move(tmp, destPath, ATOMIC_MOVE);
204                file = dest;
205            } catch (IOException ioe) {
206                // don't leave tmp file in case of error
207                Files.deleteIfExists(tmp);
208                throw ioe;
209            }
210        }
211        isTemporary = false;
212    }
213
214}