/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library 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 Lesser General Public License for more
* details.
*/
package com.liferay.sync.engine.document.library.handler;
import com.liferay.sync.engine.SyncEngine;
import com.liferay.sync.engine.document.library.event.Event;
import com.liferay.sync.engine.document.library.util.FileEventUtil;
import com.liferay.sync.engine.file.system.Watcher;
import com.liferay.sync.engine.file.system.util.WatcherManager;
import com.liferay.sync.engine.model.SyncAccount;
import com.liferay.sync.engine.model.SyncFile;
import com.liferay.sync.engine.service.SyncAccountService;
import com.liferay.sync.engine.service.SyncFileService;
import com.liferay.sync.engine.session.Session;
import com.liferay.sync.engine.session.SessionManager;
import com.liferay.sync.engine.session.rate.limiter.RateLimitedInputStream;
import com.liferay.sync.engine.util.FileKeyUtil;
import com.liferay.sync.engine.util.FileUtil;
import com.liferay.sync.engine.util.GetterUtil;
import com.liferay.sync.engine.util.IODeltaUtil;
import com.liferay.sync.engine.util.MSOfficeFileUtil;
import com.liferay.sync.engine.util.StreamUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.ExecutorService;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.http.ConnectionClosedException;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Shinn Lok
*/
public class DownloadFileHandler extends BaseHandler {
public DownloadFileHandler(Event event) {
super(event);
}
@Override
public void handleException(Exception e) {
if (isEventCancelled()) {
return;
}
if (e instanceof ConnectionClosedException) {
String message = e.getMessage();
if (message.startsWith("Premature end of Content-Length")) {
_logger.error(message, e);
removeEvent();
SyncFile localSyncFile = getLocalSyncFile();
if (localSyncFile == null) {
return;
}
FileEventUtil.downloadFile(
getSyncAccountId(), localSyncFile, false);
return;
}
}
else if (e instanceof HttpResponseException) {
_logger.error(e.getMessage(), e);
HttpResponseException hre = (HttpResponseException)e;
int statusCode = hre.getStatusCode();
if (statusCode != HttpStatus.SC_NOT_FOUND) {
super.handleException(e);
return;
}
SyncAccount syncAccount = SyncAccountService.fetchSyncAccount(
getSyncAccountId());
if (syncAccount.getState() != SyncAccount.STATE_CONNECTED) {
super.handleException(e);
return;
}
removeEvent();
SyncFile syncFile = getLocalSyncFile();
if (syncFile == null) {
return;
}
if ((boolean)getParameterValue("patch")) {
FileEventUtil.downloadFile(getSyncAccountId(), syncFile, false);
}
else {
handleException(syncFile);
}
return;
}
super.handleException(e);
}
@Override
public boolean handlePortalException(String exception) throws Exception {
SyncFile syncFile = getLocalSyncFile();
if (syncFile == null) {
return true;
}
if (_logger.isDebugEnabled()) {
_logger.debug(
"Handling exception {} file path {}", exception,
syncFile.getFilePathName());
}
if (exception.endsWith("PrincipalException")) {
syncFile.setState(SyncFile.STATE_ERROR);
syncFile.setUiEvent(SyncFile.UI_EVENT_INVALID_PERMISSIONS);
SyncFileService.update(syncFile);
return true;
}
else if (exception.endsWith("NoSuchFileVersionException") &&
(boolean)getParameterValue("patch")) {
removeEvent();
FileEventUtil.downloadFile(getSyncAccountId(), syncFile, false);
return true;
}
else if (exception.endsWith("NoSuchFileEntryException") ||
exception.endsWith("NoSuchFileException")) {
SyncFileService.deleteSyncFile(syncFile);
return true;
}
removeEvent();
handleException(syncFile);
return true;
}
protected void copyFile(
final SyncFile syncFile, Path filePath, InputStream inputStream,
boolean append)
throws Exception {
OutputStream outputStream = null;
Watcher watcher = WatcherManager.getWatcher(getSyncAccountId());
try {
Path tempFilePath = FileUtil.getTempFilePath(syncFile);
boolean exists = FileUtil.exists(filePath);
if (append) {
outputStream = Files.newOutputStream(
tempFilePath, StandardOpenOption.APPEND);
IOUtils.copyLarge(inputStream, outputStream);
}
else {
if (exists && (boolean)getParameterValue("patch")) {
if (_logger.isDebugEnabled()) {
_logger.debug(
"Patching {}", syncFile.getFilePathName());
}
Files.copy(
filePath, tempFilePath,
StandardCopyOption.REPLACE_EXISTING);
IODeltaUtil.patch(tempFilePath, inputStream);
}
else {
Files.copy(
inputStream, tempFilePath,
StandardCopyOption.REPLACE_EXISTING);
}
}
watcher.addDownloadedFilePathName(filePath.toString());
if (GetterUtil.getBoolean(
syncFile.getLocalExtraSettingValue("restoreEvent"))) {
syncFile.unsetLocalExtraSetting("restoreEvent");
syncFile.setUiEvent(SyncFile.UI_EVENT_RESTORED_REMOTE);
}
else if (exists) {
syncFile.setUiEvent(SyncFile.UI_EVENT_DOWNLOADED_UPDATE);
}
else {
syncFile.setUiEvent(SyncFile.UI_EVENT_DOWNLOADED_NEW);
}
FileKeyUtil.writeFileKey(
tempFilePath, String.valueOf(syncFile.getSyncFileId()), false);
FileUtil.setModifiedTime(tempFilePath, syncFile.getModifiedTime());
if (MSOfficeFileUtil.isLegacyExcelFile(filePath)) {
syncFile.setLocalExtraSetting(
"lastSavedDate",
MSOfficeFileUtil.getLastSavedDate(tempFilePath));
}
Files.move(
tempFilePath, filePath, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
ExecutorService executorService = SyncEngine.getExecutorService();
Runnable runnable = new Runnable() {
@Override
public void run() {
IODeltaUtil.checksums(syncFile);
syncFile.setState(SyncFile.STATE_SYNCED);
SyncFileService.update(syncFile);
}
};
executorService.execute(runnable);
}
catch (FileSystemException fse) {
if (fse instanceof AccessDeniedException) {
_logger.error(fse.getMessage(), fse);
syncFile.setState(SyncFile.STATE_ERROR);
syncFile.setUiEvent(SyncFile.UI_EVENT_ACCESS_DENIED_LOCAL);
SyncFileService.update(syncFile);
return;
}
else if (fse instanceof NoSuchFileException) {
if (isEventCancelled()) {
SyncFileService.deleteSyncFile(syncFile);
return;
}
}
watcher.removeDownloadedFilePathName(filePath.toString());
String message = fse.getMessage();
_logger.error(message, fse);
syncFile.setState(SyncFile.STATE_ERROR);
if (message.contains("File name too long")) {
syncFile.setUiEvent(SyncFile.UI_EVENT_FILE_NAME_TOO_LONG);
}
SyncFileService.update(syncFile);
}
finally {
StreamUtil.cleanUp(outputStream);
}
}
@Override
protected void doHandleResponse(HttpResponse httpResponse)
throws Exception {
Header errorHeader = httpResponse.getFirstHeader("Sync-Error");
if (errorHeader != null) {
handleSiteDeactivatedException();
}
long syncAccountId = getSyncAccountId();
final Session session = SessionManager.getSession(syncAccountId);
Header tokenHeader = httpResponse.getFirstHeader("Sync-JWT");
if (tokenHeader != null) {
session.addHeader("Sync-JWT", tokenHeader.getValue());
}
InputStream inputStream = null;
SyncFile syncFile = getLocalSyncFile();
if ((syncFile == null) || isUnsynced(syncFile)) {
return;
}
Path filePath = Paths.get(syncFile.getFilePathName());
try {
HttpEntity httpEntity = httpResponse.getEntity();
inputStream = new CountingInputStream(httpEntity.getContent()) {
@Override
protected synchronized void afterRead(int n) {
session.incrementDownloadedBytes(n);
super.afterRead(n);
}
};
inputStream = new RateLimitedInputStream(
inputStream, syncAccountId);
if (httpResponse.getFirstHeader("Accept-Ranges") != null) {
copyFile(syncFile, filePath, inputStream, true);
}
else {
copyFile(syncFile, filePath, inputStream, false);
}
}
finally {
StreamUtil.cleanUp(inputStream);
}
}
protected void handleException(SyncFile syncFile) {
syncFile.setState(SyncFile.STATE_ERROR);
syncFile.setUiEvent(SyncFile.UI_EVENT_DOWNLOAD_EXCEPTION);
SyncFileService.update(syncFile);
Path filePathName = Paths.get(syncFile.getFilePathName());
if (FileUtil.notExists(filePathName)) {
try {
Files.createFile(filePathName);
}
catch (IOException ioe) {
_logger.error(ioe.getMessage(), ioe);
}
}
}
protected boolean isUnsynced(SyncFile syncFile) {
if (syncFile != null) {
syncFile = SyncFileService.fetchSyncFile(syncFile.getSyncFileId());
}
if (syncFile == null) {
return true;
}
if (syncFile.getState() == SyncFile.STATE_UNSYNCED) {
if (_logger.isDebugEnabled()) {
_logger.debug(
"Skipping file {}. File is unsynced.", syncFile.getName());
}
return true;
}
Path filePath = Paths.get(syncFile.getFilePathName());
if (FileUtil.notExists(filePath.getParent())) {
if (_logger.isDebugEnabled()) {
_logger.debug(
"Skipping file {}. Missing parent file path {}.",
syncFile.getName(), filePath.getParent());
}
syncFile.setState(SyncFile.STATE_ERROR);
syncFile.setUiEvent(SyncFile.UI_EVENT_PARENT_MISSING);
SyncFileService.update(syncFile);
return true;
}
return false;
}
private static final Logger _logger = LoggerFactory.getLogger(
DownloadFileHandler.class);
}