package ftp; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.nio.charset.StandardCharsets; import java.util.Map; import javax.swing.JOptionPane; import javax.swing.SwingWorker; import models.Image; import models.SessionData; import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.apache.thrift.TException; import org.openslx.filetransfer.DataReceivedCallback; import org.openslx.filetransfer.Downloader; import org.openslx.filetransfer.FileRange; import org.openslx.filetransfer.WantRangeCallback; import org.openslx.thrifthelper.ThriftManager; import util.ResourceLoader; /** * Execute file download in a background thread and update the progress. * * @author www.codejava.net * */ public class DownloadTask extends SwingWorker { /** * Logger instance for this class. */ private final static Logger LOGGER = Logger.getLogger(DownloadTask.class); private static final double UPDATE_INTERVAL_SECONDS = 0.6; private static final double UPDATE_INTERVAL_MS = UPDATE_INTERVAL_SECONDS * 1000; private static final double BYTES_PER_MIB = 1024 * 1024; private static final long CHUNK_SIZE = 16 * 1024 * 1024; private final String host; private final int port; private final String downloadToken; private final String saveDir; private final long fileSize; private boolean success = false; public DownloadTask(String host, int port, String downloadToken, String saveDir, long fileSize) { this.host = host; this.port = port; this.downloadToken = downloadToken; this.saveDir = saveDir; this.fileSize = fileSize; } class Callbacks implements WantRangeCallback, DataReceivedCallback { // initialize the counters needed for speed calculations private long currentRequestedOffset = -1; private long totalBytesRead = 0; private long lastUpdate = 0; private long lastBytes = 0; private long currentBytes = 0; private final RandomAccessFile file; public Callbacks(RandomAccessFile file) { this.file = file; } @Override public FileRange get() { if (currentRequestedOffset == -1) currentRequestedOffset = 0; else currentRequestedOffset += CHUNK_SIZE; if (currentRequestedOffset >= fileSize) return null; long end = currentRequestedOffset + CHUNK_SIZE; if (end > fileSize) end = fileSize; return new FileRange(currentRequestedOffset, end); } @Override public boolean dataReceived(final long fileOffset, final int dataLength, final byte[] data) { try { file.seek(fileOffset); file.write(data, 0, dataLength); } catch (Exception e) { LOGGER.error("Could not write to file at offset " + fileOffset, e); return false; } currentBytes += dataLength; totalBytesRead += dataLength; final long now = System.currentTimeMillis(); if (lastUpdate + UPDATE_INTERVAL_MS < now) { final int percentCompleted = (int) ((totalBytesRead * 100) / fileSize); setProgress(percentCompleted); lastBytes = (lastBytes * 2 + currentBytes) / 3; final double speed = lastBytes / UPDATE_INTERVAL_SECONDS; firePropertyChange("speed", 0, speed / BYTES_PER_MIB); firePropertyChange("bytesread", 0, totalBytesRead); lastUpdate = now; currentBytes = 0; } return true; } } /** * Executed in background thread */ @Override protected Void doInBackground() throws Exception { boolean ret = false; // show filesize in the GUI firePropertyChange("filesize", 0, fileSize); Downloader download = null; RandomAccessFile file = null; try { download = new Downloader(host, port, null, downloadToken); // TODO: SSL try { file = new RandomAccessFile(new File(saveDir), "rw"); } catch (Exception e2) { JOptionPane.showMessageDialog(null, "Could not open destination file:\n" + saveDir + "\n" + e2.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); e2.printStackTrace(); setProgress(0); return null; } Callbacks cb = new Callbacks(file); ret = download.download(cb, cb); } finally { if (file != null) { try { file.close(); } catch (Exception e) { } } if (download != null) download.close(null); } // if the download succeeded, set the progress to 100% manually again here to make // sure the GUI knows about it. if (ret) { setProgress(100); firePropertyChange("bytesread", 0, fileSize); firePropertyChange("success", false, true); success = true; } return null; } /** * Executed in Swing's event dispatching thread */ @Override protected void done() { if (isCancelled()) return; if (success) { LOGGER.info("Datei erfolgreich heruntergeladen."); String vmxResult = ""; vmxResult = generateVmx() ? "Passende VMX generiert." : "Keine passende VMX generiert!"; JOptionPane.showMessageDialog(null, "Datei erfolgreich heruntergeladen. " + vmxResult, "Message", JOptionPane.INFORMATION_MESSAGE); } else { LOGGER.error("Datei wurde unvollständig heruntergeladen."); JOptionPane.showMessageDialog(null, "Datei wurde unvollständig heruntergeladen. Bitte wiederholen.", "Message", JOptionPane.INFORMATION_MESSAGE); } } /** * Helper to generate the vmx for the downloaded image * TODO: Not really related to DownloadTask... * * @return true|false indicating the success of the file creation */ private boolean generateVmx() { String vmxTemplate = ResourceLoader.getTextFile("/txt/vmx_template"); // TODO: sanity checks on vmxTemplate would be good here... just to be safe // now we replace the placeholder variables with the real data // for this, we first need to get the image information from the server LOGGER.debug("Image's ID: " + Image.ImageId); Map imageData = null; try { imageData = ThriftManager.getSatClient().getImageData(Image.ImageId, Image.Version, SessionData.authToken); } catch (TException e) { LOGGER.error("Thrift exception during transfer, see trace: ", e); return false; } // sanity check, shouldn't happen. if (imageData == null) { LOGGER.error("Could not query the image information from the server!"); LOGGER.error("Image's ID: " + Image.ImageId); LOGGER.error("Image's version: " + Image.Version); return false; } int hardwareVersion = extractHardwareVersion(saveDir + File.separator + imageData.get("path").replaceFirst("^prod/", "")); if (hardwareVersion == 0) { LOGGER.error("'extractHardwareVersion' returned 0 indicating some problem. See logs."); LOGGER.error("Falling back to default hardware version of '10'."); hardwareVersion = 10; } // TODO: sanity checks on the content of imageData would be good here... // use the information we received about the image vmxTemplate = vmxTemplate.replace("%VM_DISPLAY_NAME%", imageData.get("name")); vmxTemplate = vmxTemplate.replace("%VM_GUEST_OS%", imageData.get("os")); vmxTemplate = vmxTemplate.replace("%VM_CPU_COUNT%", imageData.get("cpu")); vmxTemplate = vmxTemplate.replace("%VM_RAM_SIZE%", String.valueOf(Integer.valueOf(imageData.get("ram")) * 1024)); vmxTemplate = vmxTemplate.replace("%VM_DISK_PATH%", imageData.get("path").replaceFirst("^prod/", "")); vmxTemplate = vmxTemplate.replace("%VM_HW_VERSION%", String.valueOf(hardwareVersion)); // build filename for the vmx, basicly the same as the path of the vmdk // just without the leading "prod/" and "vmx" instead of "vmdk" at the end. String targetFilename = saveDir + File.separator + imageData.get("path").replaceFirst("^prod/", "").replaceFirst("\\.vmdk$", "") + ".vmx"; try { // try to write it to file FileUtils.writeStringToFile(new File(targetFilename), vmxTemplate, StandardCharsets.UTF_8); } catch (IOException e) { LOGGER.error("Could not write vmx-template to '" + targetFilename + "'. See trace: ", e); return false; } return true; } /** * Helper to extract the hardware version of the VMDK file by inspecting its * content. * * @return value of hardware version as integer. A return value of 0 * indicates * an error. */ private int extractHardwareVersion(String path) { BufferedReader br = null; try { try { br = new BufferedReader(new InputStreamReader(new FileInputStream(path))); String line; // first 4 characters of a VMDK file start with 'KDMV' // first lets check if this is the case line = br.readLine(); if (!line.subSequence(0, 4).equals("KDMV")) { LOGGER.error("Did not see 'KDMV' as first chars of the VMDK! Returning 0."); LOGGER.debug("First line was: " + line); LOGGER.debug("First 4 characters of it: " + line.subSequence(0, 4)); return 0; } // only read a maximum of 25 lines, just in case... int round = 0; while ((line = br.readLine()) != null && round < 25) { if (line.startsWith("ddb.virtualHWVersion")) { String[] tmp = line.split("="); // we should get 2 strings only after the split, lets be sure if (tmp.length != 2) { LOGGER.debug("Splitting returned more than 2 parts, this should not happen!"); return 0; } int candidate = Integer.parseInt(tmp[1].trim().replace("\"", "")); LOGGER.debug("Considering hardware version: " + candidate); if (candidate > 0) { LOGGER.debug("Valid value of the candidate. Using hardware version of: " + candidate); return candidate; } else { LOGGER.error("Candidate is not > 0! Returning 0."); return 0; } } round++; } LOGGER.error("Failed to find hardware version. Tried " + round + " rounds."); } catch (NumberFormatException e) { // Not a number? return 0; } finally { br.close(); } } catch (FileNotFoundException e) { LOGGER.debug("File not found, see trace: ", e); } catch (IOException e) { LOGGER.debug("I/O Exception, see trace: ", e); } return 0; } }