io.github.eternalbits.compactvd.CompactVD.java Source code

Java tutorial

Introduction

Here is the source code for io.github.eternalbits.compactvd.CompactVD.java

Source

/*
 * Copyright 2016 Rui Baptista
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.github.eternalbits.compactvd;

import java.awt.GraphicsEnvironment;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.channels.FileLock;
import java.util.Arrays;
import java.util.List;

import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

import io.github.eternalbits.compactvd.gui.FrontEnd;
import io.github.eternalbits.disk.DiskImage;
import io.github.eternalbits.disk.DiskImageJournal;
import io.github.eternalbits.disk.DiskImageObserver;
import io.github.eternalbits.disk.DiskImageProgress;
import io.github.eternalbits.disks.DiskImages;

/**
 * Virtual Disk Compact and Copy command line interface. Without parameters
 * the GUI takes precedence, use {@code --help} to get the command list.
 * <p>
 */
public class CompactVD implements DiskImageObserver {

    // TODO: move this stuff to DiskImage[s] and merge with ListItem methods

    private Thread mainThread = null;
    private int task = DiskImageProgress.NO_TASK;

    private final static String SEARCHING_SPACE = "Searching space not in use or zero filled";
    private final static String IMAGE_CHANGED = "The disk image '%s' was changed.";
    private final static String IMAGE_CREATED = "The disk image '%s' was created in '%s' directory.";
    private final static String IMAGE_NOT_CHANGED = "The disk image '%s' was NOT changed.";
    private final static String IMAGE_NOT_CREATED = "No disk image was created.";
    private boolean verbose;

    private boolean isCancelled() {
        return mainThread.isInterrupted();
    }

    private void verboseProgress(String verb) {
        if (verbose)
            System.out.println(verb);
    }

    private void showView(File file, int options) throws IOException {
        task = DiskImageProgress.OPTIMIZE;
        try (DiskImage image = DiskImages.open(file, "r")) {
            verboseProgress(SEARCHING_SPACE);
            image.addObserver(this, false);
            image.optimize(options);
            image.removeObserver(this);
            dump(image.getView());
        }
        task = DiskImageProgress.NO_TASK;
    }

    private void compact(File file, int options) throws IOException {
        long mtime = file.lastModified();
        task = DiskImageProgress.COMPACT;
        try (RandomAccessFile check = new RandomAccessFile(file, "r")) { // is file?
            try (DiskImage image = DiskImages.open(file, "rw")) {
                FileLock fileLock = image.tryLock();
                verboseProgress(SEARCHING_SPACE);
                image.addObserver(this, false);
                image.optimize(options);
                image.removeObserver(this);
                if (!isCancelled()) {
                    verboseProgress("Compacting " + file.getName());
                    image.addObserver(this, false);
                    image.compact();
                    image.removeObserver(this);
                }
                fileLock.release();
            }
        } finally {
            System.out.println(String.format(mtime != file.lastModified() ? IMAGE_CHANGED : IMAGE_NOT_CHANGED,
                    file.getName()));
        }
    }

    private void copy(File from, int options, File to, String type) throws IOException {
        long mtime = to.lastModified();
        task = DiskImageProgress.COPY;
        File copy = null;
        try (RandomAccessFile check = new RandomAccessFile(from, "r")) { // is file?
            // If writable, source is open in write mode for an exclusive file lock
            String mode = from.canWrite() ? "rw" : "r";
            try (DiskImage image = DiskImages.open(from, mode)) {
                if (type == null)
                    type = image.getType();
                try (DiskImage clone = DiskImages.create(type, to, image.getDiskSize())) {
                    copy = to; // copy open by DiskImage
                    FileLock source = null;
                    if (mode.equals("rw"))
                        source = image.tryLock();
                    verboseProgress(SEARCHING_SPACE);
                    image.addObserver(this, false);
                    image.optimize(options);
                    image.removeObserver(this);
                    if (!isCancelled()) {
                        verboseProgress("Copying " + from.getName() + " to " + to.getName());
                        FileLock fileLock = clone.tryLock();
                        clone.addObserver(this, false);
                        clone.copy(image);
                        clone.removeObserver(this);
                        fileLock.release();
                    }
                    if (source != null)
                        source.release();
                    if (!isCancelled()) {
                        copy = null;
                    }
                }
            }
        } finally {
            if (copy != null && copy.isFile())
                copy.delete();
            System.out.println(mtime != to.lastModified()
                    ? String.format(IMAGE_CREATED, to.getName(), to.getAbsoluteFile().getParent())
                    : IMAGE_NOT_CREATED);
        }
    }

    private String lastProgress = null;
    private char[] bar = new char[58];

    private int boundedInt(float f, int m) {
        int b = Math.round(f * m);
        if (b > m)
            b = m;
        if (b < 0)
            b = 0;
        return b;
    }

    @Override
    public void update(DiskImage image, Object arg) {
        if (arg instanceof DiskImageProgress) {
            DiskImageProgress progress = (DiskImageProgress) arg;
            Arrays.fill(bar, '.');
            Arrays.fill(bar, 0, boundedInt(progress.value, bar.length), '#');
            String pb = String.format("\rProgress: [%3d%%] [%s]", boundedInt(progress.value, 100),
                    String.valueOf(bar));
            if (!isCancelled() && !pb.equals(lastProgress)) {
                System.out.print(pb);
                lastProgress = pb;
            }
            if (progress.done) {
                if (verbose || progress.task == task)
                    System.out.println();
                lastProgress = null;
            }
        }
    }

    private static final String version = "1.4";
    private static final String jar = new java.io.File(
            CompactVD.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getName();

    private static final String FILE_ALREADY_EXISTS = "File \"%s\" already exists";

    public static void main(String[] args) throws Exception {

        DiskImageJournal.scanDirectory(Static.getWorkingDirectory());

        if (args.length == 0 && !GraphicsEnvironment.isHeadless()) {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    new FrontEnd();
                }
            });
            return;
        }

        new CompactVD().commandLine(args);

    }

    private static Options buildHelpers() {
        Options helpers = new Options();
        helpers.addOption(Option.builder("h").longOpt("help").desc("print this help and exit").build());
        helpers.addOption(Option.builder().longOpt("version").desc("print version information and exit").build());
        return helpers;
    }

    private static Options buildOptions() {
        Options options = new Options();
        OptionGroup source = new OptionGroup();
        source.addOption(Option.builder("i").longOpt("inplace").desc("compact <src> image file in place").hasArgs()
                .argName("src").build());
        source.addOption(Option.builder("c").longOpt("copy").desc("copy <src> to a new, optimized image").hasArgs()
                .argName("src").build());
        source.addOption(Option.builder("d").longOpt("dump").desc("print <src> disk image details").hasArgs()
                .argName("src").build());
        source.setRequired(true);
        options.addOptionGroup(source);
        options.addOptionGroup(new OptionGroup()
                .addOption(Option.builder("u").longOpt("drop-unused")
                        .desc("drop space not in use by system and files").build())
                .addOption(Option.builder("U").longOpt("keep-unused")
                        .desc("keep space not in use by system and files").build()));
        options.addOptionGroup(new OptionGroup()
                .addOption(Option.builder("z").longOpt("drop-zeroed").desc("drop space filled with zeros").build())
                .addOption(
                        Option.builder("Z").longOpt("keep-zeroed").desc("keep space filled with zeros").build()));
        options.addOption(Option.builder("w").longOpt("write").desc("set <out> as destination file for copy")
                .hasArgs().argName("out").build());
        options.addOption(Option.builder("f").longOpt("format")
                .desc("copy output format: VDI, VMDK, VHD or IMG|RAW").hasArgs().argName("fmt").build());
        options.addOption(Option.builder("o").longOpt("overwrite").desc("overwrite existing file on copy").build());
        options.addOption(Option.builder("v").longOpt("verbose").desc("explain what is being done").build());
        return options;
    }

    private void commandLine(String[] args) {
        mainThread = Thread.currentThread();

        Runtime.getRuntime().addShutdownHook(new Thread() { // Ctrl+C
            @Override
            public void run() {
                try {
                    mainThread.interrupt();
                    mainThread.join();
                } catch (InterruptedException e) {
                }
            }
        });

        Options options = buildOptions();
        Options helpers = buildHelpers();
        for (Option opt : helpers.getOptions()) {
            options.addOption(opt);
        }
        try {
            CommandLine cmd = new DefaultParser().parse(helpers, args, true);
            if (cmd.hasOption("version")) {
                printAbout();
                return;
            }
            if (cmd.hasOption("h")) {
                printHelp(options);
                return;
            }

            cmd = new DefaultParser().parse(options, args);

            int opt = DiskImage.FREE_BLOCKS_UNUSED;
            if (cmd.hasOption("U"))
                opt &= ~DiskImage.FREE_BLOCKS_UNUSED;
            if (cmd.hasOption("z"))
                opt |= DiskImage.FREE_BLOCKS_ZEROED;

            verbose = cmd.hasOption("v");

            if (cmd.hasOption("d")) {
                showView(new File(cmd.getOptionValue("d")), opt);
                return;
            }

            if (cmd.hasOption("i")) {
                compact(new File(cmd.getOptionValue("i")), opt);
                return;
            }

            if (cmd.hasOption("c")) {
                if (!cmd.hasOption("w"))
                    throw new MissingOptionException(Arrays.asList(new String[] { "w" }));
                File to = new File(cmd.getOptionValue("w"));

                if (!cmd.hasOption("o") && to.exists())
                    throw new IOException(String.format(FILE_ALREADY_EXISTS, to));

                String f = cmd.hasOption("f") ? cmd.getOptionValue("f") : null;
                copy(new File(cmd.getOptionValue("c")), opt, to, f);
                return;
            }

        } catch (ParseException | IOException e) {
            printHelp(options);
            System.out.println("\n\n" + Static.simpleString(e));
            System.exit(1);
        }

    }

    private static void printAbout() {
        System.out.println("CompactVD version " + version + " copyright 2016 Rui Baptista");
        System.out.println("Licensed under the Apache License, Version 2.0.");
    }

    private static void printHelp(Options options) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.setSyntaxPrefix("Usage: ");
        final String prefix = "--";
        String header = "\nTo reduce the size of dynamic disk images. Version " + version + "\n\n";
        String footer = ("\nOne of ^inline, ^copy or ^dump is required. For ^inline and ^dump"
                + " the default options are ^drop-unused ^keep-zeroed. For ^copy the default"
                + " is ^drop-unused and ^drop-zeroed is implied. Not applicable arguments" + " are ignored.\n")
                        .replace("^", prefix);
        formatter.setLongOptPrefix(" " + prefix);
        formatter.printHelp("java -jar " + jar, header, options, footer, true);
    }

    public static void dump(Object obj) {
        dump(obj, "");
    }

    private static void dump(Object obj, String in) {
        for (Field fld : obj.getClass().getDeclaredFields()) {
            try {
                if (!Modifier.isPrivate(fld.getModifiers())) {
                    if (fld.getAnnotation(Deprecated.class) == null) {
                        if (!fld.getType().isAssignableFrom(List.class)) {
                            System.out.println(in + fld.getName() + ": " + fld.get(obj));
                        } else {
                            int i = 0;
                            for (Object item : (List<?>) fld.get(obj)) {
                                System.out.println(in + fld.getName() + "[" + i + "]");
                                dump(item, in + "    ");
                                i++;
                            }
                        }
                    }
                }
            } catch (IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}