diff --git a/src/main/java/ru/kirillius/mktbk/App.java b/src/main/java/ru/kirillius/mktbk/App.java index b08a7fc..15e1d69 100644 --- a/src/main/java/ru/kirillius/mktbk/App.java +++ b/src/main/java/ru/kirillius/mktbk/App.java @@ -16,10 +16,18 @@ public class App { private static final String LOG_CONTEXT = App.class.getSimpleName(); private List containers; private final Console console; - private DeviceContext deviceContext; + private Device device; + private final static String BACKUP_FOLDER = "backups"; + + private File backupFolder; public App() { + backupFolder = new File(BACKUP_FOLDER); + + if(!backupFolder.exists()){ + backupFolder.mkdir(); + } console = new Console(); try { @@ -94,7 +102,7 @@ public class App { backupName = suggestion; } - var outputFile = new File(backupName + ".tar.gz"); + var outputFile = new File(backupFolder,backupName + ".tar.gz"); List files; @@ -104,7 +112,9 @@ public class App { throw new RuntimeException("Error loading container file tree", e); } - stopContainer(container); + if(console.confirm("Container can be stopped now. Stop it?")) { + stopContainer(container); + } container = validate(container); SystemLogger.message("Building image layer from remote files", LOG_CONTEXT); createSystemLayer(container, files, layerFile); @@ -120,14 +130,16 @@ public class App { layerFile.delete(); } SystemLogger.message("Backup is done ", LOG_CONTEXT); - console.pause(); + if (!container.isRunning() && console.confirm("Do you want to start container?")) { + startContainer(container); + } } private void stopContainer(ContainerMetadata container) { SystemLogger.message("Stopping container " + container, LOG_CONTEXT); try { - deviceContext.getApiConnection().execute("/container/stop .id=" + container.getId()); + device.getApiConnection().execute("/container/stop .id=" + container.getId()); Thread.sleep(10000L); } catch (MikrotikApiException | InterruptedException e) { throw new RuntimeException("Error stopping container", e); @@ -140,8 +152,18 @@ public class App { if (!container.isRunning()) { SystemLogger.warning("The selected container is not running. There is a limitation that requires the container to be running.", LOG_CONTEXT); console.pause(); + startContainer(container); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void startContainer(ContainerMetadata container) { + try { + if (!container.isRunning()) { SystemLogger.message("Starting container " + container, LOG_CONTEXT); - deviceContext.getApiConnection().execute("/container/start .id=" + container.getId()); + device.getApiConnection().execute("/container/start .id=" + container.getId()); Thread.sleep(10000L); updateContainers(); var id = container.getId(); @@ -164,12 +186,12 @@ public class App { } private void updateContainers() throws MikrotikApiException { - containers = deviceContext.getContainers(); + containers = device.getContainers(); } private void createSystemLayer(ContainerMetadata container, List files, File outputFile) { try { - var sftp = deviceContext.getSftpClient(); + var sftp = device.getSftpClient(); try (var outputStream = new FileOutputStream(outputFile)) { try (var tar = new TarArchiveOutputStream(outputStream)) { tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); @@ -223,7 +245,7 @@ public class App { SystemLogger.message("Reading container filesystem metadata...", LOG_CONTEXT); var systemDirs = List.of("/sys/", "/proc/", "/dev/", "/mnt/", "/run/", "/tmp/"); var filterFiles = List.of("/.type"); - var files = deviceContext.listFilesystem(container).stream().filter(fileMetadata -> !filterFiles.contains(fileMetadata.getPath())).filter(f -> systemDirs.contains(f.getPath()) || systemDirs.stream().noneMatch(s -> f.getPath().startsWith(s))).toList(); + var files = device.listFilesystem(container).stream().filter(fileMetadata -> !filterFiles.contains(fileMetadata.getPath())).filter(f -> systemDirs.contains(f.getPath()) || systemDirs.stream().noneMatch(s -> f.getPath().startsWith(s))).toList(); SystemLogger.message("Found " + files.size() + " files", LOG_CONTEXT); return files; } @@ -238,16 +260,16 @@ public class App { console.clear(); - deviceContext = new DeviceContext(host, username, password); - deviceContext.getApiConnection(); + device = new Device(host, username, password); + device.getApiConnection(); } catch (Exception e) { SystemLogger.error("Unable to connect", LOG_CONTEXT, e); - if (deviceContext != null) { - deviceContext.close(); + if (device != null) { + device.close(); } - deviceContext = null; + device = null; } - } while (deviceContext == null); + } while (device == null); SystemLogger.message("Connected", LOG_CONTEXT); } diff --git a/src/main/java/ru/kirillius/mktbk/Console.java b/src/main/java/ru/kirillius/mktbk/Console.java index 79c54e9..5e06711 100644 --- a/src/main/java/ru/kirillius/mktbk/Console.java +++ b/src/main/java/ru/kirillius/mktbk/Console.java @@ -69,13 +69,14 @@ public class Console implements Closeable { if (values.size() > 1) { do { clear(); - System.out.println(caption + ":"); + System.out.println(caption); var i = 0; for (var row : values) { System.out.println(String.valueOf(++i) + ' ' + row); } index = -1; + System.out.print("Select item: "); try { index = Integer.parseInt(read()) - 1; } catch (NumberFormatException ignored) { diff --git a/src/main/java/ru/kirillius/mktbk/DeviceContext.java b/src/main/java/ru/kirillius/mktbk/Device.java similarity index 82% rename from src/main/java/ru/kirillius/mktbk/DeviceContext.java rename to src/main/java/ru/kirillius/mktbk/Device.java index 040c753..48d79c1 100644 --- a/src/main/java/ru/kirillius/mktbk/DeviceContext.java +++ b/src/main/java/ru/kirillius/mktbk/Device.java @@ -11,12 +11,16 @@ import ru.kirillius.utils.logging.SystemLogger; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; -public class DeviceContext implements Closeable { +public class Device implements Closeable { - private final static String LOG_CONTEXT = DeviceContext.class.getSimpleName(); + private final static String LOG_CONTEXT = Device.class.getSimpleName(); + private final static String LIST_COMMAND = "ls --color=never -lAnpR --full-time /"; + private final static String BEGIN_PATTERN = "///BEGIN-OF-STREAM///"; + private final static String END_PATTERN = "///END-OF-STREAM///"; private final String host; private final String username; private final String password; @@ -24,10 +28,12 @@ public class DeviceContext implements Closeable { private SSHClient sshClient; private ApiConnection apiConnection; - public DeviceContext(String host, String username, String password) { + public Device(String host, String username, String password) { this.host = host; this.username = username; this.password = password; + + } public SFTPClient getSftpClient() throws IOException { @@ -42,6 +48,14 @@ public class DeviceContext implements Closeable { return getApiConnection().execute("/container/print").stream().map(ContainerMetadata::new).toList(); } + private String bytesToString(List list) { + var bytes = new byte[list.size()]; + for (var i = 0; i < list.size(); i++) { + bytes[i] = list.get(i); + } + return new String(bytes); + } + private String stripBashSlashes(String path) { var escaped = false; var doubleQuotes = false; @@ -55,6 +69,35 @@ public class DeviceContext implements Closeable { return path; } + if (path.contains("'$'")) { + var builder = new StringBuilder(); + var first = true; + for (var part : path.split(Pattern.quote("'$'"))) { + if (first) { + first = false; + builder.append(part); + continue; + } + var group = part.split(Pattern.quote("''")); + if (group.length != 2) { + SystemLogger.warning("Unable to parse file path: " + path, LOG_CONTEXT); + builder.append(part); + } else { + + + var encoded = group[0]; + var bytes = new ArrayList(); + for (var code : Arrays.stream(encoded.split(Pattern.quote("\\"))).skip(1).toArray(String[]::new)) { + bytes.add((byte) Integer.parseInt(code, 8)); + } + builder.append(bytesToString(bytes)); + builder.append(group[1]); + } + } + path = builder.toString(); + } + + path = path.substring(1, path.length() - 1); if (doubleQuotes) { @@ -69,11 +112,6 @@ public class DeviceContext implements Closeable { } } - private final static String LIST_COMMAND = "ls --color=never -lAnpR --full-time /"; - private final static String BEGIN_PATTERN = ":::BEGIN-OF-STREAM:::"; - private final static String END_PATTERN = ":::END-OF-STREAM:::"; - - public List listFilesystem(ContainerMetadata container) throws IOException, InterruptedException { var files = new ArrayList(); @@ -102,6 +140,10 @@ public class DeviceContext implements Closeable { if (line.endsWith(END_PATTERN)) { break; } + + if (line.isBlank()) { + continue; + } if ((line.startsWith("/") || line.startsWith("'/")) && line.endsWith(":")) { directory = line.substring(0, line.length() - 1); if (directory.indexOf('\'') == 0) { diff --git a/src/main/java/ru/kirillius/mktbk/DownloadStatus.java b/src/main/java/ru/kirillius/mktbk/DownloadStatus.java index 3a4d1b6..d2df141 100644 --- a/src/main/java/ru/kirillius/mktbk/DownloadStatus.java +++ b/src/main/java/ru/kirillius/mktbk/DownloadStatus.java @@ -8,6 +8,7 @@ public class DownloadStatus { private long startTime; private String label; private long lastUpdate = 0; + private long lastBytes = 0; public DownloadStatus(String label, long total) { this.label = label; @@ -56,7 +57,8 @@ public class DownloadStatus { return; } clear(); - var speed = bytes * 1000 / (System.currentTimeMillis() - startTime); + var speed = (bytes - lastBytes) * 1000 / (System.currentTimeMillis() - lastUpdate); + lastBytes = bytes; buffer = ("[" + spinner() + "] " + label + " " + Math.floor((float) copied / total * 1000f) / 10 + "% " + copied + "/" + total + " (" + formatBytes(bytes) + ") " + formatBytes(speed) + "/s"); System.out.print(buffer); lastUpdate = System.currentTimeMillis(); @@ -72,6 +74,7 @@ public class DownloadStatus { public void finish() { lastUpdate = 0; update(); + System.out.println(); } }