package org.ovirt.engine.ui.uicommonweb.models.storage;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import org.ovirt.engine.core.common.AuditLogType;
import org.ovirt.engine.core.common.action.AddDiskParameters;
import org.ovirt.engine.core.common.action.TransferDiskImageParameters;
import org.ovirt.engine.core.common.action.TransferImageStatusParameters;
import org.ovirt.engine.core.common.action.VdcActionParametersBase;
import org.ovirt.engine.core.common.action.VdcActionType;
import org.ovirt.engine.core.common.businessentities.StorageFormatType;
import org.ovirt.engine.core.common.businessentities.StoragePool;
import org.ovirt.engine.core.common.businessentities.storage.Disk;
import org.ovirt.engine.core.common.businessentities.storage.DiskImage;
import org.ovirt.engine.core.common.businessentities.storage.DiskStorageType;
import org.ovirt.engine.core.common.businessentities.storage.ImageTransfer;
import org.ovirt.engine.core.common.businessentities.storage.ImageTransferPhase;
import org.ovirt.engine.core.common.utils.SizeConverter;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.compat.StringHelper;
import org.ovirt.engine.ui.frontend.Frontend;
import org.ovirt.engine.ui.uicommonweb.ICommandTarget;
import org.ovirt.engine.ui.uicommonweb.UICommand;
import org.ovirt.engine.ui.uicommonweb.dataprovider.AsyncDataProvider;
import org.ovirt.engine.ui.uicommonweb.help.HelpTag;
import org.ovirt.engine.ui.uicommonweb.models.ConfirmationModel;
import org.ovirt.engine.ui.uicommonweb.models.EntityModel;
import org.ovirt.engine.ui.uicommonweb.models.ListModel;
import org.ovirt.engine.ui.uicommonweb.models.Model;
import org.ovirt.engine.ui.uicommonweb.models.vms.AbstractDiskModel;
import org.ovirt.engine.ui.uicommonweb.models.vms.NewDiskModel;
import org.ovirt.engine.ui.uicommonweb.models.vms.ReadOnlyDiskModel;
import org.ovirt.engine.ui.uicommonweb.validation.IValidation;
import org.ovirt.engine.ui.uicommonweb.validation.ValidationResult;
import org.ovirt.engine.ui.uicompat.ConstantsManager;
import org.ovirt.engine.ui.uicompat.EventDefinition;
import org.ovirt.engine.ui.uicompat.FrontendActionAsyncResult;
import org.ovirt.engine.ui.uicompat.PropertyChangedEventArgs;
import org.ovirt.engine.ui.uicompat.UIConstants;
import org.ovirt.engine.ui.uicompat.UIMessages;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Window;
/**
* The Java and JavaScript (JSNI) code to perform an image upload lives here.
*
* The general upload flow is as follows:
* - The view calls UploadImageModel.onUpload()
* - onUpload() does some initialization and runs Upload[Disk]ImageCommand
* - Upload[Disk]ImageCommand initializes and returns to us
* - The engine command creates an ImageUpload entity to track the execution
* state and returns success to the model. Meanwhile, it uses CoCo callbacks
* to continue execution for the duration of the upload, using a state
* machine driven primarily by the entity state.
* - The model starts polling the engine, sending UploadImageStatusCommand,
* and uses a callback on the response to drive its own state machine driven
* by the entity state.
* - The engine command creates the disk, starts the upload session with vdsm
* and vdsm-imaged, creates a signed ticket for us to send to the proxy, and
* updates the entity phase to TRANSFERRING.
* - The model responds by initiating the upload, passing control to the
* JavaScript code. The progress of the JS is shared with the Java in a
* variable called uploadState.
* - (Meanwhile, the model continues to poll and may receive and error from
* engine, a request to pause the upload, etc which would result in the
* model requesting the JS to stop by changing uploadState.)
* - (Also meanwhile, the engine command continues its callbacks which will
* periodically poll vdsm->vdsm-imaged for the upload progress and update
* the entity. The UI can then retrieve the progress by refreshing the
* disk list, as the disks show upload progress via a database view that
* joins with the image_upload entity.)
* - The JS will start the upload, sending data to the proxy and imaged. When
* complete or on error, it sets the uploadState, signalling to the model's
* callbacks that it can now take control of the upload.
* - Upon JS completion, if the request was to pause, the model stops, leaving
* the command to poll for a resumption request. If there was an error or
* successful completion, the model updates the entity state as appropriate
* to FINALIZING_[SUCCESS|FAILURE].
* - The engine command will respond to a FINALIZING_* state and finalize the
* upload, setting the entity state to FINISHED_[SUCCESS|FAILURE].
*/
public class UploadImageModel extends Model implements ICommandTarget {
private static final Logger log = Logger.getLogger(UploadImageModel.class.getName());
private static final int POLLING_DELAY_MS = 4000;
private static final int MAX_FAILED_POLL_ATTEMPTS = 3;
private static UIConstants constants = ConstantsManager.getInstance().getConstants();
private static UIMessages messages = ConstantsManager.getInstance().getMessages();
private static EventDefinition selectedItemChangedEventDefinition;
static {
selectedItemChangedEventDefinition = new EventDefinition("SelectedItemChanged", ListModel.class); //$NON-NLS-1$
}
// Note: Keep these in sync with the constants in the JavaScript below
// as well as the stopAnyActiveJsUploadExecution() method!
private enum UploadState {
NEW, // Model loaded but upload not started
INITIALIZING, // Upload triggered, transferring control to JS
TRANSFERRING, // JS active, blocks being sent to client
SUCCESS, // JS finished, success
ENGINE_PAUSE, // JS stopped due to pause by user or system sent from engine
ENGINE_CANCEL, // JS stopped due to cancellation by engine
CLIENT_ERROR, // JS stopped due to error detected on the client (frontend/JS)
}
private List<EntityModel> entities;
private EntityModel<Boolean> imageSourceLocalEnabled;
private EntityModel<String> imagePath;
private EntityModel<String> imageUri;
private AbstractDiskModel diskModel;
private UICommand okCommand;
private UICommand cancelCommand;
private boolean isResumeUpload;
private Guid commandId;
private String transferToken;
private Guid imageId;
private String vdsId;
private Element imageFileUploadElement;
private boolean browserSupportsUpload;
private int failedPollAttempts;
private int failedFinalizationAttempts;
// The following are shared by the Java and JS (JSNI) code
private long bytesSent;
private String progressStr;
private String errorMessage;
private UploadState uploadState;
private boolean continuePolling;
private AuditLogType auditLogType;
private ImageInfoModel imageInfoModel;
public List<EntityModel> getEntities() {
return entities;
}
public void setEntities(List<EntityModel> entities) {
if (entities != this.entities) {
this.entities = entities;
onPropertyChanged(new PropertyChangedEventArgs("UploadImageEntities")); //$NON-NLS-1$
}
}
public EntityModel<Boolean> getImageSourceLocalEnabled() {
return imageSourceLocalEnabled;
}
public void setImageSourceLocalEnabled(EntityModel<Boolean> imageSourceLocalEnabled) {
this.imageSourceLocalEnabled = imageSourceLocalEnabled;
}
public EntityModel<String> getImagePath() {
return imagePath;
}
public void setImagePath(EntityModel<String> imagePath) {
this.imagePath = imagePath;
}
public EntityModel<String> getImageUri() {
return imageUri;
}
public void setImageUri(EntityModel<String> imageUri) {
this.imageUri = imageUri;
}
public AbstractDiskModel getDiskModel() {
return diskModel;
}
public void setDiskModel(AbstractDiskModel diskModel) {
this.diskModel = diskModel;
}
public UICommand getOkCommand() {
return okCommand;
}
public void setOkCommand(UICommand okCommand) {
this.okCommand = okCommand;
}
@Override
public UICommand getCancelCommand() {
return cancelCommand;
}
public void setCancelCommand(UICommand cancelCommand) {
this.cancelCommand = cancelCommand;
}
public boolean getIsResumeUpload() {
return isResumeUpload;
}
public void setIsResumeUpload(boolean isResumeUpload) {
this.isResumeUpload = isResumeUpload;
}
public Guid getCommandId() {
return commandId;
}
public void setCommandId(Guid commandId) {
this.commandId = commandId;
}
public String getTransferToken() {
return transferToken;
}
public void setTransferToken(String transferToken) {
this.transferToken = transferToken;
}
public Guid getImageId() {
return imageId;
}
public void setImageId(Guid imageId) {
this.imageId = imageId;
}
public String getVdsId() {
return vdsId;
}
public void setVdsId(String vdsId) {
this.vdsId = vdsId;
}
public Element getImageFileUploadElement() {
return imageFileUploadElement;
}
public void setImageFileUploadElement(Element imageFileUploadElement) {
this.imageFileUploadElement = imageFileUploadElement;
}
public boolean getBrowserSupportsUpload() {
return browserSupportsUpload;
}
public void setBrowserSupportsUpload(boolean browserSupportsUpload) {
this.browserSupportsUpload = browserSupportsUpload;
}
// The following are shared by the Java and JS (JSNI) code
public long getBytesSent() {
return bytesSent;
}
public void setBytesSent(long bytesSent) {
this.bytesSent = bytesSent;
}
public void setBytesSent(double bytesSent) {
this.bytesSent = (long)bytesSent;
setProgressStr("Sent " + bytesSent / SizeConverter.BYTES_IN_MB + "MB"); //$NON-NLS-1$ //$NON-NLS-2$
}
public String getProgressStr() {
return progressStr;
}
private void setProgressStr(String progressStr) {
this.progressStr = progressStr;
onPropertyChanged(new PropertyChangedEventArgs("Progress") ); //$NON-NLS-1$
}
protected String getErrorMessage() {
return errorMessage;
}
protected void setErrorMessage(String message) {
errorMessage = message;
}
public UploadState getUploadState() {
return uploadState;
}
public String getUploadStateString() {
return uploadState.name();
}
public void setUploadState(UploadState uploadState) {
this.uploadState = uploadState;
}
public void setUploadStateByString(String uploadState) {
this.uploadState = UploadState.valueOf(uploadState);
}
public void setAuditLogType(AuditLogType auditLogType) {
this.auditLogType = auditLogType;
}
private boolean getContinuePolling() {
return continuePolling;
}
private void setContinuePolling(boolean value) {
continuePolling = value;
}
public UploadImageModel(final Guid limitToStorageDomainId, final DiskImage resumeUploadDisk) {
if (resumeUploadDisk == null) {
setDiskModel(new NewDiskModel() {
@Override
public void initialize() {
super.initialize();
getStorageDomain().setIsChangeable(limitToStorageDomainId == null);
getDataCenter().setIsChangeable(limitToStorageDomainId == null);
getStorageType().setIsChangeable(false);
}
@Override
protected void updateStorageDomains(final StoragePool datacenter) {
if (limitToStorageDomainId == null) {
super.updateStorageDomains(datacenter);
} else {
AsyncDataProvider.getInstance().getStorageDomainById(new AsyncQuery<>(storageDomain -> getStorageDomain().setSelectedItem(storageDomain)), limitToStorageDomainId);
}
}
@Override
public int getMinimumDiskSize() {
return Math.max(getImageInfoModel().getActualSize(), getImageInfoModel().getVirtualSize());
}
@Override
protected boolean performUpdateHosts() {
return true;
}
});
} else {
setDiskModel(new ReadOnlyDiskModel() {
@Override
protected boolean performUpdateHosts() {
return true;
}
});
setImageId(resumeUploadDisk.getImageId());
getDiskModel().setDisk(resumeUploadDisk);
getDiskModel().getDiskInterface().setIsAvailable(false);
setIsResumeUpload(true);
}
setImageSourceLocalEnabled(new EntityModel<Boolean>());
getImageSourceLocalEnabled().setEntity(true);
getImageSourceLocalEnabled().getEntityChangedEvent().addListener(this);
setImagePath(new EntityModel<String>());
setImageUri(new EntityModel<String>());
setUploadState(UploadState.NEW);
setProgressStr(""); //$NON-NLS-1$
setErrorMessage(null);
setBrowserSupportsUpload(browserSupportsUploadAPIs());
setOkCommand(UICommand.createDefaultOkUiCommand("Ok", this)); //$NON-NLS-1$
getOkCommand().setIsExecutionAllowed(true);
getCommands().add(getOkCommand());
getDiskModel().getStorageDomain().getSelectedItemChangedEvent().addListener(this);
getDiskModel().getVolumeType().setIsAvailable(false);
getDiskModel().getHost().setIsAvailable(true);
imageInfoModel = new ImageInfoModel();
}
@Override
public void initialize() {
getDiskModel().initialize();
}
@Override
public void executeCommand(UICommand command) {
super.executeCommand(command);
if (getOkCommand().equals(command)) {
onUpload();
}
}
public void onUpload() {
if (flush()) {
if (getProgress() != null) {
return;
}
if (!isResumeUpload) {
initiateNewUpload();
} else {
initiateResumeUpload();
}
}
}
public boolean flush() {
if (validate()) {
diskModel.flush();
DiskImage diskImage = (DiskImage) getDiskModel().getDisk();
diskImage.setActualSizeInBytes(getImageSize());
diskImage.setVolumeFormat(getImageInfoModel().getFormat());
diskImage.setVolumeType(AsyncDataProvider.getInstance().getVolumeType(
diskImage.getVolumeFormat(),
getDiskModel().getStorageDomain().getSelectedItem().getStorageType()));
return true;
} else {
setIsValid(false);
}
return false;
}
public boolean validate() {
boolean uploadImageIsValid;
setIsValid(true);
getInvalidityReasons().clear();
getImageInfoModel().getInvalidityReasons().clear();
if (getImageSourceLocalEnabled().getEntity()) {
getImagePath().validateEntity(new IValidation[] { value -> {
ValidationResult result = new ValidationResult();
if (value == null || StringHelper.isNullOrEmpty((String) value)) {
result.setSuccess(false);
result.getReasons().add(constants.emptyImagePath());
}
return result;
} });
if (getImagePath().getIsValid()) {
getImageInfoModel().validateEntity(new IValidation[]{
value -> {
ValidationResult result = new ValidationResult();
ImageInfoModel.QemuCompat qcowCompat = getImageInfoModel().getQcowCompat();
if (qcowCompat != null && qcowCompat != ImageInfoModel.QemuCompat.V2) {
StorageFormatType storageFormatType = getDiskModel().getStorageDomain().getSelectedItem().getStorageFormat();
switch (storageFormatType) {
case V1:
case V2:
case V3:
result.setSuccess(false);
result.getReasons().add(messages.uploadImageQemuCompatUnsupported(
qcowCompat.getValue(), storageFormatType.name()));
break;
}
}
return result;
}
});
}
uploadImageIsValid = getImagePath().getIsValid() && getImageInfoModel().validate();
getInvalidityReasons().addAll(getImagePath().getInvalidityReasons());
getInvalidityReasons().addAll(getImageInfoModel().getInvalidityReasons());
} else {
// TODO remote/download
uploadImageIsValid = false;
}
return uploadImageIsValid && diskModel.validate();
}
private void initiateNewUpload() {
startProgress(null);
setProgressStr("Initiating new upload"); //$NON-NLS-1$
final TransferDiskImageParameters parameters = createInitParams();
Frontend.getInstance().runAction(VdcActionType.TransferDiskImage, parameters,
result -> {
UploadImageModel model = (UploadImageModel) result.getState();
if (result.getReturnValue().getSucceeded()) {
setCommandId((Guid) result.getReturnValue().getActionReturnValue());
setBytesSent(0);
startStatusPolling();
// The dialog will be closed, but the model's upload code will continue in the background
model.stopProgress();
model.getCancelCommand().execute();
} else {
setProgressStr(messages.uploadImageFailedToStartMessage(result.getReturnValue().getDescription()));
model.stopProgress();
}
}, this);
}
private TransferDiskImageParameters createInitParams() {
Disk newDisk = diskModel.getDisk();
AddDiskParameters diskParameters = new AddDiskParameters(newDisk);
if (diskModel.getDiskStorageType().getEntity() == DiskStorageType.IMAGE ||
diskModel.getDiskStorageType().getEntity() == DiskStorageType.CINDER) {
diskParameters.setStorageDomainId(getDiskModel().getStorageDomain().getSelectedItem().getId());
}
TransferDiskImageParameters parameters = new TransferDiskImageParameters(
diskParameters.getStorageDomainId(),
AsyncDataProvider.getInstance().getUploadImageUiInactivityTimeoutInSeconds(),
diskParameters);
parameters.setTransferSize(getImageSize());
parameters.setVdsId(getDiskModel().getHost().getSelectedItem().getId());
return parameters;
}
private void initiateResumeUpload() {
startProgress(null);
final TransferImageStatusParameters parameters = new TransferImageStatusParameters();
parameters.setDiskId(getDiskModel().getDisk().getId());
Frontend.getInstance().runAction(VdcActionType.TransferImageStatus, parameters,
result -> initiateResumeUploadCheckStatus(result), this);
}
private void initiateResumeUploadCheckStatus(FrontendActionAsyncResult result) {
UploadImageModel model = (UploadImageModel) result.getState();
if (result.getReturnValue() != null && result.getReturnValue().getSucceeded()) {
ImageTransfer rv = result.getReturnValue().getActionReturnValue();
if (rv.getBytesTotal() != getImageSize()) {
if (rv.getBytesTotal() == 0) {
// This upload was generated by the API.
setMessage(messages.uploadImageFailedToResumeUploadOriginatedInAPI());
} else {
setMessage(messages.uploadImageFailedToResumeSizeMessage(rv.getBytesTotal(), getImageSize()));
}
model.stopProgress();
return;
}
// Resumable uploads already have a command running on engine, so get its id and resume it.
ImageTransfer updates = new ImageTransfer();
updates.setPhase(ImageTransferPhase.RESUMING);
final TransferImageStatusParameters parameters = new TransferImageStatusParameters(rv.getId());
parameters.setUpdates(updates);
Frontend.getInstance().runAction(VdcActionType.TransferImageStatus, parameters,
this::initiateResumeUploadStartTransfer, model);
} else {
setProgressStr(messages.uploadImageFailedToResumeMessage(result.getReturnValue().getDescription()));
model.stopProgress();
}
}
private void initiateResumeUploadStartTransfer(FrontendActionAsyncResult result) {
UploadImageModel model = (UploadImageModel) result.getState();
if (result.getReturnValue() != null && result.getReturnValue().getSucceeded()) {
ImageTransfer rv = result.getReturnValue().getActionReturnValue();
setCommandId(rv.getId());
setBytesSent(rv.getBytesSent());
startStatusPolling();
// The dialog will be closed, but the model's upload code will continue in the background.
model.stopProgress();
// For debugging, removing the following line will keep the dialog open so that status set
// by the various setProgressStr() calls will be visible.
model.getCancelCommand().execute();
} else {
setProgressStr(messages.uploadImageFailedToResumeMessage(result.getReturnValue().getDescription()));
model.stopProgress();
}
}
private void startStatusPolling() {
setContinuePolling(true);
manageWindowClosingHandler(true);
Scheduler.get().scheduleFixedDelay(() -> {
log.info("Polling for status"); //$NON-NLS-1$
TransferImageStatusParameters statusParameters = new TransferImageStatusParameters(getCommandId());
// TODO: temp updates from UI until updates from VDSM are implemented
ImageTransfer updates = new ImageTransfer();
updates.setBytesSent(getBytesSent());
updates.setMessage(getMessage() != null ? getMessage() : getProgressStr());
statusParameters.setUpdates(updates);
Frontend.getInstance().runAction(VdcActionType.TransferImageStatus, statusParameters,
result -> respondToPollStatus(result));
if (!getContinuePolling()) {
manageWindowClosingHandler(false);
}
return getContinuePolling();
}, POLLING_DELAY_MS);
}
private void respondToPollStatus(FrontendActionAsyncResult result) {
if (result.getReturnValue() != null && result.getReturnValue().getSucceeded()) {
ImageTransfer rv = result.getReturnValue().getActionReturnValue();
log.info("Upload phase: " + rv.getPhase().toString()); //$NON-NLS-1$
switch (rv.getPhase()) {
case UNKNOWN:
// The job may have failed and removed the entity
pollingFailed();
return;
case INITIALIZING:
case RESUMING:
break;
case TRANSFERRING:
if (getUploadState() == UploadState.NEW) {
setVdsId(rv.getVdsId().toString());
setImageId(rv.getDiskId());
setTransferToken(rv.getImagedTicketId().toString());
String proxyURI = rv.getProxyUri();
String signedTicket = rv.getSignedTicket();
int chunkSizeKB = AsyncDataProvider.getInstance().getUploadImageChunkSizeKB();
int xhrTimeoutSec = AsyncDataProvider.getInstance().getUploadImageXhrTimeoutInSeconds();
int xhrRetryIntervalSec = AsyncDataProvider.getInstance().getUploadImageXhrRetryIntervalInSeconds();
int maxRetries = AsyncDataProvider.getInstance().getUploadImageXhrMaxRetries();
// Start upload task
setUploadState(UploadState.INITIALIZING);
setProgressStr("Uploading from byte " + getBytesSent()); //$NON-NLS-1$
startUpload(getImageFileUploadElement(), proxyURI,
getTransferToken(), getBytesSent(), signedTicket,
chunkSizeKB, xhrTimeoutSec, xhrRetryIntervalSec, maxRetries);
}
break;
case PAUSED_USER:
case PAUSED_SYSTEM:
setContinuePolling(false);
setUploadState(UploadState.ENGINE_PAUSE);
break;
// The frontend may not receive these; the backend code iterates over the cancelled and
// finalizing states, and the image transfer entity is removed upon upload completion.
// In this case, the default case is reached which does largely the same thing.
case CANCELLED:
case FINALIZING_SUCCESS:
case FINALIZING_FAILURE:
case FINISHED_SUCCESS:
case FINISHED_FAILURE:
log.info("Upload task terminating"); //$NON-NLS-1$
setContinuePolling(false);
stopJsUpload(UploadState.ENGINE_CANCEL);
break;
default:
log.info("Unknown upload status from backend, job is likely complete"); //$NON-NLS-1$
setContinuePolling(false);
stopJsUpload(UploadState.CLIENT_ERROR);
break;
}
failedPollAttempts = 0;
} else {
log.info("No poll result for upload status"); //$NON-NLS-1$
pollingFailed();
}
}
void pollingFailed() {
// Not sure what happened to the backend; we'll try a few times and then
// stop polling. If the job is running on the backend, it will then pause.
if (++failedPollAttempts >= MAX_FAILED_POLL_ATTEMPTS) {
log.severe("Polling failed, stopping model execution"); //$NON-NLS-1$
setContinuePolling(false);
stopJsUpload(UploadState.CLIENT_ERROR);
}
}
/**
* Stop execution of the JavaScript upload routine if it is active. If it
* isn't active, the upload state used to control the JS flow is untouched.
*/
private void stopJsUpload(UploadState newUploadState) {
switch (getUploadState()) {
case NEW:
case INITIALIZING:
case TRANSFERRING:
setUploadState(newUploadState);
break;
default:
break;
}
}
private void finalizeImageUpload() {
if (getUploadState() == UploadState.ENGINE_PAUSE) {
log.info("Upload paused; stopping model execution"); //$NON-NLS-1$
return;
}
ImageTransfer updates = new ImageTransfer();
TransferImageStatusParameters statusParameters = new TransferImageStatusParameters(getCommandId(), updates);
if (getUploadState() == UploadState.SUCCESS) {
setProgressStr("Finalizing success..."); //$NON-NLS-1$
statusParameters.getUpdates().setPhase(ImageTransferPhase.FINALIZING_SUCCESS);
}
else if (getUploadState() == UploadState.CLIENT_ERROR) {
setProgressStr("Pausing due to client error"); //$NON-NLS-1$
statusParameters.getUpdates().setPhase(ImageTransferPhase.PAUSED_SYSTEM);
statusParameters.setDiskId(getImageId());
statusParameters.setAuditLogType(auditLogType);
}
else {
setProgressStr("Finalizing failure..."); //$NON-NLS-1$
statusParameters.getUpdates().setPhase(ImageTransferPhase.FINALIZING_FAILURE);
}
log.info("Updating status to " + statusParameters.getUpdates().getPhase()); //$NON-NLS-1$
Frontend.getInstance().runAction(VdcActionType.TransferImageStatus, statusParameters,
result -> {
if (!result.getReturnValue().getSucceeded()) {
if (++failedFinalizationAttempts < MAX_FAILED_POLL_ATTEMPTS) {
finalizeImageUpload();
} else {
setContinuePolling(false);
setProgressStr("Failed to update upload status on engine"); //$NON-NLS-1$
}
}
});
}
/**
* Ensures that a window closing warning is present when uploads are in progress.
*
* @param add True if starting an upload in this window; false if the window is no longer active in the upload process
*/
private void manageWindowClosingHandler(boolean add) {
int uploadCount = adjustTotalUploadCount(add ? 1 : -1);
if (uploadCount == 1) {
HandlerRegistration handlerRegistration = Window.addWindowClosingHandler(event -> {
// If the window is closed, uploads will time out and pause
event.setMessage(constants.uploadImageLeaveWindowPopupWarning());
});
storeHandlerReference(handlerRegistration);
} else if (uploadCount == 0) {
HandlerRegistration handlerRegistration = (HandlerRegistration) getHandlerReference();
handlerRegistration.removeHandler();
}
}
private native int adjustTotalUploadCount(int difference) /*-{
if (typeof $wnd.uploadImageCount == 'undefined') {
$wnd.uploadImageCount = difference;
} else {
$wnd.uploadImageCount += difference;
}
return $wnd.uploadImageCount;
}-*/;
private native void storeHandlerReference(Object handlerRegistration) /*-{
$wnd.uploadImageHandler = handlerRegistration;
}-*/;
private native Object getHandlerReference() /*-{
return $wnd.uploadImageHandler;
}-*/;
public static native boolean browserSupportsUploadAPIs() /*-{
return window.File && window.FileReader && window.Blob ? true : false;
}-*/;
private native double getSizeOfImage(Element fileUploadElement) /*-{
return !fileUploadElement.files.length ? 0 : fileUploadElement.files[0].size;
}-*/;
private long getImageSize() {
return (long) getSizeOfImage(getImageFileUploadElement());
}
private void logDebug(String txt) {
log.fine(txt);
}
private void logInfo(String txt) {
log.info(txt);
}
private void logWarn(String txt) {
log.warning(txt);
}
private void logError(String txt) {
log.severe(txt);
}
private native void startUpload(Element fileUploadElement, String proxyUri,
String resourceId, double startByte, String signedTicket,
int chunkSizeKB, int xhrTimeoutSec, int xhrRetryIntervalSec, int maxRetries) /*-{
var bytesPerMB = 1024 * 1024;
var self = this;
var file;
var startTime;
var bytesSent;
var bytesSentThisRequest;
var bytesToSend;
var xhr;
var chunkErrorCount;
var fileName;
var UploadStates = {
NEW: "NEW",
INITIALIZING: "INITIALIZING",
TRANSFERRING: "TRANSFERRING",
SUCCESS: "SUCCESS",
ENGINE_PAUSE: "ENGINE_PAUSE",
ENGINE_CANCEL: "ENGINE_CANCEL",
CLIENT_ERROR: "CLIENT_ERROR"
};
var log = {
DEBUG: function(t) { self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::logDebug(Ljava/lang/String;)(t); },
INFO: function(t) { self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::logInfo(Ljava/lang/String;)(t); },
WARN: function(t) { self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::logWarn(Ljava/lang/String;)(t); },
ERROR: function(t) { self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::logError(Ljava/lang/String;)(t); }
};
log.INFO("Starting upload to " + proxyUri
+ "\nWith imaged ticket: " + resourceId
+ "\nWith proxy ticket: " + signedTicket);
setProgressStr("Transferring - init");
doUpload(startByte);
function doUpload(startByte) {
log.INFO("doUpload: Starting at byte " + startByte);
if (!fileUploadElement.files.length) {
setUploadStateByString(UploadStates.CLIENT_ERROR);
setErrorMessage('No file selected');
return;
}
file = fileUploadElement.files[0];
fileName = file.name;
log.INFO('doUpload: Selected file: ' + file.name + ' (size: ' + file.size + ' bytes)');
chunkErrorCount = 0;
bytesSentThisRequest = 0;
startTime = performance.now();
bytesSent = startByte;
setUploadStateByString(UploadStates.TRANSFERRING);
sendChunk();
}
function sendChunk() {
if (getUploadStateString() != UploadStates.TRANSFERRING) {
finalizeUpload();
return;
}
log.DEBUG('sendChunk: Sending from byte ' + bytesSent);
bytesToSend = Math.min(file.size - bytesSent, chunkSizeKB * 1024);
bytesSentThisRequest = 0;
if (xhr == undefined) {
log.DEBUG('sendChunk: Initializing xhr');
xhr = new XMLHttpRequest();
// The load event is triggered when xhr has uploaded all the data, whereas readystatechange is
// triggered when the remote endpoint closes the connection. We want the latter for transferring; see:
// http://stackoverflow.com/questions/15418608/xmlhttprequest-level-2-determinate-if-upload-finished
xhr.onreadystatechange = onStateChangeHandler;
xhr.upload.addEventListener('progress', xhrProgress, false);
}
var address = proxyUri + '/' + resourceId;
var contentRange = 'bytes ' + bytesSent + '-' + (bytesSent + bytesToSend - 1) + '/' + file.size;
xhr.open('PUT', address);
xhr.timeout = xhrTimeoutSec * 1000; // Must be set after xhr.open()
xhr.setRequestHeader('Cache-Control', 'no-cache');
xhr.setRequestHeader('Pragma', 'no-cache');
xhr.setRequestHeader('Content-Range', contentRange);
xhr.setRequestHeader('Authorization', signedTicket);
log.INFO('sendChunk: PUT ' + address + ' ' + contentRange);
xhr.send(file.slice(bytesSent, bytesSent + bytesToSend, 'application/octet-stream'));
}
function onStateChangeHandler() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status == 204 || xhr.status == 206) {
log.DEBUG('xhrHandle: Status: ' + xhr.status
+ ', text: ' + xhr.responseText
+ ', response: ' + xhr.response);
bytesSent += getBytesFromResponse(bytesToSend);
if (file.size == 0) {
log.ERROR('Error reading selected file (' + fileName + '). ' + 'Perhaps it has been deleted?');
chunkErrorCount = maxRetries
xhrError();
return;
}
if (getUploadStateString() == UploadStates.CLIENT_ERROR) {
finalizeUpload();
} else if (bytesSent < file.size) {
chunkErrorCount = 0;
setBytesSent(bytesSent);
sendChunk();
} else {
elapsed = (performance.now() - startTime) / 1000;
bytesPerSec = (elapsed > 0 ? file.size / elapsed : file.size);
log.INFO('xhrHandle: Finished transfer in ' + elapsed + ' seconds, '
+ bytesPerSec / bytesPerMB + ' MB per second');
setUploadStateByString(UploadStates.SUCCESS);
finalizeUpload();
}
} else {
log.ERROR('xhrHandle: Status: ' + xhr.status
+ ', text: ' + xhr.responseText
+ ', response: ' + xhr.response);
xhrError();
}
}
}
function getBytesFromResponse(bytesToSend) {
range = xhr.getResponseHeader('Range');
if (range != null) {
// Parse the range header; the byte range x-y is inclusive.
m = range.match(/bytes=(\d+)-(\d+)/i);
log.DEBUG('getBytesFromResponse: ' + m);
if (m != null) {
return m[2] - m[1] + 1;
}
log.ERROR('Invalid Range header from client');
setErrorMessage('Transfer failed: invalid Range header received from proxy');
setUploadStateByString(UploadStates.CLIENT_ERROR);
}
return bytesToSend;
}
function xhrProgress(e) {
if (e.lengthComputable) {
bytesSentThisRequest = e.loaded;
updateProgress();
}
if (getUploadStateString() != UploadStates.TRANSFERRING) {
xhr.abort();
}
}
function xhrError() {
log.ERROR('xhrError: ' + xhr.status + ' ' + xhr.statusText);
if (chunkErrorCount < maxRetries) {
chunkErrorCount++;
log.WARN('xhrError: Retrying (attempt ' + chunkErrorCount + ' of ' + maxRetries + ')');
bytesSentThisRequest = 0;
updateProgress();
setTimeout(sendChunk, xhrRetryIntervalSec * 1000);
} else {
log.ERROR('Transfer failed after ' + chunkErrorCount + '/' + maxRetries + ' errors');
setErrorMessage('Transfer to proxy failed, code: ' + xhr.status + ', text: ' + xhr.responseText + ', response: ' + xhr.response);
setUploadStateByString(UploadStates.CLIENT_ERROR);
setAuditLogMessageByXhrError();
finalizeUpload();
}
}
function updateProgress() {
// This is mostly useful to track data within the JS; the engine will get updates through vdsm
var bytes = bytesSent + bytesSentThisRequest;
setBytesSent(bytes);
}
function finalizeUpload() {
log.WARN('Finalizing upload with status ' + getUploadStateString());
updateProgress();
self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::finalizeImageUpload()();
}
function setProgressStr(txt) {
self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::setProgressStr(Ljava/lang/String;)(txt);
}
function setErrorMessage(msg) {
self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::setErrorMessage(Ljava/lang/String;)(msg);
}
function setBytesSent(bytes) {
self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::setBytesSent(D)(bytes);
}
function getUploadStateString() {
return self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::getUploadStateString()();
}
function setUploadStateByString(state) {
self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::setUploadStateByString(Ljava/lang/String;)(state);
}
function setAuditLogMessageByXhrError() {
// According to xhr specifications, all network errors set the status to 0.
if (xhr.status == 0) {
setAuditLogMessage(@org.ovirt.engine.core.common.AuditLogType::UPLOAD_IMAGE_NETWORK_ERROR);
}
else {
setAuditLogMessage(@org.ovirt.engine.core.common.AuditLogType::UPLOAD_IMAGE_CLIENT_ERROR);
}
}
function setAuditLogMessage(auditLogType) {
self.@org.ovirt.engine.ui.uicommonweb.models.storage.UploadImageModel::setAuditLogType(Lorg/ovirt/engine/core/common/AuditLogType;)(
auditLogType
);
}
}-*/;
/**
* Build and display the Upload Image dialog.
*
* @param parent Parent model
* @param helpTag Help tag (dependent upon location in UI)
* @param limitToStorageDomainId Pre-selected storage domain, or null to not limit selection
* @param resumeUploadDisk DiskImage corresponding to upload being resumed, or null for new upload
*/
public static <T extends Model & ICommandTarget> void showUploadDialog(
T parent,
HelpTag helpTag,
Guid limitToStorageDomainId,
DiskImage resumeUploadDisk) {
UploadImageModel model = new UploadImageModel(limitToStorageDomainId, resumeUploadDisk);
model.setTitle(resumeUploadDisk == null
? ConstantsManager.getInstance().getConstants().uploadImageTitle()
: ConstantsManager.getInstance().getConstants().uploadImageResumeTitle());
model.setHelpTag(helpTag);
model.setHashName("upload_disk_image"); //$NON-NLS-1$
UICommand cancelCommand = UICommand.createCancelUiCommand("Cancel", parent); //$NON-NLS-1$
model.setCancelCommand(cancelCommand);
model.getCommands().add(cancelCommand);
parent.setWindow(model);
model.initialize();
}
/**
* Display the cancellation dialog for Image Upload, showing the list of selected
* items which will be cancelled upon confirmation. The parent model must have an
* "OnCancelUpload" UICommand handler defined, which should call onCancelUpload().
*
* @param parent Parent model
* @param helptag Help tag (dependent upon location in UI)
* @param images List of selected images
* @param <T> Model which implements ICommandTarget
*/
public static <T extends Model & ICommandTarget> void showCancelUploadDialog(
T parent,
HelpTag helptag,
List<? extends Disk> disks) {
ConfirmationModel model = new ConfirmationModel();
model.setTitle(ConstantsManager.getInstance().getConstants().uploadImageCancelTitle());
model.setHelpTag(helptag);
model.setHashName("cancel_upload_image"); //$NON-NLS-1$
model.setMessage(ConstantsManager.getInstance().getConstants().uploadImageCancelConfirmationMessage());
parent.setWindow(model);
ArrayList<String> items = new ArrayList<>();
for (Disk disk : disks) {
items.add(disk.getDiskAlias());
}
model.setItems(items);
UICommand okCommand = new UICommand("OnCancelUpload", parent); //$NON-NLS-1$
okCommand.setTitle(ConstantsManager.getInstance().getConstants().ok());
okCommand.setIsDefault(true);
model.getCommands().add(okCommand);
UICommand cancelCommand = UICommand.createCancelUiCommand("Cancel", parent); //$NON-NLS-1$
cancelCommand.setIsCancel(true);
model.getCommands().add(cancelCommand);
}
public static void onCancelUpload(ConfirmationModel model, List<? extends Disk> disks) {
if (model.getProgress() != null) {
return;
}
model.startProgress(null);
ArrayList<VdcActionParametersBase> list = new ArrayList<>();
for (Disk disk : disks) {
ImageTransfer updates = new ImageTransfer();
updates.setPhase(ImageTransferPhase.CANCELLED);
TransferImageStatusParameters parameters = new TransferImageStatusParameters();
parameters.setUpdates(updates);
parameters.setDiskId(disk.getId());
list.add(parameters);
}
Frontend.getInstance().runMultipleAction(VdcActionType.TransferImageStatus, list,
result -> {
ConfirmationModel localModel = (ConfirmationModel) result.getState();
localModel.stopProgress();
localModel.getCancelCommand().execute(); //parent.cancel();
}, model);
}
public static void pauseUploads(List<? extends Disk> disks) {
ArrayList<VdcActionParametersBase> list = new ArrayList<>();
for (Disk disk : disks) {
ImageTransfer updates = new ImageTransfer();
updates.setPhase(ImageTransferPhase.PAUSED_USER);
TransferImageStatusParameters parameters = new TransferImageStatusParameters();
parameters.setUpdates(updates);
parameters.setDiskId(disk.getId());
list.add(parameters);
}
Frontend.getInstance().runMultipleAction(VdcActionType.TransferImageStatus, list);
}
public static boolean isCancelAllowed(List<? extends Disk> disks) {
if (disks == null || disks.isEmpty()) {
return false;
}
for (Disk disk : disks) {
if (!(disk instanceof DiskImage)
|| disk.getImageTransferPhase() == null
|| !disk.getImageTransferPhase().canBeCancelled()
|| isImageUploadViaAPI((DiskImage) disk)) {
return false;
}
}
return true;
}
public static boolean isPauseAllowed(List<? extends Disk> disks) {
if (disks == null || disks.isEmpty()) {
return false;
}
for (Disk disk : disks) {
if (!(disk instanceof DiskImage)
|| disk.getImageTransferPhase() == null
|| !disk.getImageTransferPhase().canBePaused()
|| isImageUploadViaAPI((DiskImage) disk)) {
return false;
}
}
return true;
}
public static boolean isResumeAllowed(List<? extends Disk> disks) {
return disks != null
&& disks.size() == 1
&& disks.get(0) instanceof DiskImage
&& disks.get(0).getImageTransferPhase() != null
&& disks.get(0).getImageTransferPhase().canBeResumed()
&& !isImageUploadViaAPI((DiskImage) disks.get(0));
}
private static boolean isImageUploadViaAPI(DiskImage diskImage) {
return diskImage.getImageTransferPhase() == ImageTransferPhase.TRANSFERRING
&& diskImage.getImageTransferBytesTotal() == 0;
}
public ImageInfoModel getImageInfoModel() {
return imageInfoModel;
}
}