/* * LinShare is an open source filesharing software, part of the LinPKI software * suite, developed by Linagora. * * Copyright (C) 2015-2016 LINAGORA * * This program 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, provided you comply with the Additional Terms applicable for * LinShare software by Linagora pursuant to Section 7 of the GNU Affero General * Public License, subsections (b), (c), and (e), pursuant to which you must * notably (i) retain the display of the “LinShare™” trademark/logo at the top * of the interface window, the display of the “You are using the Open Source * and free version of LinShare™, powered by Linagora © 2009–2015. Contribute to * Linshare R&D by subscribing to an Enterprise offer!” infobox and in the * e-mails sent with the Program, (ii) retain all hypertext links between * LinShare and linshare.org, between linagora.com and Linagora, and (iii) * refrain from infringing Linagora intellectual property rights over its * trademarks and commercial brands. Other Additional Terms apply, see * <http://www.linagora.com/licenses/> for more details. * * This program 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 and * its applicable Additional Terms for LinShare along with this program. If not, * see <http://www.gnu.org/licenses/> for the GNU Affero General Public License * version 3 and <http://www.linagora.com/licenses/> for the Additional Terms * applicable to LinShare software. */ package org.linagora.linshare.core.business.service.impl; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Calendar; import java.util.List; import java.util.Set; import javax.imageio.ImageIO; import org.bouncycastle.tsp.TSPException; import org.bouncycastle.tsp.TimeStampResponse; import org.linagora.LinThumbnail.FileResource; import org.linagora.LinThumbnail.FileResourceFactory; import org.linagora.LinThumbnail.utils.Constants; import org.linagora.LinThumbnail.utils.ImageUtils; import org.linagora.linshare.core.business.service.DocumentEntryBusinessService; import org.linagora.linshare.core.business.service.SignatureBusinessService; import org.linagora.linshare.core.business.service.UploadRequestEntryBusinessService; import org.linagora.linshare.core.dao.FileDataStore; import org.linagora.linshare.core.domain.constants.FileMetaDataKind; import org.linagora.linshare.core.domain.entities.Account; import org.linagora.linshare.core.domain.entities.Document; import org.linagora.linshare.core.domain.entities.DocumentEntry; import org.linagora.linshare.core.domain.entities.Entry; import org.linagora.linshare.core.domain.entities.ShareEntry; import org.linagora.linshare.core.domain.entities.Signature; import org.linagora.linshare.core.domain.entities.Thread; import org.linagora.linshare.core.domain.entities.ThreadEntry; import org.linagora.linshare.core.domain.entities.UploadRequestEntry; import org.linagora.linshare.core.domain.objects.FileMetaData; import org.linagora.linshare.core.exception.BusinessErrorCode; import org.linagora.linshare.core.exception.BusinessException; import org.linagora.linshare.core.exception.TechnicalErrorCode; import org.linagora.linshare.core.exception.TechnicalException; import org.linagora.linshare.core.repository.AccountRepository; import org.linagora.linshare.core.repository.DocumentEntryRepository; import org.linagora.linshare.core.repository.DocumentRepository; import org.linagora.linshare.core.repository.ThreadEntryRepository; import org.linagora.linshare.core.service.TimeStampingService; import org.linagora.linshare.core.utils.AESCrypt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DocumentEntryBusinessServiceImpl implements DocumentEntryBusinessService { private static final Logger logger = LoggerFactory.getLogger(DocumentEntryBusinessServiceImpl.class); private final FileDataStore fileDataStore; private final TimeStampingService timeStampingService; private final DocumentEntryRepository documentEntryRepository; private final ThreadEntryRepository threadEntryRepository; private final DocumentRepository documentRepository; private final AccountRepository<Account> accountRepository; private final SignatureBusinessService signatureBusinessService; private final UploadRequestEntryBusinessService uploadRequestEntryBusinessService; private final boolean thumbEnabled; private final boolean pdfThumbEnabled; private final boolean deduplication; public DocumentEntryBusinessServiceImpl( final FileDataStore fileSystemDao, final TimeStampingService timeStampingService, final DocumentEntryRepository documentEntryRepository, final DocumentRepository documentRepository, final AccountRepository<Account> accountRepository, final SignatureBusinessService signatureBusinessService, final ThreadEntryRepository threadEntryRepository, final UploadRequestEntryBusinessService uploadRequestEntryBusinessService, final boolean thumbEnabled, final boolean pdfThumbEnabled, final boolean deduplication) { super(); this.fileDataStore = fileSystemDao; this.timeStampingService = timeStampingService; this.documentEntryRepository = documentEntryRepository; this.documentRepository = documentRepository; this.accountRepository = accountRepository; this.signatureBusinessService = signatureBusinessService; this.threadEntryRepository = threadEntryRepository; this.uploadRequestEntryBusinessService = uploadRequestEntryBusinessService; this.thumbEnabled = thumbEnabled; this.pdfThumbEnabled = pdfThumbEnabled; this.deduplication = deduplication; } @Override public DocumentEntry createDocumentEntry(Account owner, File myFile, Long size, String fileName, String comment, Boolean checkIfIsCiphered, String timeStampingUrl, String mimeType, Calendar expirationDate, boolean isFromCmis, String metadata) throws BusinessException { // add an entry for the file in DB DocumentEntry entity = null; try { Document document = createDocument(owner, myFile, size, fileName, timeStampingUrl, mimeType); if (comment == null) comment = ""; DocumentEntry docEntry = new DocumentEntry(owner, fileName, comment, document); // We need to set an expiration date in case of file cleaner activation. docEntry.setExpirationDate(expirationDate); docEntry.setMetaData(metadata); //aes encrypt ? check headers if(checkIfIsCiphered) { docEntry.setCiphered(checkIfFileIsCiphered(fileName, myFile)); } docEntry.setCmisSync(isFromCmis); entity = documentEntryRepository.create(docEntry); owner.getEntries().add(entity); } catch (BusinessException e) { logger.error("Could not add " + fileName + " to user " + owner.getLsUuid() + ", reason : ", e); throw new TechnicalException(TechnicalErrorCode.COULD_NOT_INSERT_DOCUMENT, "couldn't register the file in the database"); } return entity; } @Override public byte[] getTimeStamp(String fileName, File tempFile, String timeStampingUrl) throws BusinessException { if(timeStampingUrl == null) { return null; } byte[] timestampToken = null; FileInputStream fis = null; try{ fis = new FileInputStream(tempFile); TimeStampResponse resp = timeStampingService.getTimeStamp(timeStampingUrl, fis); timestampToken = resp.getEncoded(); } catch (TSPException e) { logger.error(e.toString()); throw new BusinessException(BusinessErrorCode.FILE_TIMESTAMP_NOT_COMPUTED,"TimeStamp on file is not computed", new String[] {fileName}); } catch (FileNotFoundException e) { logger.error(e.toString()); throw new BusinessException(BusinessErrorCode.FILE_TIMESTAMP_NOT_COMPUTED,"TimeStamp on file is not computed", new String[] {fileName}); } catch (IOException e) { logger.error(e.toString()); throw new BusinessException(BusinessErrorCode.FILE_TIMESTAMP_NOT_COMPUTED,"TimeStamp on file is not computed", new String[] {fileName}); } catch (URISyntaxException e) { logger.error(e.toString()); throw new BusinessException(BusinessErrorCode.FILE_TIMESTAMP_WRONG_TSA_URL,"The Tsa Url is empty or invalid", new String[] {fileName}); } finally { try { if (fis != null) fis.close(); fis = null; } catch (IOException e) { logger.error(e.toString()); } } return timestampToken ; } @Override public InputStream getDocumentThumbnailStream(DocumentEntry entry) { Document doc = entry.getDocument(); String thmbUuid = doc.getThmbUuid(); if (thmbUuid != null && thmbUuid.length()>0) { FileMetaData metadata = new FileMetaData(FileMetaDataKind.THUMBNAIL, entry.getDocument()); InputStream stream = fileDataStore.get(metadata); return stream; } return null; } @Override public InputStream getThreadEntryThumbnailStream(ThreadEntry entry) { Document doc = documentRepository.findByUuid(entry.getDocument().getUuid()); String thmbUuid = doc.getThmbUuid(); if (thmbUuid != null && thmbUuid.length()>0) { FileMetaData metadata = new FileMetaData(FileMetaDataKind.THUMBNAIL, entry.getDocument()); InputStream stream = fileDataStore.get(metadata); return stream; } return null; } @Override public InputStream getDocumentStream(DocumentEntry entry) { String UUID = entry.getDocument().getUuid(); if (UUID!=null && UUID.length()>0) { logger.debug("retrieve from jackrabbity : " + UUID); FileMetaData metadata = new FileMetaData(FileMetaDataKind.DATA, entry.getDocument()); InputStream stream = fileDataStore.get(metadata); return stream; } return null; } @Override public DocumentEntry find(String uuid) { return documentEntryRepository.findById(uuid); } @Override public List<DocumentEntry> findAllMyDocumentEntries(Account owner) { return documentEntryRepository.findAllMyDocumentEntries(owner); } @Override public DocumentEntry renameDocumentEntry(DocumentEntry entry, String newName) throws BusinessException { entry.setName(newName); return documentEntryRepository.update(entry); } @Override public DocumentEntry updateFileProperties(DocumentEntry entry, String newName, String fileComment, String meta) throws BusinessException { entry.setBusinessName(newName); entry.setBusinessComment(fileComment); entry.setBusinessMetaData(meta); return documentEntryRepository.update(entry); } @Override public ThreadEntry updateFileProperties(ThreadEntry entry, String fileComment, String metaData, String newName) throws BusinessException { entry.setBusinessComment(fileComment); entry.setBusinessMetaData(metaData); entry.setBusinessName(newName); return threadEntryRepository.update(entry); } @Override public DocumentEntry updateDocumentEntry(Account owner, DocumentEntry docEntry, File myFile, Long size, String fileName, Boolean checkIfIsCiphered, String timeStampingUrl, String mimeType, Calendar expirationDate) throws BusinessException { String sha256sum = SHA256CheckSumFileStream(myFile); FileMetaData metadata = null; FileMetaData metadataThmb = null; // want a timestamp on doc ? if timeStamping url is null, time stamp will be null try { Document document = null; List<Document> documents = documentRepository.findBySha256Sum(sha256sum); if (documents.isEmpty()) { // Storing file metadata = new FileMetaData(FileMetaDataKind.DATA, mimeType, size, fileName); metadata.setFileName(fileName); metadata = fileDataStore.add(myFile, metadata); // Computing and storing thumbnail metadataThmb = computeAndStoreThumbnail(owner, myFile, metadata); byte[] timestampToken = getTimeStamp(fileName, myFile, timeStampingUrl); document = new Document(metadata); document.setThmbUuid(metadataThmb.getUuid()); document.setTimeStamp(timestampToken); document.setSha256sum(sha256sum); document = documentRepository.create(document); } else { document = documents.get(0); } docEntry.setName(fileName); docEntry.setDocument(document); docEntry.setExpirationDate(expirationDate); docEntry.setSize(size); docEntry.setType(mimeType); docEntry.setSha256sum(sha256sum); //aes encrypt ? check headers if(checkIfIsCiphered) { docEntry.setCiphered(checkIfFileIsCiphered(fileName, myFile)); } documentEntryRepository.update(docEntry); return docEntry; } catch (BusinessException e) { logger.error("Could not add " + fileName + " to user " + owner.getAccountRepresentation() + ", reason : ", e); if (metadata != null) fileDataStore.remove(metadata); if (metadataThmb != null) fileDataStore.remove(metadataThmb); throw new TechnicalException(TechnicalErrorCode.COULD_NOT_INSERT_DOCUMENT, "couldn't register the file in the database"); } } @Override public void deleteDocumentEntry(DocumentEntry documentEntry) throws BusinessException { UploadRequestEntry uploadRequestEntry = uploadRequestEntryBusinessService.findRelative(documentEntry); if (uploadRequestEntry != null) { uploadRequestEntry.setDocumentEntry(null); uploadRequestEntryBusinessService.update(uploadRequestEntry); } logger.debug("Deleting document entry: " + documentEntry.getUuid()); Account owner = documentEntry.getEntryOwner(); owner.getEntries().remove(documentEntry); documentEntryRepository.delete(documentEntry); } @Override public ThreadEntry createThreadEntry(Thread owner, File myFile, Long size, String fileName, Boolean checkIfIsCiphered, String timeStampingUrl, String mimeType) throws BusinessException { // add an entry for the file in DB ThreadEntry entity = null; Document document = createDocument(owner, myFile, size, fileName, timeStampingUrl, mimeType); ThreadEntry docEntry = new ThreadEntry(owner, fileName, document); //aes encrypt ? check headers if (checkIfIsCiphered) { docEntry.setCiphered(checkIfFileIsCiphered(fileName, myFile)); } entity = threadEntryRepository.create(docEntry); owner.getEntries().add(entity); return entity; } @Override public ThreadEntry copyFromDocumentEntry(Thread thread, DocumentEntry documentEntry) throws BusinessException { String name = documentEntry.getName(); Document document = documentEntry.getDocument(); ThreadEntry threadEntry = new ThreadEntry(thread, name, document); threadEntry.setCiphered(documentEntry.getCiphered()); threadEntry = threadEntryRepository.create(threadEntry); thread.getEntries().add(threadEntry); return threadEntry; } @Override public DocumentEntry copyFromThreadEntry(Account owner, ThreadEntry entry, Calendar expirationDate) throws BusinessException { String name = entry.getName(); Document document = entry.getDocument(); DocumentEntry docEntry = new DocumentEntry(owner, name, entry.getComment(), document); // We need to set an expiration date in case of file cleaner activation. docEntry.setExpirationDate(expirationDate); docEntry.setMetaData(entry.getMetaData()); docEntry.setCiphered(entry.getCiphered()); docEntry = documentEntryRepository.create(docEntry); owner.getEntries().add(docEntry); return docEntry; } @Override public DocumentEntry copyFromShareEntry(Account owner, ShareEntry entry, Calendar expirationDate) throws BusinessException { String name = entry.getName(); Document document = entry.getDocumentEntry().getDocument(); DocumentEntry docEntry = new DocumentEntry(owner, name, entry.getComment(), document); // We need to set an expiration date in case of file cleaner activation. docEntry.setExpirationDate(expirationDate); docEntry.setMetaData(entry.getMetaData()); docEntry.setCiphered(entry.getDocumentEntry().getCiphered()); docEntry = documentEntryRepository.create(docEntry); owner.getEntries().add(docEntry); return docEntry; } @Override public ThreadEntry findThreadEntryById(String docEntryUuid) { return threadEntryRepository.findByUuid(docEntryUuid); } @Override public List<ThreadEntry> findAllThreadEntries(Thread owner) { return threadEntryRepository.findAllThreadEntries(owner); } @Override public long countThreadEntries(Thread thread) { return threadEntryRepository.count(thread); } @Override public InputStream getDocumentStream(ThreadEntry entry) { String UUID = entry.getDocument().getUuid(); if (UUID!=null && UUID.length()>0) { logger.debug("retrieve from jackrabbit : " + UUID); FileMetaData metadata = new FileMetaData(FileMetaDataKind.DATA, entry.getDocument()); InputStream stream = fileDataStore.get(metadata); return stream; } return null; } private Document createDocument(Account owner, File myFile, Long size, String fileName, String timeStampingUrl, String mimeType) throws BusinessException { String sha256sum = SHA256CheckSumFileStream(myFile); List<Document> documents = documentRepository.findBySha256Sum(sha256sum); if (documents.isEmpty() || !deduplication) { // Storing file FileMetaData metadata = new FileMetaData(FileMetaDataKind.DATA, mimeType, size, fileName); metadata = fileDataStore.add(myFile, metadata); // Computing and storing thumbnail FileMetaData metadataThmb = computeAndStoreThumbnail(owner, myFile, metadata); try { // want a timestamp on doc ? byte[] timestampToken = null; if(timeStampingUrl != null) { timestampToken = getTimeStamp(fileName, myFile, timeStampingUrl); } // add an document for the file in DB Document document = new Document(metadata); document.setSha256sum(sha256sum); document.setSha1sum(SHA1CheckSumFileStream(myFile)); if (metadataThmb != null) document.setThmbUuid(metadataThmb.getUuid()); document.setTimeStamp(timestampToken); return documentRepository.create(document); } catch (Exception e) { if (metadata != null) fileDataStore.remove(metadata); if (metadataThmb != null) fileDataStore.remove(metadataThmb); throw e; } } else { // we return the first one, the others will be removed by time (expiration, users, ...) return documents.get(0); } } @Override public void deleteDocument(Document document) throws BusinessException { // delete old thumbnail in JCR String oldThumbUuid = document.getThmbUuid(); if (oldThumbUuid != null && oldThumbUuid.length() > 0) { logger.debug("suppression of Thumb, Uuid : " + oldThumbUuid); FileMetaData metadata = new FileMetaData(FileMetaDataKind.THUMBNAIL, document, "image/png"); fileDataStore.remove(metadata); } // remove old document in JCR logger.debug("suppression of doc, Uuid : " + document.getUuid()); FileMetaData metadata = new FileMetaData(FileMetaDataKind.DATA, document); fileDataStore.remove(metadata); //clean all signatures ... Set<Signature> signatures = document.getSignatures(); for (Signature signature : signatures) { signatureBusinessService.deleteSignature(signature); } signatures.clear(); documentRepository.update(document); // remove old document from database documentRepository.delete(document); } private FileMetaData computeAndStoreThumbnail(Account owner, File tempFile, FileMetaData metadata) { if (!thumbEnabled || (!pdfThumbEnabled && metadata.getMimeType().contains("pdf"))) { logger.warn("Thumbnail generation is disabled."); return null; } FileMetaData ret = null; FileResourceFactory fileResourceFactory = FileResourceFactory.getInstance(); FileResource fileResource = fileResourceFactory.getFileResource(tempFile); InputStream fisThmb = null; BufferedImage bufferedImage=null; File tempThumbFile = null; if (fileResource != null) { try { try { bufferedImage = fileResource.generateThumbnailImage(); } catch (Exception e) { logger.error(e.getLocalizedMessage()); logger.debug(e.toString()); } if (bufferedImage != null) { fisThmb = ImageUtils.getInputStreamFromImage(bufferedImage, "png"); tempThumbFile = File.createTempFile("linthumbnail", owner+"_thumb.png"); tempThumbFile.createNewFile(); if (bufferedImage!=null) ImageIO.write(bufferedImage, Constants.THMB_DEFAULT_FORMAT, tempThumbFile); if (logger.isDebugEnabled()) { logger.debug("5.1)start insert of thumbnail in jack rabbit:" + tempThumbFile.getName()); } ret = new FileMetaData(FileMetaDataKind.THUMBNAIL, "image/png", tempThumbFile.length(), metadata.getFileName()); ret = fileDataStore.add(tempThumbFile, ret); } } catch (FileNotFoundException e) { logger.error(e.toString()); // if the thumbnail generation fails, it's not big deal, it has not to block // the entire process, we just don't have a thumbnail for this document } catch (IOException e) { logger.error(e.toString()); } finally { try { if (fisThmb != null) fisThmb.close(); } catch (IOException e) { // Do nothing Happy java :) logger.error(e.toString()); } if(tempThumbFile!=null){ tempThumbFile.delete(); } } } return ret; } private boolean checkIfFileIsCiphered(String fileName, File tempFile) throws BusinessException { boolean testheaders = false; if(fileName.endsWith(".aes")){ try { AESCrypt aestest = new AESCrypt(); testheaders = aestest.ckeckFileHeader(tempFile.getAbsolutePath()); } catch (IOException e) { throw new BusinessException(BusinessErrorCode.FILE_ENCRYPTION_UNDEFINED,"undefined encryption format"); } catch (GeneralSecurityException e) { throw new BusinessException(BusinessErrorCode.FILE_ENCRYPTION_UNDEFINED,"undefined encryption format"); } if(!testheaders) throw new BusinessException(BusinessErrorCode.FILE_ENCRYPTION_UNDEFINED,"undefined encryption format"); } return testheaders; } @Override public long getRelatedEntriesCount(DocumentEntry documentEntry) { return documentEntryRepository.getRelatedEntriesCount(documentEntry); } @Override public void deleteThreadEntry(ThreadEntry threadEntry) throws BusinessException { Account owner = threadEntry.getEntryOwner(); owner.getEntries().remove(threadEntry); accountRepository.update(owner); threadEntryRepository.delete(threadEntry); } @Override public void deleteSetThreadEntry(Set<Entry> setThreadEntry) throws BusinessException { for (Object threadEntry : setThreadEntry.toArray()) { this.deleteThreadEntry((ThreadEntry) threadEntry); } } @Override public long getUsedSpace(Account owner) throws BusinessException { return documentEntryRepository.getUsedSpace(owner); } @Override public String SHA256CheckSumFileStream(File file) { try(InputStream fs = new FileInputStream(file)) { return SHA256CheckSumFileStream(fs); } catch (IOException e) { logger.error("Could not found the file : " + file.getName(), e); throw new BusinessException(BusinessErrorCode.FILE_UNREACHABLE, "Coul not found the file " + file.getName()); } } @Override public String SHA1CheckSumFileStream(File file) { try(InputStream fs = new FileInputStream(file)) { return SHA1CheckSumFileStream(fs); } catch (IOException e) { logger.error("Could not found the file : " + file.getName(), e); throw new BusinessException(BusinessErrorCode.FILE_UNREACHABLE, "Coul not found the file " + file.getName()); } } @Override public String SHA256CheckSumFileStream(InputStream fis) throws IOException { StringBuffer hexString = new StringBuffer(); try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] dataBytes = new byte[1024]; int nread = 0; while ((nread = fis.read(dataBytes)) != -1) { md.update(dataBytes, 0, nread); } byte[] mdbytes = md.digest(); // convert the byte to hex format for (int i = 0; i < mdbytes.length; i++) { hexString.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16) .substring(1)); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { logger.error(e.getMessage(), e); throw new TechnicalException(e.getMessage()); } } @Override public String SHA1CheckSumFileStream(InputStream fs) throws IOException { try { StringBuffer hexString = new StringBuffer(); MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] dataBytes = new byte[1024]; int nread = 0; while ((nread = fs.read(dataBytes)) != -1) { md.update(dataBytes, 0, nread); } byte[] mdbytes = md.digest(); // convert the byte to hex format for (int i = 0; i < mdbytes.length; i++) { hexString.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16) .substring(1)); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { logger.error(e.getMessage(), e); throw new TechnicalException(e.getMessage()); } } @Override public void update(DocumentEntry docEntry) throws BusinessException { documentEntryRepository.update(docEntry); } @Override public List<ThreadEntry> findMoreRecentByName(Thread thread) throws BusinessException { return threadEntryRepository.findAllDistinctEntries(thread); } @Override public DocumentEntry findMoreRecentByName(Account owner, String fileName) throws BusinessException { return documentEntryRepository.findMoreRecentByName(owner, fileName); } @Override public void syncUniqueDocument(Account owner, String fileName) throws BusinessException { documentEntryRepository.syncUniqueDocument(owner, fileName); } @Override public List<DocumentEntry> findAllMySyncEntries(Account owner) throws BusinessException { return documentEntryRepository.findAllMySyncEntries(owner); } @Override public List<String> findAllExpiredEntries() { return documentEntryRepository.findAllExpiredEntries(); } }