/**
* Copyright (C) 2010-2017 Structr GmbH
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.web.common;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.activation.MimetypesFileTypeMap;
import net.sf.jmimemagic.Magic;
import net.sf.jmimemagic.MagicException;
import net.sf.jmimemagic.MagicMatch;
import net.sf.jmimemagic.MagicMatchNotFoundException;
import net.sf.jmimemagic.MagicParseException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.config.Settings;
import org.structr.common.PathHelper;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.LinkedTreeNode;
import org.structr.core.property.PropertyMap;
import org.structr.util.Base64;
import org.structr.web.entity.AbstractFile;
import org.structr.web.entity.FileBase;
import org.structr.web.entity.Folder;
//~--- classes ----------------------------------------------------------------
/**
* File utility class.
*
*
*/
public class FileHelper {
private static final String UNKNOWN_MIME_TYPE = "application/octet-stream";
private static final Logger logger = LoggerFactory.getLogger(FileHelper.class.getName());
private static final MimetypesFileTypeMap mimeTypeMap = new MimetypesFileTypeMap(FileHelper.class.getResourceAsStream("/mime.types"));
//~--- methods --------------------------------------------------------
/**
* Transform an existing file into the target class.
*
* @param <T>
* @param securityContext
* @param uuid
* @param fileType
* @return transformed file
* @throws FrameworkException
* @throws IOException
*/
public static <T extends org.structr.web.entity.FileBase> T transformFile(final SecurityContext securityContext, final String uuid, final Class<T> fileType) throws FrameworkException, IOException {
AbstractFile existingFile = getFileByUuid(securityContext, uuid);
if (existingFile != null) {
existingFile.unlockSystemPropertiesOnce();
existingFile.setProperties(securityContext, new PropertyMap(AbstractNode.type, fileType == null ? org.structr.dynamic.File.class.getSimpleName() : fileType.getSimpleName()));
existingFile = getFileByUuid(securityContext, uuid);
return (T)(fileType != null ? fileType.cast(existingFile) : (org.structr.dynamic.File) existingFile);
}
return null;
}
/**
* Create a new image node from image data encoded in base64 format.
*
* If the given string is an uuid of an existing file, transform it into
* the target class.
*
* @param <T>
* @param securityContext
* @param rawData
* @param t defaults to File.class if null
* @return file
* @throws FrameworkException
* @throws IOException
*/
public static <T extends org.structr.web.entity.FileBase> T createFileBase64(final SecurityContext securityContext, final String rawData, final Class<T> t) throws FrameworkException, IOException {
Base64URIData uriData = new Base64URIData(rawData);
return createFile(securityContext, uriData.getBinaryData(), uriData.getContentType(), t);
}
/**
* Create a new file node from the given input stream
*
* @param <T>
* @param securityContext
* @param fileStream
* @param contentType
* @param fileType defaults to File.class if null
* @param name
* @return file
* @throws FrameworkException
* @throws IOException
*/
public static <T extends org.structr.web.entity.FileBase> T createFile(final SecurityContext securityContext, final InputStream fileStream, final String contentType, final Class<T> fileType, final String name)
throws FrameworkException, IOException {
final byte[] data = IOUtils.toByteArray(fileStream);
return createFile(securityContext, data, contentType, fileType, name);
}
/**
* Create a new file node from the given byte array
*
* @param <T>
* @param securityContext
* @param fileData
* @param contentType if null, try to auto-detect content type
* @param t
* @param name
* @return file
* @throws FrameworkException
* @throws IOException
*/
public static <T extends org.structr.web.entity.FileBase> T createFile(final SecurityContext securityContext, final byte[] fileData, final String contentType, final Class<T> t, final String name)
throws FrameworkException, IOException {
PropertyMap props = new PropertyMap();
props.put(AbstractNode.name, name);
T newFile = (T) StructrApp.getInstance(securityContext).create(t, props);
setFileData(newFile, fileData, contentType);
return newFile;
}
/**
* Create a new file node from the given byte array
*
* @param <T>
* @param securityContext
* @param fileData
* @param contentType
* @param t defaults to File.class if null
* @return file
* @throws FrameworkException
* @throws IOException
*/
public static <T extends org.structr.web.entity.FileBase> T createFile(final SecurityContext securityContext, final byte[] fileData, final String contentType, final Class<T> t)
throws FrameworkException, IOException {
return createFile(securityContext, fileData, contentType, t, null);
}
/**
* Decodes base64-encoded raw data into binary data and writes it to the
* given file.
*
* @param file
* @param rawData
* @throws FrameworkException
* @throws IOException
*/
public static void decodeAndSetFileData(final org.structr.dynamic.File file, final String rawData) throws FrameworkException, IOException {
Base64URIData uriData = new Base64URIData(rawData);
setFileData(file, uriData.getBinaryData(), uriData.getContentType());
}
/**
* Write image data to the given file node and set checksum and size.
*
* @param file
* @param fileData
* @param contentType if null, try to auto-detect content type
* @throws FrameworkException
* @throws IOException
*/
public static void setFileData(final FileBase file, final byte[] fileData, final String contentType) throws FrameworkException, IOException {
FileHelper.writeToFile(file, fileData);
final PropertyMap map = new PropertyMap();
map.put(FileBase.contentType, contentType != null ? contentType : getContentMimeType(file));
map.put(FileBase.checksum, FileHelper.getChecksum(file));
map.put(FileBase.size, FileHelper.getSize(file));
map.put(FileBase.version, 1);
file.setProperties(file.getSecurityContext(), map);
}
/**
* Update checksum content type and size of the given file
*
* @param file the file
* @throws FrameworkException
* @throws IOException
*/
public static void updateMetadata(final FileBase file) throws FrameworkException, IOException {
final PropertyMap map = new PropertyMap();
map.put(FileBase.contentType, getContentMimeType(file));
map.put(FileBase.checksum, FileHelper.getChecksum(file));
map.put(FileBase.size, FileHelper.getSize(file));
file.setProperties(file.getSecurityContext(), map);
}
//~--- get methods ----------------------------------------------------
public static String getBase64String(final FileBase file) {
try {
final InputStream is = file.getInputStream();
if (is != null) {
return Base64.encodeToString(IOUtils.toByteArray(is), false);
}
} catch (IOException ex) {
logger.error("Could not get base64 string from file ", ex);
}
return null;
}
//~--- inner classes --------------------------------------------------
public static class Base64URIData {
private String contentType = null;
private String data = null;
//~--- constructors -------------------------------------------
public Base64URIData(final String rawData) {
if (rawData.contains(",")) {
String[] parts = StringUtils.split(rawData, ",");
if (parts.length == 2) {
contentType = StringUtils.substringBetween(parts[0], "data:", ";base64");
data = parts[1];
}
} else {
data = rawData;
}
}
//~--- get methods --------------------------------------------
public String getContentType() {
return contentType;
}
public String getData() {
return data;
}
public byte[] getBinaryData() {
return Base64.decode(data);
}
}
/**
* Write binary data to a file and reference the file on disk at the
* given file node
*
* @param fileNode
* @param inStream
* @throws FrameworkException
* @throws IOException
*/
public static void writeToFile(final org.structr.dynamic.File fileNode, final InputStream inStream) throws FrameworkException, IOException {
writeToFile(fileNode, IOUtils.toByteArray(inStream));
}
/**
* Write binary data to a file and reference the file on disk at the
* given file node
*
* @param fileNode
* @param data
* @throws FrameworkException
* @throws IOException
* @return the file on disk
*/
public static File writeToFile(final FileBase fileNode, final byte[] data) throws FrameworkException, IOException {
final PropertyMap properties = new PropertyMap();
String id = fileNode.getProperty(GraphObject.id);
if (id == null) {
final String newUuid = UUID.randomUUID().toString().replaceAll("[\\-]+", "");
id = newUuid;
fileNode.unlockSystemPropertiesOnce();
properties.put(GraphObject.id, newUuid);
}
properties.put(FileBase.relativeFilePath, FileBase.getDirectoryPath(id) + "/" + id);
fileNode.unlockSystemPropertiesOnce();
fileNode.setProperties(fileNode.getSecurityContext(), properties);
final String filesPath = Settings.FilesPath.getValue();
final java.io.File fileOnDisk = new java.io.File(filesPath + "/" + fileNode.getRelativeFilePath());
fileOnDisk.getParentFile().mkdirs();
FileUtils.writeByteArrayToFile(fileOnDisk, data);
return fileOnDisk;
}
//~--- get methods ----------------------------------------------------
public static File getFile(final FileBase file) {
return new java.io.File(getFilePath(file.getRelativeFilePath()));
}
public static Path getPath(final FileBase file) {
return Paths.get(getFilePath(file.getRelativeFilePath()));
}
/**
* Return mime type of given file
*
* @param file
* @return content type
* @throws java.io.IOException
*/
public static String getContentMimeType(final FileBase file) throws IOException {
return getContentMimeType(file.getFileOnDisk(), file.getProperty(AbstractNode.name));
}
/**
* Return mime type of given file
*
* @param file
* @param name
* @return content type
* @throws java.io.IOException
*/
public static String getContentMimeType(final java.io.File file, final String name) throws IOException {
String mimeType;
// try name first, if not null
if (name != null) {
mimeType = mimeTypeMap.getContentType(name);
if (mimeType != null && !UNKNOWN_MIME_TYPE.equals(mimeType)) {
return mimeType;
}
}
// then file content
mimeType = Files.probeContentType(file.toPath());
if (mimeType != null && !UNKNOWN_MIME_TYPE.equals(mimeType)) {
return mimeType;
}
// fallback: jmimemagic
try {
final MagicMatch match = Magic.getMagicMatch(file, false, true);
if (match != null) {
return match.getMimeType();
}
} catch (MagicParseException | MagicMatchNotFoundException | MagicException ignore) {
// mlogger.warn("", ex);
}
// no success :(
return UNKNOWN_MIME_TYPE;
}
/**
* Calculate CRC32 checksum of given file
*
* @param file
* @return checksum
*/
public static Long getChecksum(final FileBase file) {
String relativeFilePath = file.getRelativeFilePath();
if (relativeFilePath != null) {
String filePath = getFilePath(relativeFilePath);
try {
return getChecksum(new java.io.File(filePath));
} catch (IOException ex) {
logger.warn("Could not calculate checksum of file {}: {}", filePath, ex);
}
}
return null;
}
public static Long getChecksum(final java.io.File fileOnDisk) throws IOException {
return FileUtils.checksumCRC32(fileOnDisk);
}
/**
* Return size of file on disk, or -1 if not possible
*
* @param file
* @return size
*/
public static long getSize(final FileBase file) {
String path = file.getRelativeFilePath();
if (path != null) {
String filePath = getFilePath(path);
try {
java.io.File fileOnDisk = new java.io.File(filePath);
long fileSize = fileOnDisk.length();
logger.debug("File size of node {} ({}): {}", new Object[]{file.getUuid(), filePath, fileSize});
return fileSize;
} catch (Exception ex) {
logger.warn("Could not calculate file size of file {}: {}", filePath, ex);
}
}
return -1;
}
/**
* Find a file by its absolute ancestor path.
*
* File may not be hidden or deleted.
*
* @param securityContext
* @param absolutePath
* @return file
*/
public static AbstractFile getFileByAbsolutePath(final SecurityContext securityContext, final String absolutePath) {
try {
return StructrApp.getInstance(securityContext).nodeQuery(AbstractFile.class).and(AbstractFile.path, absolutePath).getFirst();
} catch (FrameworkException ex) {
logger.warn("File not found: {}", new Object[] { absolutePath });
}
return null;
}
public static AbstractFile getFileByUuid(final SecurityContext securityContext, final String uuid) {
logger.debug("Search for file with uuid: {}", uuid);
try {
return StructrApp.getInstance(securityContext).get(AbstractFile.class, uuid);
} catch (FrameworkException fex) {
logger.warn("Unable to find a file by UUID {}: {}", uuid, fex.getMessage());
}
return null;
}
public static AbstractFile getFirstFileByName(final SecurityContext securityContext, final String name) {
logger.debug("Search for file with name: {}", name);
try {
return StructrApp.getInstance(securityContext).nodeQuery(AbstractFile.class).andName(name).getFirst();
} catch (FrameworkException fex) {
logger.warn("Unable to find a file for name {}: {}", name, fex.getMessage());
}
return null;
}
/**
* Find the first file with given name on root level (without parent folder).
*
* @param securityContext
* @param name
* @return file
*/
public static AbstractFile getFirstRootFileByName(final SecurityContext securityContext, final String name) {
logger.debug("Search for file with name: {}", name);
try {
final List<AbstractFile> files = StructrApp.getInstance(securityContext).nodeQuery(AbstractFile.class).andName(name).getAsList();
for (final AbstractFile file : files) {
if (file.getProperty(AbstractFile.parent) == null) {
return file;
}
}
} catch (FrameworkException fex) {
logger.warn("Unable to find a file for name {}: {}", name, fex.getMessage());
}
return null;
}
/**
* Return the virtual folder path of any
* {@link File} or
* {@link org.structr.web.entity.Folder}
*
* @param file
* @return path
*/
public static String getFolderPath(final AbstractFile file) {
LinkedTreeNode parentFolder = file.getProperty(AbstractFile.parent);
String folderPath = file.getProperty(AbstractFile.name);
if (folderPath == null) {
folderPath = file.getProperty(GraphObject.id);
}
while (parentFolder != null) {
folderPath = parentFolder.getName().concat("/").concat(folderPath);
parentFolder = parentFolder.getProperty(AbstractFile.parent);
}
return "/".concat(folderPath);
}
public static String getFilePath(final String... pathParts) {
final String filePath = Settings.FilesPath.getValue();
final StringBuilder returnPath = new StringBuilder();
returnPath.append(filePath);
returnPath.append(filePath.endsWith("/") ? "" : "/");
for (String pathPart : pathParts) {
returnPath.append(pathPart);
}
return returnPath.toString();
}
/**
* Create one folder per path item and return the last folder.
*
* F.e.: /a/b/c => Folder["name":"a"] --HAS_CHILD--> Folder["name":"b"]
* --HAS_CHILD--> Folder["name":"c"], returns Folder["name":"c"]
*
* @param securityContext
* @param path
* @return folder
* @throws FrameworkException
*/
public static Folder createFolderPath(final SecurityContext securityContext, final String path) throws FrameworkException {
final App app = StructrApp.getInstance(securityContext);
if (path == null) {
return null;
}
Folder folder = (Folder) FileHelper.getFileByAbsolutePath(securityContext, path);
if (folder != null) {
return folder;
}
String[] parts = PathHelper.getParts(path);
String partialPath = "";
for (String part : parts) {
// ignore ".." and "." in paths
if ("..".equals(part) || ".".equals(part)) {
continue;
}
Folder parent = folder;
partialPath += PathHelper.PATH_SEP + part;
folder = (Folder) FileHelper.getFileByAbsolutePath(securityContext, partialPath);
if (folder == null) {
folder = app.create(Folder.class, part);
}
if (parent != null) {
folder.setProperties(securityContext, new PropertyMap(AbstractFile.parent, parent));
}
}
return folder;
}
public static String getDateString() {
return new SimpleDateFormat("yyyy-MM-dd-HHmmss").format(new Date());
}
}