diff --git a/pom.xml b/pom.xml index 078edc0..6b6b208 100644 --- a/pom.xml +++ b/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"> 4.0.0 - org.example + ru.kirillius mikrotik-container-backup-utility - 1.0-SNAPSHOT + 1.0.0.0 21 @@ -14,4 +14,98 @@ UTF-8 + + + kirillius + kirillius + https://repo.kirillius.ru/maven + + true + always + fail + + default + + + + + + + ru.kirillius.utils + common-logging + 1.3.0.0 + + + + + org.apache.commons + commons-compress + 1.20 + + + + + org.slf4j + slf4j-api + 2.0.16 + + + + + org.slf4j + slf4j-jdk14 + 2.0.16 + + + + + org.junit.jupiter + junit-jupiter-api + 5.11.4 + test + + + + org.junit.jupiter + junit-jupiter-engine + 5.11.4 + test + + + + + me.legrange + mikrotik + 3.0.8 + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + + org.easytesting + fest-assert-core + 2.0M10 + test + + + + + + com.hierynomus + sshj + 0.39.0 + + + + ru.kirillius + json-convert + 2.1.0.0 + + \ No newline at end of file diff --git a/src/main/java/ru/kirillius/mktbk/App.java b/src/main/java/ru/kirillius/mktbk/App.java new file mode 100644 index 0000000..c0fe10e --- /dev/null +++ b/src/main/java/ru/kirillius/mktbk/App.java @@ -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 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); + } + +} diff --git a/src/main/java/ru/kirillius/mktbk/ContainerMetadata.java b/src/main/java/ru/kirillius/mktbk/ContainerMetadata.java new file mode 100644 index 0000000..72728f1 --- /dev/null +++ b/src/main/java/ru/kirillius/mktbk/ContainerMetadata.java @@ -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 data) { + var fields = new HashMap(); + + 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; + + +} diff --git a/src/main/java/ru/kirillius/mktbk/DeviceContext.java b/src/main/java/ru/kirillius/mktbk/DeviceContext.java new file mode 100644 index 0000000..d609578 --- /dev/null +++ b/src/main/java/ru/kirillius/mktbk/DeviceContext.java @@ -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 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 listFilesystem(ContainerMetadata container) throws IOException, InterruptedException { + var files = new ArrayList(); + + + 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); + } + } + } +} diff --git a/src/main/java/ru/kirillius/mktbk/DownloadStatus.java b/src/main/java/ru/kirillius/mktbk/DownloadStatus.java new file mode 100644 index 0000000..9f1c7ad --- /dev/null +++ b/src/main/java/ru/kirillius/mktbk/DownloadStatus.java @@ -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'); + } + } + +} diff --git a/src/main/java/ru/kirillius/mktbk/FileMetadata.java b/src/main/java/ru/kirillius/mktbk/FileMetadata.java new file mode 100644 index 0000000..3599ed2 --- /dev/null +++ b/src/main/java/ru/kirillius/mktbk/FileMetadata.java @@ -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); + } +} diff --git a/src/main/java/ru/kirillius/mktbk/SFTPFileStream.java b/src/main/java/ru/kirillius/mktbk/SFTPFileStream.java new file mode 100644 index 0000000..795de9d --- /dev/null +++ b/src/main/java/ru/kirillius/mktbk/SFTPFileStream.java @@ -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; + } + +} diff --git a/src/main/java/ru/kirillius/mktbk/VirtualTerminal.java b/src/main/java/ru/kirillius/mktbk/VirtualTerminal.java new file mode 100644 index 0000000..6e3bd67 --- /dev/null +++ b/src/main/java/ru/kirillius/mktbk/VirtualTerminal.java @@ -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(); + } + } +} diff --git a/src/test/java/ru/kirillius/mktbk/FileMetadataTest.java b/src/test/java/ru/kirillius/mktbk/FileMetadataTest.java new file mode 100644 index 0000000..53c6257 --- /dev/null +++ b/src/test/java/ru/kirillius/mktbk/FileMetadataTest.java @@ -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); + } + +} \ No newline at end of file