Промежуточный коммит

This commit is contained in:
kirillius 2025-01-29 10:12:36 +03:00
parent 633e0cfb05
commit 27ffa6ef6d
9 changed files with 962 additions and 2 deletions

98
pom.xml
View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}
}

View File

@ -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');
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}