Search code examples
javalinuxterminal7zip

Compress a folder using 7-zip in Linux with Java


I have the following function in my code.

public static void runExecutable(String filePath, String command, boolean commandLine, boolean wait) {
    
    InputStream inputStream = CommonMethods.class.getClassLoader().getResourceAsStream(filePath);
    String tempFileSuffix = "";
    switch (CommonMethods.getOperatingSystem()) {
        case "Windows":
            tempFileSuffix = ".exe";
            break;
        case "Linux":
        case "Mac":
            tempFileSuffix = ".bin";
            break;
    }
    
    
    
    File tempFile;
    try {
        tempFile = File.createTempFile("AutoFPCurator", tempFileSuffix);
    } catch (IOException e) {
        new ErrorDialog(e);
        return;
    }

    
    try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
        byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) > 0) {
            outputStream.write(buffer, 0, length);
        }
        outputStream.close();
    } catch (IOException e) {
        new ErrorDialog(e);
        return;
    }
    
    
    
    
    String cmdProgram = "";
    String cFlag = "-c";
    switch (CommonMethods.getOperatingSystem()) {
        case "Windows": 
            cmdProgram = "cmd.exe";
            cFlag = "/c";
            break;
        case "Mac":
            cmdProgram = "sh";
            break;
        case "Linux":
            cmdProgram = "bash";
            break;
        default:
            return;
    }
    
    ProcessBuilder processBuilder = !commandLine ? new ProcessBuilder(tempFile.getAbsolutePath(), command)
                            : new ProcessBuilder(cmdProgram, cFlag, tempFile.getAbsolutePath(), command);
    processBuilder.redirectErrorStream(true);
    
    
    Process process;
    try {
        process = processBuilder.start();
    } catch (IOException e) {
        new ErrorDialog(e);
        return;
    }
    if (!wait) {
        return;
    }
    
    
    try {
        process.waitFor();
    } catch (InterruptedException e) {
        new ErrorDialog(e);
    }
    
    
}

This code is designed to run executables from src/main/resources/, as it specifies, however it's especially used to compress folders into 7z files (using standalone 7z executables in its src/main/resources directory. I use executables because 7Z compression algorithms made for Java (such as Apache Commons's), doesn't compress it the way I need it to be compressed for my purposes, unlike the executable. I downloaded the Linux 7z executable, extracted it, and used 7zz in my program). When I call it like so:

switch (CommonMethods.getOperatingSystem()) {
         case "Windows":
             exeString = "programs/7zip/7za.exe";
             break;
         case "Mac":
             exeString = "programs/7zip/7zzmac";
             break;
         case "Linux":
             exeString = "programs/7zip/7zznux";
             break;
         default:
             System.out.println(commonStrs.get("unsupportedOS"));
             return;
    }
    CommonMethods.runExecutable(exeString, args, true, true);

It works on Windows, but I tried it on Linux and it didn't work. I've been trying to pinpoint the problem. The arguments are valid and it does appear to be running, but it just doesn't compress the folder. I ran the raw argument that the function builds myself into Linux Terminal and it said "Permission denied" in response to my attempt to access to the tmp folder (which is where the temporary 7zip executable is stored). I tried storing it in an alternate location that is perfectly accessible to no avail.

TL;DR, how do I get this working in Linux (and Mac by extension maybe?)? Just to be clear, this works on Windows.


Solution

  • On macOS (Ventura 13.4), I downloaded the executable for 7zz. I then wrote a quick program to verify that I could run the executable directly via ProcessBuilder (no need for a sh). This was successful.

    I then added 7zz to my applications resources and proceeded to extract it to a temp file. This failed as the file was not executable (File#canExecute was false).

    Then, based on How do I programmatically change file permissions?, I modified the execution attribute of the temp file (after it was extracted) and this was successful

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.nio.file.Files;
    import java.nio.file.attribute.PosixFilePermissions;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    public class Main {
        public static void main(String[] args) {
            try {
                new Main();
            } catch (IOException ex) {
                Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InterruptedException ex) {
                Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    
        public Main() throws IOException, InterruptedException {
            File binary = extractBinary();
            System.out.println(binary);
            System.out.println("Executable: " + binary.canExecute());
            // This could be moved to extractBinary
            // But I wanted to demonstrate the difference between
            // the two binary.canExecute() statements
            Files.setPosixFilePermissions(binary.toPath(), PosixFilePermissions.fromString("rwxr-x---"));
            System.out.println("Executable: " + binary.canExecute());
    
            ProcessBuilder pb = new ProcessBuilder(
                    binary.getAbsolutePath(), 
                    "a", 
                    "/Users/shane.whitehead/Downloads/Test.7z", 
                    "/Users/shane.whitehead/Downloads/7z2300-mac.tar/*"
            );
            pb.redirectErrorStream(true);
            Process p = pb.start();
            InputStreamConsumer consumer = new InputStreamConsumer(p.getInputStream());
            consumer.start();
    
            p.waitFor();
    
            consumer.join();
            System.out.println(consumer.getOutput());
        }
    
        protected File extractBinary() throws IOException {
            File tempFile = File.createTempFile("7zBinary", ".bin");
            try (FileOutputStream fos = new FileOutputStream(tempFile); InputStream is = getClass().getResourceAsStream("/bin/7zz")) {
                is.transferTo(fos);
            }
            return tempFile;
        }
    
        // This is just here to make my life easier
        public class InputStreamConsumer extends Thread {
    
            private InputStream is;
            private IOException exp;
            private StringBuilder output;
    
            public InputStreamConsumer(InputStream is) {
                this.is = is;
            }
    
            @Override
            public void run() {
                int in = -1;
                output = new StringBuilder(64);
                try {
                    while ((in = is.read()) != -1) {
                        output.append((char) in);
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                    exp = ex;
                }
            }
    
            public StringBuilder getOutput() {
                return output;
            }
    
            public IOException getException() {
                return exp;
            }
        }
    }
    

    You might find it easier to first find a working solution on each platform and then design your solution around those requirements, maybe having a factory which generated the ProcessBuilder based on the platform, as an idea.