/* * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Antoine Taillefer <ataillefer@nuxeo.com> */ package org.nuxeo.ecm.automation.server.jaxrs.batch; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.Blobs; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.transientstore.api.TransientStore; import org.nuxeo.runtime.api.Framework; /** * Represents a batch file backed by the {@link TransientStore}. * <p> * The file can be chunked or not. If it is chunked it references its chunks as {@link TransientStore} entry keys. * * @since 7.4 * @see Batch */ public class BatchFileEntry { protected static final Log log = LogFactory.getLog(BatchFileEntry.class); protected String key; protected Map<String, Serializable> params; protected Blob blob; protected Blob chunkedBlob; /** * Returns a file entry that holds the given blob, not chunked. */ public BatchFileEntry(String key, Blob blob) { this(key, false); this.blob = blob; } /** * Returns a file entry that references the file chunks. * * @see BatchChunkEntry */ public BatchFileEntry(String key, int chunkCount, String fileName, String mimeType, long fileSize) { this(key, true); params.put("chunkCount", String.valueOf(chunkCount)); if (!StringUtils.isEmpty(fileName)) { params.put("fileName", fileName); } if (!StringUtils.isEmpty(mimeType)) { params.put("mimeType", mimeType); } params.put("fileSize", String.valueOf(fileSize)); } /** * Returns a file entry that holds the given parameters. */ public BatchFileEntry(String key, Map<String, Serializable> params) { this.key = key; this.params = params; } protected BatchFileEntry(String key, boolean chunked) { this.key = key; params = new HashMap<>(); params.put(Batch.CHUNKED_PARAM_NAME, String.valueOf(chunked)); } public String getKey() { return key; } public Map<String, Serializable> getParams() { return params; } public boolean isChunked() { return Boolean.parseBoolean((String) params.get(Batch.CHUNKED_PARAM_NAME)); } public String getFileName() { if (isChunked()) { return (String) params.get("fileName"); } else { Blob blob = getBlob(); if (blob == null) { return null; } else { return blob.getFilename(); } } } public String getMimeType() { if (isChunked()) { return (String) params.get("mimeType"); } else { Blob blob = getBlob(); if (blob == null) { return null; } else { return blob.getMimeType(); } } } public long getFileSize() { if (isChunked()) { return Long.parseLong((String) params.get("fileSize")); } else { Blob blob = getBlob(); if (blob == null) { return -1; } else { return blob.getLength(); } } } public int getChunkCount() { if (!isChunked()) { throw new NuxeoException(String.format("Cannot get chunk count of file entry %s as it is not chunked", key)); } return Integer.parseInt((String) params.get("chunkCount")); } public Map<Integer, String> getChunks() { if (!isChunked()) { throw new NuxeoException(String.format("Cannot get chunks of file entry %s as it is not chunked", key)); } Map<Integer, String> chunks = new HashMap<>(); for (String param : params.keySet()) { if (NumberUtils.isDigits(param)) { chunks.put(Integer.parseInt(param), (String) params.get(param)); } } return chunks; } public List<Integer> getOrderedChunkIndexes() { if (!isChunked()) { throw new NuxeoException(String.format("Cannot get chunk indexes of file entry %s as it is not chunked", key)); } List<Integer> sortedChunkIndexes = new ArrayList<Integer>(getChunks().keySet()); Collections.sort(sortedChunkIndexes); return sortedChunkIndexes; } public Collection<String> getChunkEntryKeys() { if (!isChunked()) { throw new NuxeoException(String.format("Cannot get chunk entry keys of file entry %s as it is not chunked", key)); } return getChunks().values(); } public boolean isChunksCompleted() { return getChunks().size() == getChunkCount(); } public Blob getBlob() { if (isChunked()) { // First check if blob chunks have already been read and concatenated if (chunkedBlob != null) { return chunkedBlob; } File tmpChunkedFile = null; try { Map<Integer, String> chunks = getChunks(); int uploadedChunkCount = chunks.size(); int chunkCount = getChunkCount(); if (uploadedChunkCount != chunkCount) { log.warn(String.format( "Cannot get blob for file entry %s as there are only %d uploaded chunks out of %d.", key, uploadedChunkCount, chunkCount)); return null; } chunkedBlob = Blobs.createBlobWithExtension(null); // Temporary file made from concatenated chunks tmpChunkedFile = chunkedBlob.getFile(); BatchManager bm = Framework.getService(BatchManager.class); TransientStore ts = bm.getTransientStore(); // Sort chunk indexes and concatenate them to build the entire blob List<Integer> sortedChunkIndexes = getOrderedChunkIndexes(); for (int index : sortedChunkIndexes) { Blob chunk = getChunk(ts, chunks.get(index)); if (chunk != null) { transferTo(chunk, tmpChunkedFile); } } // Store tmpChunkedFile as a parameter for later deletion ts.putParameter(key, "tmpChunkedFilePath", tmpChunkedFile.getAbsolutePath()); chunkedBlob.setMimeType(getMimeType()); chunkedBlob.setFilename(getFileName()); return chunkedBlob; } catch (IOException ioe) { if (tmpChunkedFile != null && tmpChunkedFile.exists()) { tmpChunkedFile.delete(); } chunkedBlob = null; throw new NuxeoException(ioe); } } else { return blob; } } protected Blob getChunk(TransientStore ts, String key) { List<Blob> blobs = ts.getBlobs(key); if (CollectionUtils.isEmpty(blobs)) { return null; } return blobs.get(0); } /** * Appends the given blob to the given file. */ protected void transferTo(Blob blob, File file) throws IOException { try (OutputStream out = new FileOutputStream(file, true)) { try (InputStream in = blob.getStream()) { IOUtils.copy(in, out); } } } public String addChunk(int index, Blob blob) { if (!isChunked()) { throw new NuxeoException("Cannot add a chunk to a non chunked file entry."); } int chunkCount = getChunkCount(); if (index < 0) { throw new NuxeoException(String.format("Cannot add chunk with negative index %d.", index)); } if (index >= chunkCount) { throw new NuxeoException(String.format( "Cannot add chunk with index %d to file entry %s as chunk count is %d.", index, key, chunkCount)); } if (getChunks().containsKey(index)) { throw new NuxeoException(String.format( "Cannot add chunk with index %d to file entry %s as it already exists.", index, key)); } String chunkEntryKey = key + "_" + index; BatchManager bm = Framework.getService(BatchManager.class); TransientStore ts = bm.getTransientStore(); ts.putBlobs(chunkEntryKey, Collections.singletonList(blob)); ts.putParameter(key, String.valueOf(index), chunkEntryKey); return chunkEntryKey; } public void beforeRemove() { BatchManager bm = Framework.getService(BatchManager.class); String tmpChunkedFilePath = (String) bm.getTransientStore().getParameter(key, "tmpChunkedFilePath"); if (tmpChunkedFilePath != null) { File tmpChunkedFile = new File(tmpChunkedFilePath); if (tmpChunkedFile.exists()) { log.debug(String.format("Deleting temporary chunked file %s", tmpChunkedFilePath)); tmpChunkedFile.delete(); } } } }