mikrotik-container-backup-u.../src/main/java/ru/kirillius/mktbk/Device.java

290 lines
9.9 KiB
Java

package ru.kirillius.mktbk;
import me.legrange.mikrotik.ApiConnection;
import me.legrange.mikrotik.ApiConnectionException;
import me.legrange.mikrotik.MikrotikApiException;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.sftp.SFTPClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
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 Device implements Closeable {
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;
private SFTPClient sftpClient;
private SSHClient sshClient;
private ApiConnection apiConnection;
public Device(String host, String username, String password) {
this.host = host;
this.username = username;
this.password = password;
}
public SFTPClient getSftpClient() throws IOException {
if (sftpClient == null) {
SystemLogger.message("Initializing sftp connection", LOG_CONTEXT);
sftpClient = getSshClient().newSFTPClient();
}
return sftpClient;
}
public List<ContainerMetadata> getContainers() throws MikrotikApiException {
return getApiConnection().execute("/container/print").stream().map(ContainerMetadata::new).toList();
}
private String bytesToString(List<Byte> 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;
if (path.startsWith("\"")) {
escaped = true;
doubleQuotes = true;
} else if (path.startsWith("'")) {
escaped = true;
}
if (!escaped) {
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<Byte>();
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) {
var special = List.of("$", "\\");
for (var symbol : special) {
path = path.replaceAll(Pattern.quote("\\" + symbol), symbol);
}
return path;
} else {
return path.replaceAll(Pattern.quote("'\\''"), "'");
}
}
public List<FileMetadata> listFilesystem(ContainerMetadata container) throws IOException, InterruptedException {
var files = new ArrayList<FileMetadata>();
try (var session = getSshClient().startSession()) {
session.allocateDefaultPTY();
try (var terminal = new VirtualTerminal(session)) {
SystemLogger.message("Waiting for terminal be ready", LOG_CONTEXT);
terminal.newLine().newLine().newLine().expect("[" + username + "@", 30000).expect("]", 5000);
SystemLogger.message("Opening container shell", LOG_CONTEXT);
terminal.skipEverything().write("/container/shell " + container.getId()).write("\r\n\r\n").expect("#", 20000);
terminal.skipEverything().write("echo " + BEGIN_PATTERN).newLine().expect(BEGIN_PATTERN, 5000).skipEverything();
terminal.write(LIST_COMMAND).newLine().write("echo " + END_PATTERN).newLine();
var reader = terminal.getReader();
String line;
String directory = null;
while ((line = reader.readLine()) != null) {
if (line.trim().endsWith(LIST_COMMAND)) {
continue;
}
if (line.contains(BEGIN_PATTERN)) {
continue;
}
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) {
directory = directory.substring(1, directory.length() - 2);
}
if (!directory.endsWith("/")) {
directory = directory + "/";
}
continue;
}
if (line.trim().isEmpty()) {
continue;
}
if (line.startsWith("total")) {
continue;
}
line = stripSpaces(line);
var split = line.split(Pattern.quote(" "), 9);
if (split.length < 9) {
SystemLogger.warning("Unable to parse line " + line, LOG_CONTEXT);
continue;
}
var path = stripBashSlashes(split[8].trim());
String target = null;
if (path.contains("->")) {
var parts = path.split(Pattern.quote("->"));
path = stripBashSlashes(parts[0].trim());
target = stripBashSlashes(parts[1].trim());
}
if (directory == null) {
SystemLogger.warning("Current directory is not set! Skipping", LOG_CONTEXT);
continue;
}
path = directory.trim() + path;
var file = FileMetadata.builder().permissions(split[0]).owner(split[2]).group(split[3]).path(path).target(target).build();
files.add(file);
if (files.size() % 10000 == 0) {
SystemLogger.message("Processed " + files.size() + " files", LOG_CONTEXT);
}
}
}
}
return files;
}
private String stripSpaces(String line) {
String[] parts;
var singleQuote = line.indexOf("'");
var doubleQuote = line.indexOf("\"");
if (singleQuote == -1 && doubleQuote == -1) {
parts = new String[]{line};
} else {
var offset = -1;
if (singleQuote == -1) {
offset = doubleQuote;
} else if (doubleQuote == -1) {
offset = singleQuote;
} else {
offset = Math.min(singleQuote, doubleQuote);
}
parts = new String[]{line.substring(0, offset), line.substring(offset)};
}
while (parts[0].contains(" ")) {
parts[0] = parts[0].replaceAll(Pattern.quote(" "), " ");
}
var builder = new StringBuilder();
for (var part : parts) {
builder.append(part);
}
return builder.toString();
}
public SSHClient getSshClient() throws IOException {
if (sshClient == null) {
SystemLogger.message("Initializing ssh connection", LOG_CONTEXT);
sshClient = new SSHClient();
sshClient.addHostKeyVerifier(new PromiscuousVerifier());
sshClient.connect(host);
sshClient.useCompression();
sshClient.authPassword(username, password);
}
return sshClient;
}
public ApiConnection getApiConnection() {
if (apiConnection == null) {
SystemLogger.message("Initializing api connection", LOG_CONTEXT);
try {
apiConnection = ApiConnection.connect(host);
apiConnection.login(username, password);
} catch (MikrotikApiException e) {
throw new RuntimeException(e);
}
}
return apiConnection;
}
@Override
public void close() {
if (apiConnection != null) {
try {
apiConnection.close();
} catch (ApiConnectionException e) {
SystemLogger.error("Error closing api connection", LOG_CONTEXT, e);
}
}
if (sftpClient != null) {
try {
sftpClient.close();
} catch (IOException e) {
SystemLogger.error("Error closing sftp connection", LOG_CONTEXT, e);
}
}
if (sshClient != null) {
try {
sshClient.close();
} catch (IOException e) {
SystemLogger.error("Error closing ssh connection", LOG_CONTEXT, e);
}
}
}
}