Промежуточный коммит
This commit is contained in:
parent
633e0cfb05
commit
27ffa6ef6d
98
pom.xml
98
pom.xml
|
|
@ -4,9 +4,9 @@
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>org.example</groupId>
|
<groupId>ru.kirillius</groupId>
|
||||||
<artifactId>mikrotik-container-backup-utility</artifactId>
|
<artifactId>mikrotik-container-backup-utility</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|
@ -14,4 +14,98 @@
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>kirillius</id>
|
||||||
|
<name>kirillius</name>
|
||||||
|
<url>https://repo.kirillius.ru/maven</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
<updatePolicy>always</updatePolicy>
|
||||||
|
<checksumPolicy>fail</checksumPolicy>
|
||||||
|
</releases>
|
||||||
|
<layout>default</layout>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ru.kirillius.utils</groupId>
|
||||||
|
<artifactId>common-logging</artifactId>
|
||||||
|
<version>1.3.0.0</version>
|
||||||
|
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-compress</artifactId>
|
||||||
|
<version>1.20</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>2.0.16</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-jdk14</artifactId>
|
||||||
|
<version>2.0.16</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>5.11.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>5.11.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.legrange</groupId>
|
||||||
|
<artifactId>mikrotik</artifactId>
|
||||||
|
<version>3.0.8</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.36</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.easytesting/fest-assert-core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.easytesting</groupId>
|
||||||
|
<artifactId>fest-assert-core</artifactId>
|
||||||
|
<version>2.0M10</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.hierynomus/sshj -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.hierynomus</groupId>
|
||||||
|
<artifactId>sshj</artifactId>
|
||||||
|
<version>0.39.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>ru.kirillius</groupId>
|
||||||
|
<artifactId>json-convert</artifactId>
|
||||||
|
<version>2.1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
import me.legrange.mikrotik.MikrotikApiException;
|
||||||
|
import net.schmizz.sshj.sftp.SFTPException;
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarConstants;
|
||||||
|
import ru.kirillius.utils.logging.SystemLogger;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.rmi.RemoteException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class App {
|
||||||
|
private static final String LOG_CONTEXT = App.class.getSimpleName();
|
||||||
|
private List<ContainerMetadata> containers;
|
||||||
|
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException, MikrotikApiException, InterruptedException {
|
||||||
|
SystemLogger.initializeLogging(Level.INFO, Collections.emptyList());
|
||||||
|
|
||||||
|
new App();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cin() {
|
||||||
|
try {
|
||||||
|
return reader.readLine();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pause() {
|
||||||
|
System.out.println("Press any key to continue...");
|
||||||
|
cin();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedReader reader;
|
||||||
|
|
||||||
|
private ContainerMetadata selectContainer() {
|
||||||
|
System.out.println("Found containers:");
|
||||||
|
var i = 0;
|
||||||
|
for (var container : containers) {
|
||||||
|
System.out.println(String.valueOf(++i) + ' ' + container);
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
|
||||||
|
var index = -1;
|
||||||
|
|
||||||
|
if (containers.size() > 1) {
|
||||||
|
do {
|
||||||
|
index = -1;
|
||||||
|
System.out.print("Select container you want to backup:");
|
||||||
|
try {
|
||||||
|
index = Integer.parseInt(cin()) - 1;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
SystemLogger.error("Invalid number", LOG_CONTEXT, e);
|
||||||
|
}
|
||||||
|
} while (index >= containers.size() || index < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containers.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = containers.get(index);
|
||||||
|
System.out.print("Do you really want to backup container " + container + "? [y/n]");
|
||||||
|
|
||||||
|
if (cin().equals("y")) {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
} while (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public App() throws IOException, MikrotikApiException, InterruptedException {
|
||||||
|
try {
|
||||||
|
reader = new BufferedReader(new InputStreamReader(System.in));
|
||||||
|
auth();
|
||||||
|
containers = deviceContext.getContainers();
|
||||||
|
|
||||||
|
ContainerMetadata container;
|
||||||
|
do {
|
||||||
|
container = selectContainer();
|
||||||
|
if (container == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!container.isRunning()) {
|
||||||
|
SystemLogger.warning("The selected container is not running. There is a limitation that requires the container to be running.", LOG_CONTEXT);
|
||||||
|
SystemLogger.message("Starting container " + container, LOG_CONTEXT);
|
||||||
|
deviceContext.getApiConnection().execute("/container/start .id=" + container.getId());
|
||||||
|
try {
|
||||||
|
Thread.sleep(10000L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
containers = deviceContext.getContainers();
|
||||||
|
var id = container.getId();
|
||||||
|
container = containers.stream().filter(containerMetadata -> containerMetadata.getId().equals(id)).findFirst().orElse(null);
|
||||||
|
if (container == null || !container.isRunning()) {
|
||||||
|
SystemLogger.error("Something went wrong. Unable to start container.", LOG_CONTEXT);
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (container == null);
|
||||||
|
|
||||||
|
SystemLogger.warning("/!\\ We are going to make a backup. You have to stop all services in this container to prevent data corruption.", LOG_CONTEXT);
|
||||||
|
System.out.print("Type yes to continue or Ctrl+C to abort: ");
|
||||||
|
while (!"yes".equals(cin())) ;
|
||||||
|
SystemLogger.message("Reading container filesystem metadata...", LOG_CONTEXT);
|
||||||
|
var filterContents = List.of("/sys/", "/proc/", "/dev/", "/mnt/", "/run/", "/tmp/");
|
||||||
|
var files = deviceContext.listFilesystem(container).stream().filter(f -> filterContents.contains(f.getPath()) || filterContents.stream().noneMatch(s -> f.getPath().startsWith(s))).toList();
|
||||||
|
SystemLogger.message("Found " + files.size() + " files", LOG_CONTEXT);
|
||||||
|
|
||||||
|
var sftp = deviceContext.getSftpClient();
|
||||||
|
final var localTmpFile = "./transfer.tmp";
|
||||||
|
|
||||||
|
try (var outputStream = new FileOutputStream(container.getGuid())) {
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
try (var tar = new TarArchiveOutputStream(outputStream)) {
|
||||||
|
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
|
||||||
|
var status = new DownloadStatus("Downloading", files.size());
|
||||||
|
for (var file : files) {
|
||||||
|
try {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
var entry = new TarArchiveEntry(file.getPath(), TarConstants.LF_DIR);
|
||||||
|
entry.setGroupId(file.getGroupId());
|
||||||
|
entry.setUserId(file.getUserId());
|
||||||
|
entry.setMode(file.getMode());
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
status.addCopiedFile(0);
|
||||||
|
} else if (file.isSymlink()) {
|
||||||
|
var entry = new TarArchiveEntry(file.getPath(), TarConstants.LF_SYMLINK);
|
||||||
|
entry.setLinkName(file.getTarget());
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
status.addCopiedFile(0);
|
||||||
|
} else {
|
||||||
|
var entry = new TarArchiveEntry(file.getPath());
|
||||||
|
entry.setGroupId(file.getGroupId());
|
||||||
|
entry.setUserId(file.getUserId());
|
||||||
|
entry.setMode(file.getMode());
|
||||||
|
var remotePath = "/" + container.getGuid() + file.getPath();
|
||||||
|
var size = sftp.size(remotePath);
|
||||||
|
|
||||||
|
|
||||||
|
entry.setSize(size);
|
||||||
|
tar.putArchiveEntry(entry);
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
sftp.get(remotePath, new SFTPFileStream(size, tar));
|
||||||
|
}catch (AssertionError ae){
|
||||||
|
throw new RuntimeException(ae);
|
||||||
|
}
|
||||||
|
|
||||||
|
tar.closeArchiveEntry();
|
||||||
|
status.addCopiedFile(size);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
status.clear();
|
||||||
|
SystemLogger.error("Unable to copy file " + file.getPath(), LOG_CONTEXT, e);
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
tar.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println(files.size());
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
if (reader != null) {
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
if (deviceContext != null) {
|
||||||
|
deviceContext.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// openSFTP();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// private void openSSH() throws TransportException, ConnectionException {
|
||||||
|
// session = client.startSession();
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// private void openSFTP() throws IOException {
|
||||||
|
// sftpClient = client.newSFTPClient();
|
||||||
|
// for (var info : sftpClient.ls("/")) {
|
||||||
|
// System.out.println(info.getPath());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
private DeviceContext deviceContext;
|
||||||
|
|
||||||
|
private void auth() {
|
||||||
|
do {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
System.out.print("Enter remote host:");
|
||||||
|
var host = cin();
|
||||||
|
if (host.trim().isEmpty()) {
|
||||||
|
throw new RuntimeException("Remote host is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.print("Enter username:");
|
||||||
|
var username = cin();
|
||||||
|
if (username.trim().isEmpty()) {
|
||||||
|
throw new RuntimeException("Username is empty");
|
||||||
|
}
|
||||||
|
System.out.print("Enter password:");
|
||||||
|
var password = cin();
|
||||||
|
if (password.trim().isEmpty()) {
|
||||||
|
throw new RuntimeException("Password is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceContext = new DeviceContext(host, username, password);
|
||||||
|
deviceContext.getApiConnection();
|
||||||
|
} catch (Exception e) {
|
||||||
|
SystemLogger.error("Unable to connect", LOG_CONTEXT, e);
|
||||||
|
if (deviceContext != null) {
|
||||||
|
deviceContext.close();
|
||||||
|
}
|
||||||
|
deviceContext = null;
|
||||||
|
}
|
||||||
|
} while (deviceContext == null);
|
||||||
|
|
||||||
|
SystemLogger.message("Connected", LOG_CONTEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import ru.kirillius.json.JSONSerializable;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@JSONSerializable
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class ContainerMetadata {
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
public @interface FieldAlias {
|
||||||
|
String value();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() {
|
||||||
|
return "running".equals(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
var line = new StringJoiner(" ");
|
||||||
|
|
||||||
|
if (isRunning()) {
|
||||||
|
line.add("[R]");
|
||||||
|
}
|
||||||
|
if (comment != null) {
|
||||||
|
line.add(comment);
|
||||||
|
}
|
||||||
|
line.add(hostname);
|
||||||
|
line.add(tag);
|
||||||
|
line.add(os);
|
||||||
|
line.add(arch);
|
||||||
|
line.add(ifname);
|
||||||
|
|
||||||
|
return line.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerMetadata(Map<String, String> data) {
|
||||||
|
var fields = new HashMap<String, Field>();
|
||||||
|
|
||||||
|
for (var field : getClass().getDeclaredFields()) {
|
||||||
|
var key = field.getName();
|
||||||
|
if (field.isAnnotationPresent(FieldAlias.class)) {
|
||||||
|
key = field.getAnnotation(FieldAlias.class).value();
|
||||||
|
}
|
||||||
|
fields.put(key, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (var key : data.keySet()) {
|
||||||
|
if (fields.containsKey(key)) {
|
||||||
|
Field field = fields.get(key);
|
||||||
|
field.setAccessible(true);
|
||||||
|
try {
|
||||||
|
field.set(this, data.get(key));
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FieldAlias(".id")
|
||||||
|
private String id;
|
||||||
|
@FieldAlias("name")
|
||||||
|
private String guid;
|
||||||
|
private String tag;
|
||||||
|
private String comment;
|
||||||
|
private String os;
|
||||||
|
private String arch;
|
||||||
|
@FieldAlias("interface")
|
||||||
|
private String ifname;
|
||||||
|
private String entrypoint;
|
||||||
|
private String mounts;
|
||||||
|
private String dns;
|
||||||
|
private String hostname;
|
||||||
|
private String workdir;
|
||||||
|
@FieldAlias("start-on-boot")
|
||||||
|
private String autostart;
|
||||||
|
@FieldAlias("domain-name")
|
||||||
|
private String domainname;
|
||||||
|
private String envlist;
|
||||||
|
private String logging;
|
||||||
|
@FieldAlias("root-dir")
|
||||||
|
private String rootdir;
|
||||||
|
@FieldAlias("stop-signal")
|
||||||
|
private String stopsignal;
|
||||||
|
private String user;
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
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.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class DeviceContext implements Closeable {
|
||||||
|
|
||||||
|
private final static String LOG_CONTEXT = DeviceContext.class.getSimpleName();
|
||||||
|
private final String host;
|
||||||
|
private final String username;
|
||||||
|
private final String password;
|
||||||
|
private SFTPClient sftpClient;
|
||||||
|
private SSHClient sshClient;
|
||||||
|
private ApiConnection apiConnection;
|
||||||
|
|
||||||
|
public DeviceContext(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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("'\\''"), "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<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.startsWith(LIST_COMMAND)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.endsWith(END_PATTERN)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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() throws MikrotikApiException {
|
||||||
|
if (apiConnection == null) {
|
||||||
|
SystemLogger.message("Initializing api connection", LOG_CONTEXT);
|
||||||
|
apiConnection = ApiConnection.connect(host);
|
||||||
|
apiConnection.login(username, password);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
public class DownloadStatus {
|
||||||
|
private String buffer = "";
|
||||||
|
private long bytes;
|
||||||
|
private long copied;
|
||||||
|
private long total;
|
||||||
|
private long startTime;
|
||||||
|
private String label;
|
||||||
|
private long lastUpdate = 0;
|
||||||
|
|
||||||
|
public DownloadStatus(String label, long total) {
|
||||||
|
this.label = label;
|
||||||
|
this.total = total;
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCopiedFile(long size) {
|
||||||
|
bytes += size;
|
||||||
|
copied++;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int spin;
|
||||||
|
|
||||||
|
private String spinner() {
|
||||||
|
spin++;
|
||||||
|
return switch (spin) {
|
||||||
|
case 1 -> "\\";
|
||||||
|
case 2 -> "|";
|
||||||
|
case 3 -> "/";
|
||||||
|
case 4 -> {
|
||||||
|
spin = 0;
|
||||||
|
yield "-";
|
||||||
|
}
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatBytes(long bytes) {
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return bytes + " B";
|
||||||
|
} else if (bytes < 1048576L) {
|
||||||
|
return Math.floor((float) bytes / 1024L) + " KB";
|
||||||
|
} else if (bytes < 1073741824L) {
|
||||||
|
return Math.floor((float) bytes / 1048576L) + " MB";
|
||||||
|
} else if (bytes < 1099511627776L) {
|
||||||
|
return Math.floor((float) bytes / 1073741824L) + " GB";
|
||||||
|
} else {
|
||||||
|
return Math.floor((float) bytes / 1099511627776L) + " TB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
if (System.currentTimeMillis() - lastUpdate < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clear();
|
||||||
|
var speed = bytes * 1000 / (System.currentTimeMillis() - startTime);
|
||||||
|
buffer = ("[" + spinner() + "] " + label + " " + Math.floor((float) copied / total * 1000f) / 10 + "% " + copied + "/" + total + " (" + formatBytes(bytes) + ") " + formatBytes(speed) + "/s");
|
||||||
|
System.out.print(buffer);
|
||||||
|
lastUpdate = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
for (var i = 0; i < buffer.length(); i++) {
|
||||||
|
System.out.print('\b');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FileMetadata {
|
||||||
|
@Getter
|
||||||
|
private String path;
|
||||||
|
private String permissions;
|
||||||
|
private String owner;
|
||||||
|
private String group;
|
||||||
|
@Getter
|
||||||
|
private String target;
|
||||||
|
|
||||||
|
public boolean isSymlink() {
|
||||||
|
return permissions.startsWith("l");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return permissions.startsWith("d");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getGroupId() {
|
||||||
|
return Integer.parseInt(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUserId() {
|
||||||
|
return Integer.parseInt(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMode() {
|
||||||
|
return (parseModeOctet(permissions.substring(1, 4)) << 6) +
|
||||||
|
(parseModeOctet(permissions.substring(4, 7)) << 3) +
|
||||||
|
parseModeOctet(permissions.substring(7, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseModeOctet(String octet) {
|
||||||
|
return 0xFFFFFF & (octet.contains("r") ? 4 : 0) + (octet.contains("w") ? 2 : 0) + (octet.contains("x") ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
import net.schmizz.sshj.xfer.InMemoryDestFile;
|
||||||
|
import net.schmizz.sshj.xfer.LocalDestFile;
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class SFTPFileStream extends InMemoryDestFile {
|
||||||
|
private final OutputStream os;
|
||||||
|
private final long size;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLastAccessedTime(long t)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLastModifiedTime(long t)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPermissions(int perms)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDestFile getTargetDirectory(String dirname) throws IOException {
|
||||||
|
return super.getTargetDirectory(dirname);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SFTPFileStream(long size, TarArchiveOutputStream tar) {
|
||||||
|
this.size = size;
|
||||||
|
os = new OutputStream() {
|
||||||
|
@Override
|
||||||
|
public void write(int i) throws IOException {
|
||||||
|
tar.write(i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLength() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() throws IOException {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream(boolean append) throws IOException {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.schmizz.sshj.connection.ConnectionException;
|
||||||
|
import net.schmizz.sshj.connection.channel.direct.Session;
|
||||||
|
import net.schmizz.sshj.transport.TransportException;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
public class VirtualTerminal implements Closeable {
|
||||||
|
private final Session.Shell shell;
|
||||||
|
@Getter
|
||||||
|
private final BufferedReader reader;
|
||||||
|
private final BufferedWriter writer;
|
||||||
|
|
||||||
|
|
||||||
|
public VirtualTerminal(Session session) throws TransportException, ConnectionException {
|
||||||
|
shell = session.startShell();
|
||||||
|
reader = new BufferedReader(new InputStreamReader(shell.getInputStream()));
|
||||||
|
writer = new BufferedWriter(new OutputStreamWriter(shell.getOutputStream()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualTerminal newLine() throws IOException {
|
||||||
|
writer.newLine();
|
||||||
|
writer.flush();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualTerminal write(String text) throws IOException {
|
||||||
|
writer.write(text);
|
||||||
|
writer.flush();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public VirtualTerminal skipEverything() throws IOException {
|
||||||
|
while (reader.ready()) {
|
||||||
|
reader.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualTerminal expect(String expected, long timeout) throws IOException, InterruptedException {
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
|
||||||
|
var startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
while (buffer.indexOf(expected) == -1 && System.currentTimeMillis() - startTime < timeout) {
|
||||||
|
if (!reader.ready()) {
|
||||||
|
Thread.sleep(50);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buffer.append((char) reader.read());
|
||||||
|
}
|
||||||
|
var i = buffer.indexOf(expected);
|
||||||
|
if (buffer.indexOf(expected) == -1) {
|
||||||
|
throw new InterruptedException("Timeout exceeded while waiting for string \"" + expected + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (shell != null) {
|
||||||
|
shell.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ru.kirillius.mktbk;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class FileMetadataTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestPermissions() {
|
||||||
|
comparePermissions("-rwxrwxrwx", 0777);
|
||||||
|
comparePermissions("-rwxrwx---", 0770);
|
||||||
|
comparePermissions("-rwx------", 0700);
|
||||||
|
comparePermissions("-r--------", 0400);
|
||||||
|
comparePermissions("-x--------", 0100);
|
||||||
|
comparePermissions("-w--------", 0200);
|
||||||
|
comparePermissions("-r---w---x", 0421);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void comparePermissions(String s, int expected) {
|
||||||
|
var m = FileMetadata.builder().permissions(s).build();
|
||||||
|
assertThat(m.getMode()).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue