Merge branch 'master' into feature/15-archive-view-url
10 files added
38 files modified
| | |
| | | compile group: 'com.esotericsoftware', name: 'kryo', version: '5.0.0-RC1' |
| | | // Serialization (faster than Java built-in) |
| | | |
| | | compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6' |
| | | compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6' |
| | | compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8' |
| | | compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.8' |
| | | |
| | | compileOnly "org.projectlombok:lombok:1.18.4" |
| | | testCompileOnly "org.projectlombok:lombok:1.18.4" |
| | |
| | | import de.micromata.borgbutler.utils.DateUtils; |
| | | import de.micromata.borgbutler.utils.ReplaceUtils; |
| | | import org.apache.commons.collections4.CollectionUtils; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.apache.commons.lang3.Validate; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | |
| | | BorgCommand command = new BorgCommand() |
| | | .setParams("--version") |
| | | .setDescription("Getting borg version."); |
| | | JobResult<String> jobResult = getResult(command); |
| | | |
| | | BorgJob<String> job = new BorgJob<>(command); |
| | | JobResult<String> jobResult = job.execute(); |
| | | if (jobResult == null || jobResult.getStatus() != JobResult.Status.OK) { |
| | | return null; |
| | | } |
| | | String version = jobResult.getResultObject(); |
| | | String origVersion = jobResult.getResultObject(); |
| | | String version = origVersion; |
| | | String[] strs = StringUtils.split(origVersion); |
| | | if (strs != null) { |
| | | if (!StringUtils.containsIgnoreCase(origVersion, "borg")) { |
| | | version = ""; |
| | | } else { |
| | | version = strs[strs.length - 1]; // Work arround: borg returns "borg-macosx64 1.1.8" as version string (command is included). |
| | | } |
| | | } |
| | | if (version.length() == 0 || !Character.isDigit(version.charAt(0))) { |
| | | log.error("Version string returned by '" + job.getCommandLineAsString() + "' not as expected (not borg?): " + origVersion); |
| | | return null; |
| | | } |
| | | log.info("Borg version: " + version); |
| | | return version; |
| | | } |
| | | |
| | | /** |
| | | * Executes borg init repository. |
| | | * |
| | | * @param repoConfig The configuration of the repo config (only repo is required). |
| | | * @param encryption The encryption value (repokey, repokey-blake2, none, ...). |
| | | * @return true, if no errors occured, otherwise false. |
| | | */ |
| | | public static boolean init(BorgRepoConfig repoConfig, String encryption) { |
| | | BorgCommand command = new BorgCommand() |
| | | .setRepoConfig(repoConfig) |
| | | .setCommand("init") |
| | | //.setParams("--json") // --progress has no effect. |
| | | .setDescription("Init new repo '" + repoConfig.getDisplayName() + "'."); |
| | | JobResult<String> jobResult = getResult(command); |
| | | String result = jobResult != null ? jobResult.getResultObject() : null; |
| | | // If everything is ok, now String will be returned, result must be blank: |
| | | if (jobResult == null || jobResult.getStatus() != JobResult.Status.OK || StringUtils.isNotBlank(result)) { |
| | | log.error("Error while trying to intialize repo '" + repoConfig.getRepo() + "': " + result); |
| | | return false; |
| | | } |
| | | log.error("Error while trying to intialize repo '" + repoConfig.getRepo() + "': " + result); |
| | | return false; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Executes borg info repository. |
| | | * |
| | | * @param repoConfig |
| | |
| | | // The returned job might be an already queued or running one! |
| | | final ProgressInfo progressInfo = new ProgressInfo() |
| | | .setMessage("Getting file list...") |
| | | .setCurrent(0) |
| | | .setTotal(archive.getStats().getNfiles()); |
| | | .setCurrent(0); |
| | | if (archive.getStats() != null) // Occurs only for demo repos. |
| | | progressInfo.setTotal(archive.getStats().getNfiles()); |
| | | BorgJob<List<BorgFilesystemItem>> job = BorgQueueExecutor.getInstance().execute(new BorgJob<List<BorgFilesystemItem>>(command) { |
| | | @Override |
| | | public void processStdOutLine(String line, int level) { |
| | |
| | | if (command == null) { |
| | | return null; |
| | | } |
| | | CommandLine commandLine = new CommandLine(ConfigurationHandler.getConfiguration().getBorgCommand()); |
| | | String borgCommand = ConfigurationHandler.getConfiguration().getBorgCommand(); |
| | | if (StringUtils.isBlank(borgCommand)) { |
| | | log.error("Can't run empty borg command."); |
| | | return null; |
| | | } |
| | | CommandLine commandLine = new CommandLine(borgCommand); |
| | | commandLine.addArgument(command.getCommand()); |
| | | if (command.getParams() != null) { |
| | | for (String param : command.getParams()) { |
| | |
| | | |
| | | @Override |
| | | public JobResult<String> execute() { |
| | | if (DemoRepos.isDemo(command.getRepoConfig().getRepo())) { |
| | | if (command.getRepoConfig() != null && DemoRepos.isDemo(command.getRepoConfig().getRepo())) { |
| | | return DemoRepos.execute(this); |
| | | } |
| | | return super.execute(); |
| | |
| | | if (progressInfo != null) { |
| | | clone.setProgressInfo(progressInfo.clone()); |
| | | } |
| | | clone.setCreateTime(getCreateTime()); |
| | | clone.setStartTime(getStartTime()); |
| | | clone.setStopTime(getStopTime()); |
| | | return clone; |
| | | } |
| | | |
| | |
| | | * For displaying purposes. |
| | | * |
| | | * @param repoConfig |
| | | * @param oldJobs If false, the running and queued jobs are returned, otherwise the done ones. |
| | | * @return A list of all jobs of the queue (as copies). |
| | | */ |
| | | public List<BorgJob<?>> getJobListCopy(BorgRepoConfig repoConfig) { |
| | | public List<BorgJob<?>> getJobListCopy(BorgRepoConfig repoConfig, boolean oldJobs) { |
| | | JobQueue<String> origQueue = getQueue(repoConfig); |
| | | List<BorgJob<?>> jobList = new ArrayList<>(); |
| | | if (origQueue == null) { |
| | | return jobList; |
| | | } |
| | | synchronized (origQueue) { |
| | | Iterator<AbstractJob<String>> it = origQueue.getQueueIterator(); |
| | | Iterator<AbstractJob<String>> it = oldJobs ? origQueue.getOldJobsIterator() : origQueue.getQueueIterator(); |
| | | while (it.hasNext()) { |
| | | AbstractJob<String> origJob = it.next(); |
| | | if (!(origJob instanceof BorgJob)) { |
| | |
| | | this.repoCacheAccess.clear(); |
| | | } |
| | | |
| | | public void clearRepoCacheAccess(String repo) { |
| | | if (this.repoCacheAccess.get(repo) != null) { |
| | | log.info("Clearing repository cache '" + repo + "'..."); |
| | | this.repoCacheAccess.remove(repo); |
| | | } |
| | | } |
| | | |
| | | public void clearRepoCacheAccess(Repository repository) { |
| | | log.info("Clearing repository cache '" + repository.getName() + "'..."); |
| | | this.repoCacheAccess.remove(repository.getName()); |
| | | clearRepoCacheAccess(repository.getName()); |
| | | } |
| | | |
| | | /** |
| | |
| | | package de.micromata.borgbutler.config; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonIgnore; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | import org.apache.commons.lang3.StringUtils; |
| | |
| | | private String passwordCommand; |
| | | @Getter |
| | | @Setter |
| | | @JsonIgnore |
| | | private String id; |
| | | |
| | | public String[] getEnvironmentVariables() { |
| | |
| | | if (StringUtils.isBlank(value)) return; |
| | | list.add(variable + "=" + value); |
| | | } |
| | | |
| | | public void copyFrom(BorgRepoConfig other) { |
| | | this.displayName = other.displayName; |
| | | this.repo = other.repo; |
| | | this.passphrase = other.passphrase; |
| | | this.passwordCommand = other.passwordCommand; |
| | | } |
| | | } |
| | |
| | | @JsonIgnore |
| | | @Setter(AccessLevel.PACKAGE) |
| | | private File workingDir; |
| | | |
| | | /** |
| | | * The path of the borg command to use. |
| | | */ |
| | | @Getter |
| | | private String borgCommand = "borg"; |
| | | @Setter |
| | | private String borgCommand; |
| | | |
| | | /** |
| | | * Default is 100 MB (approximately). |
| | | */ |
| | |
| | | } |
| | | |
| | | public List<BorgRepoConfig> getRepoConfigs() { |
| | | if (!ConfigurationHandler.getConfiguration().isShowDemoRepos()) { |
| | | DemoRepos.handleDemoRepos(repoConfigs); |
| | | return repoConfigs; |
| | | } |
| | | List<BorgRepoConfig> result = new ArrayList<>(); |
| | | result.addAll(repoConfigs); |
| | | DemoRepos.addDemoRepos(result); |
| | | return result; |
| | | } |
| | | |
| | | List<BorgRepoConfig> _getRepoConfigs() { |
| | | return repoConfigs; |
| | |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.StringWriter; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Scanner; |
| | | import java.util.*; |
| | | |
| | | public class DemoRepos { |
| | | private enum Type {FAST, SLOW, VERY_SLOW} |
| | |
| | | * |
| | | * @param repositoryList |
| | | */ |
| | | public static void addDemoRepos(List<BorgRepoConfig> repositoryList) { |
| | | public static void handleDemoRepos(List<BorgRepoConfig> repositoryList) { |
| | | if (!ConfigurationHandler.getConfiguration().isShowDemoRepos()) { |
| | | // Remove any demo repository if exist due to former settings: |
| | | Iterator<BorgRepoConfig> it = repositoryList.iterator(); |
| | | while(it.hasNext()) { |
| | | BorgRepoConfig repoConfig = it.next(); |
| | | if (!StringUtils.startsWith(repoConfig.getRepo(), DEMO_IDENTIFIER)) { |
| | | continue; |
| | | } |
| | | it.remove(); |
| | | } |
| | | return; |
| | | } |
| | | init(); |
| | | for (BorgRepoConfig repo : demoRepos) { |
| | | if (!repositoryList.contains(repo)) |
| | | repositoryList.add(repo); |
| | | } |
| | | // Remove duplicate entries (produced by former versions of BorgButler: |
| | | Set<String> set = new HashSet<>(); |
| | | Iterator<BorgRepoConfig> it = repositoryList.iterator(); |
| | | while(it.hasNext()) { |
| | | BorgRepoConfig repoConfig = it.next(); |
| | | if (!StringUtils.startsWith(repoConfig.getRepo(), DEMO_IDENTIFIER)) { |
| | | continue; |
| | | } |
| | | if (set.contains(repoConfig.getRepo())) { |
| | | it.remove(); |
| | | } else { |
| | | set.add(repoConfig.getRepo()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | public static boolean isDemo(String name) { |
| | |
| | | if (commandLine == null) { |
| | | commandLine = buildCommandLine(); |
| | | } |
| | | if (commandLine == null) { |
| | | return null; |
| | | } |
| | | if (commandLineAsString == null) { |
| | | commandLineAsString = commandLine.getExecutable() + " " + StringUtils.join(commandLine.getArguments(), " "); |
| | | } |
| | |
| | | @Override |
| | | public JobResult<String> execute() { |
| | | getCommandLineAsString(); |
| | | if (commandLine == null) { |
| | | return null; |
| | | } |
| | | DefaultExecutor executor = new DefaultExecutor(); |
| | | if (workingDirectory != null) { |
| | | executor.setWorkingDirectory(workingDirectory); |
| | |
| | | package de.micromata.borgbutler.jobs; |
| | | |
| | | import de.micromata.borgbutler.utils.DateUtils; |
| | | import lombok.AccessLevel; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.concurrent.ExecutionException; |
| | | import java.util.concurrent.Future; |
| | | |
| | |
| | | @Setter |
| | | private boolean cancellationRequested; |
| | | @Getter |
| | | @Setter(AccessLevel.PROTECTED) |
| | | private Status status; |
| | | @Getter |
| | | @Setter |
| | |
| | | @Getter |
| | | @Setter(AccessLevel.PROTECTED) |
| | | private long uniqueJobNumber = -1; |
| | | @Getter |
| | | @Setter |
| | | private String createTime; |
| | | @Getter |
| | | @Setter |
| | | private String startTime; |
| | | @Getter |
| | | @Setter |
| | | private String stopTime; |
| | | |
| | | protected AbstractJob<T> setStatus(Status status) { |
| | | if (status == Status.RUNNING && this.status != Status.RUNNING) { |
| | | this.startTime = DateUtils.format(LocalDateTime.now()); |
| | | } else if (status != Status.RUNNING && this.status == Status.RUNNING) { |
| | | this.stopTime = DateUtils.format(LocalDateTime.now()); |
| | | } |
| | | this.status = status; |
| | | return this; |
| | | } |
| | | |
| | | public void cancel() { |
| | | if (this.getStatus() == Status.QUEUED) { |
| | | this.status = Status.CANCELLED; |
| | | setStatus(Status.CANCELLED); |
| | | } |
| | | this.cancellationRequested = true; |
| | | cancelRunningProcess(); |
| | | } |
| | | |
| | | protected void setCancelled() { |
| | | this.status = Status.CANCELLED; |
| | | setStatus(Status.CANCELLED); |
| | | } |
| | | |
| | | /** |
| | |
| | | if (this.status != Status.RUNNING) { |
| | | logger.error("Internal error, illegal state! You shouldn't set the job status to FAILED if not in status RUNNING: " + this.status); |
| | | } |
| | | this.status = Status.FAILED; |
| | | setStatus(Status.FAILED); |
| | | } |
| | | |
| | | /** |
| | |
| | | * @return |
| | | */ |
| | | public abstract Object getId(); |
| | | |
| | | protected AbstractJob() { |
| | | this.createTime = DateUtils.format(LocalDateTime.now()); |
| | | } |
| | | } |
| | |
| | | import java.util.concurrent.Executors; |
| | | |
| | | public class JobQueue<T> { |
| | | private static final int MAX_OLD_JOBS_SIZE = 50; |
| | | private static final int MAX_OLD_JOBS_SIZE = 10; |
| | | private static long jobSequence = 0; |
| | | private Logger log = LoggerFactory.getLogger(JobQueue.class); |
| | | private List<AbstractJob<T>> queue = new ArrayList<>(); |
| | |
| | | } |
| | | } |
| | | |
| | | public Iterator<AbstractJob<T>> getOldJobsIterator() { |
| | | synchronized (oldJobs) { |
| | | return Collections.unmodifiableList(oldJobs).iterator(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Searches only for queued jobs (not done jobs). |
| | | * |
| | |
| | | @Test |
| | | void cleanUpMaximumSizeTest() throws Exception { |
| | | List<BorgFilesystemItem> list = createList(1000000); |
| | | ArchiveFilelistCache cache = new ArchiveFilelistCache(new File("out"), 3); |
| | | ArchiveFilelistCache cache = new ArchiveFilelistCache(new File("out"), 5); |
| | | cache.removeAllCacheFiles(); |
| | | BorgRepoConfig repoConfig = new BorgRepoConfig(); |
| | | repoConfig.setRepo("repo"); |
| | |
| | | compile group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '2.27' |
| | | compile group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '2.27' |
| | | compile group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: '2.27' |
| | | compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6' |
| | | compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6' |
| | | compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' |
| | | compile group: 'javax.xml.ws', name: 'jaxws-api', version: '2.3.1' |
| | | compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25' |
| New file |
| | |
| | | package de.micromata.borgbutler.server; |
| | | |
| | | import de.micromata.borgbutler.BorgCommands; |
| | | import de.micromata.borgbutler.config.Configuration; |
| | | import de.micromata.borgbutler.config.ConfigurationHandler; |
| | | import org.apache.commons.io.FileUtils; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.apache.http.HttpResponse; |
| | | import org.apache.http.client.config.CookieSpecs; |
| | | import org.apache.http.client.config.RequestConfig; |
| | | import org.apache.http.client.methods.HttpGet; |
| | | import org.apache.http.impl.client.CloseableHttpClient; |
| | | import org.apache.http.impl.client.HttpClientBuilder; |
| | | import org.apache.http.impl.client.HttpClients; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | |
| | | public class BorgInstallation { |
| | | private Logger log = LoggerFactory.getLogger(BorgInstallation.class); |
| | | private static final BorgInstallation instance = new BorgInstallation(); |
| | | |
| | | public static BorgInstallation getInstance() { |
| | | return instance; |
| | | } |
| | | |
| | | public void initialize() { |
| | | Configuration configuration = ConfigurationHandler.getConfiguration(); |
| | | if (StringUtils.isNotBlank(configuration.getBorgCommand())) { |
| | | if (version(configuration)) { |
| | | return; |
| | | } |
| | | } |
| | | initialize(getBinary(RunningMode.getOSType())); |
| | | if (version(configuration)) { |
| | | return; |
| | | } |
| | | BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion(); |
| | | log.warn("No working borg version found. Please configure a borg version with minimal version '" + borgVersion.getMinimumRequiredBorgVersion() + "'."); |
| | | } |
| | | |
| | | /** |
| | | * Configures a new borg configuration if modifications was done. |
| | | * @param oldVersion The old version before this current modification. |
| | | */ |
| | | public void configure(BorgVersion oldVersion) { |
| | | Configuration configuration = ConfigurationHandler.getConfiguration(); |
| | | BorgVersion newVersion = ServerConfiguration.get()._getBorgVersion(); |
| | | boolean borgBinaryChanged = !StringUtils.equals(oldVersion.getBorgBinary(), newVersion.getBorgBinary()); |
| | | boolean manualBorgCommand = "manual".equals(newVersion.getBorgBinary()); |
| | | if (manualBorgCommand) { |
| | | boolean borgCommandChanged = !StringUtils.equals(oldVersion.getBorgCommand(), newVersion.getBorgCommand()); |
| | | if (borgCommandChanged) { |
| | | configuration.setBorgCommand(newVersion.getBorgCommand()); |
| | | version(configuration); |
| | | } else { |
| | | newVersion.copyFrom(oldVersion); // Restore all old settings. |
| | | } |
| | | } else { |
| | | if (borgBinaryChanged) { |
| | | newVersion.setBorgCommand(oldVersion.getBorgCommand()); // Don't modify borg command, if not manual. |
| | | initialize(getBinary(newVersion.getBorgBinary())); |
| | | } else { |
| | | newVersion.copyFrom(oldVersion); // Restore all old settings. |
| | | } |
| | | } |
| | | } |
| | | |
| | | private boolean initialize(String[] binary) { |
| | | if (binary == null) { |
| | | return false; |
| | | } |
| | | Configuration configuration = ConfigurationHandler.getConfiguration(); |
| | | File file = download(binary); |
| | | BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion(); |
| | | if (file != null) { |
| | | configuration.setBorgCommand(file.getAbsolutePath()); |
| | | borgVersion.setBorgCommand(file.getAbsolutePath()); |
| | | borgVersion.setBorgBinary(binary[0]); |
| | | } |
| | | return version(configuration); |
| | | } |
| | | |
| | | private boolean version(Configuration configuration) { |
| | | String versionString = BorgCommands.version(); |
| | | boolean versionOK = false; |
| | | BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion(); |
| | | String msg = null; |
| | | if (versionString != null) { |
| | | borgVersion.setVersion(versionString); |
| | | int cmp = versionString.compareTo(borgVersion.getMinimumRequiredBorgVersion()); |
| | | if (cmp < 0) { |
| | | msg = "Found borg version '" + versionString + "' is less than minimum required version '" + borgVersion.getMinimumRequiredBorgVersion() + "'."; |
| | | log.info(msg); |
| | | } else { |
| | | versionOK = true; |
| | | msg = "Found borg '" + configuration.getBorgCommand() + "', version: " + versionString + " (equals to or newer than '" + borgVersion.getMinimumRequiredBorgVersion() |
| | | + "', OK)."; |
| | | log.info(msg); |
| | | } |
| | | } else { |
| | | msg = "Couldn't execute borg command '" + configuration.getBorgCommand() + "'."; |
| | | } |
| | | borgVersion.setVersionOK(versionOK); |
| | | borgVersion.setStatusMessage(msg); |
| | | return versionOK; |
| | | } |
| | | |
| | | private String[] getBinary(RunningMode.OSType osType) { |
| | | String os = null; |
| | | switch (osType) { |
| | | case MAC_OS: |
| | | os = "mac"; |
| | | break; |
| | | case LINUX: |
| | | os = "linux64"; |
| | | break; |
| | | case FREEBSD: |
| | | os = "freebsd64"; |
| | | break; |
| | | } |
| | | return getBinary(os); |
| | | } |
| | | |
| | | private String[] getBinary(String os) { |
| | | if (os == null) { |
| | | return null; |
| | | } |
| | | BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion(); |
| | | for (String[] binary : borgVersion.getBorgBinaries()) { |
| | | if (binary[0].contains(os)) { |
| | | return binary; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | File download(RunningMode.OSType osType) { |
| | | String[] binary = getBinary(osType); |
| | | if (binary == null) { |
| | | log.info("Can't download binary (no binary found for OS '" + osType + "'."); |
| | | return null; |
| | | } |
| | | return download(binary); |
| | | } |
| | | |
| | | private File download(String[] binary) { |
| | | File file = getBinaryFile(binary); |
| | | if (file.exists()) { |
| | | // File already downloaded, nothing to do. |
| | | return file; |
| | | } |
| | | BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion(); |
| | | String url = borgVersion.getBinariesDownloadUrl() + getDownloadFilename(binary); |
| | | log.info("Trying to download borg binary '" + binary[0] + "' (" + binary[1] + ") from url: " + url + "..."); |
| | | HttpClientBuilder builder = HttpClients.custom() |
| | | .setDefaultRequestConfig(RequestConfig.custom() |
| | | .setCookieSpec(CookieSpecs.STANDARD).build()); |
| | | try (CloseableHttpClient httpClient = builder.build()) { |
| | | HttpGet getRequest = new HttpGet(url); |
| | | |
| | | HttpResponse response = httpClient.execute(getRequest); |
| | | |
| | | if (response.getStatusLine().getStatusCode() != 200) { |
| | | throw new RuntimeException("Failed : HTTP error code : " |
| | | + response.getStatusLine().getStatusCode()); |
| | | } |
| | | FileUtils.copyInputStreamToFile(response.getEntity().getContent(), file); |
| | | log.info("Downloaded: " + file.getAbsolutePath()); |
| | | file.setExecutable(true, false); |
| | | return file; |
| | | } catch (IOException ex) { |
| | | log.error(ex.getMessage(), ex); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private File getBinaryFile(String[] binary) { |
| | | File dir = new File(ConfigurationHandler.getInstance().getWorkingDir(), "bin"); |
| | | if (!dir.exists()) { |
| | | log.info("Creating binary directory: " + dir.getAbsolutePath()); |
| | | dir.mkdirs(); |
| | | } |
| | | BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion(); |
| | | return new File(dir, getDownloadFilename(binary) + "-" + borgVersion.getBinariesDownloadVersion()); |
| | | } |
| | | |
| | | private String getDownloadFilename(String[] binary) { |
| | | return "borg-" + binary[0]; |
| | | } |
| | | |
| | | private BorgInstallation() { |
| | | } |
| | | } |
| New file |
| | |
| | | package de.micromata.borgbutler.server; |
| | | |
| | | import lombok.AccessLevel; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | |
| | | public class BorgVersion { |
| | | @Getter |
| | | private String binariesDownloadVersion = "1.1.8"; |
| | | @Getter |
| | | private String binariesDownloadUrl = "https://github.com/borgbackup/borg/releases/download/" + binariesDownloadVersion + "/"; |
| | | @Getter |
| | | private String[][] borgBinaries = { |
| | | {"freebsd64", "FreeBSD 64"}, |
| | | {"linux32", "Linux 32"}, |
| | | {"linux64", "Linux 64"}, |
| | | {"macosx64", "MacOS X 64"}}; |
| | | |
| | | @Getter |
| | | private String minimumRequiredBorgVersion = "1.1.8"; |
| | | |
| | | /** |
| | | * One of the values "macosx64", "linux64" etc. for using a binary provided by BorgButler or null / "manual" for |
| | | * using a manual installed borg version. |
| | | */ |
| | | @Getter |
| | | @Setter(AccessLevel.PACKAGE) |
| | | private String borgBinary; |
| | | /** |
| | | * The path of the borg command to use. |
| | | */ |
| | | @Getter |
| | | @Setter |
| | | private String borgCommand; |
| | | |
| | | @Getter |
| | | @Setter(AccessLevel.PACKAGE) |
| | | private boolean versionOK = false; |
| | | @Getter |
| | | @Setter(AccessLevel.PACKAGE) |
| | | private String version; |
| | | @Getter |
| | | @Setter(AccessLevel.PACKAGE) |
| | | private String statusMessage; |
| | | |
| | | public BorgVersion copyFrom(BorgVersion other) { |
| | | this.borgCommand = other.borgCommand; |
| | | this.borgBinary = other.borgBinary; |
| | | this.versionOK = other.versionOK; |
| | | this.version = other.version; |
| | | this.statusMessage = other.statusMessage; |
| | | return this; |
| | | } |
| | | } |
| | |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import java.awt.*; |
| | | import java.io.*; |
| | | import java.text.DateFormat; |
| | | import java.text.SimpleDateFormat; |
| | |
| | | return; |
| | | } |
| | | } |
| | | if (Desktop.isDesktopSupported()) { |
| | | RunningMode.setServerType(RunningMode.ServerType.DESKTOP); |
| | | } else { |
| | | RunningMode.setServerType(RunningMode.ServerType.SERVER); |
| | | } |
| | | RunningMode.logMode(); |
| | | Runtime.getRuntime().addShutdownHook(new Thread() { |
| | | @Override |
| | |
| | | }); |
| | | |
| | | JettyServer server = startUp(); |
| | | BorgInstallation.getInstance().initialize(); |
| | | if (!line.hasOption('q')) { |
| | | |
| | | try { |
| | |
| | | |
| | | public enum UserManagement {SINGLE} |
| | | |
| | | public enum OSType {MAC_OS, WINDOWS, LINUX, OTHER} |
| | | public enum OSType {MAC_OS, WINDOWS, LINUX, FREEBSD, OTHER} |
| | | |
| | | private static boolean running; |
| | | private static File baseDir; |
| | |
| | | osType = OSType.WINDOWS; |
| | | } else if (osTypeString.toLowerCase().contains("linux")) { |
| | | osType = OSType.LINUX; |
| | | } else if (osTypeString.toLowerCase().contains("freebsd")) { |
| | | osType = OSType.FREEBSD; |
| | | } else { |
| | | osType = OSType.OTHER; |
| | | } |
| | |
| | | |
| | | private int port = WEBSERVER_PORT_DEFAULT; |
| | | private boolean webDevelopmentMode = WEB_DEVELOPMENT_MODE_PREF_DEFAULT; |
| | | private BorgVersion borgVersion = new BorgVersion(); |
| | | @JsonProperty |
| | | public String getCacheDir() { |
| | | return ButlerCache.getInstance().getCacheDir().getAbsolutePath(); |
| | |
| | | return SUPPORTED_LANGUAGES; |
| | | } |
| | | |
| | | /** |
| | | * @return a clone of this.borgVersion. |
| | | */ |
| | | public BorgVersion getBorgVersion() { |
| | | return new BorgVersion().copyFrom(borgVersion); |
| | | } |
| | | |
| | | BorgVersion _getBorgVersion() { |
| | | return this.borgVersion; |
| | | } |
| | | |
| | | public static String getApplicationHome() { |
| | | if (applicationHome == null) { |
| | | applicationHome = System.getProperty("applicationHome"); |
| | |
| | | super.copyFrom(other); |
| | | this.port = other.port; |
| | | this.webDevelopmentMode = other.webDevelopmentMode; |
| | | this.borgVersion.copyFrom(other.borgVersion); |
| | | this.setBorgCommand(this.borgVersion.getBorgCommand()); |
| | | } |
| | | } |
| | |
| | | public class ArchivesRest { |
| | | private static Logger log = LoggerFactory.getLogger(ArchivesRest.class); |
| | | |
| | | @GET |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param repo Name of repository ({@link Repository#getName()}. |
| | | * @param repoName Name of repository ({@link Repository#getName()}. |
| | | * @param archiveId Id or name of archive. |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @return Repository (including list of archives) as json string. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getArchive(@QueryParam("repo") String repoName, |
| | | @QueryParam("archiveId") String archiveId, @QueryParam("force") boolean force, |
| | | @QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | |
| | | return JsonUtils.toJson(archive, prettyPrinter); |
| | | } |
| | | |
| | | @GET |
| | | @Path("filelist") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param archiveId Id or name of archive. |
| | |
| | | * @return Repository (including list of archives) as json string. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("filelist") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getArchiveFileList(@QueryParam("archiveId") String archiveId, |
| | | @QueryParam("searchString") String searchString, |
| | | @QueryParam("mode") String mode, |
| | |
| | | return JsonUtils.toJson(items, prettyPrinter); |
| | | } |
| | | |
| | | @GET |
| | | @Path("/restore") |
| | | @Produces(MediaType.APPLICATION_OCTET_STREAM) |
| | | /** |
| | | * @param archiveId |
| | | * @param fileNumber The fileNumber of the file or directory in the archive served by BorgButler's |
| | | */ |
| | | @GET |
| | | @Path("/restore") |
| | | @Produces(MediaType.APPLICATION_OCTET_STREAM) |
| | | public Response restore(@QueryParam("archiveId") String archiveId, @QueryParam("fileNumber") int fileNumber) { |
| | | log.info("Requesting file #" + fileNumber + " of archive '" + archiveId + "'."); |
| | | FileSystemFilter filter = new FileSystemFilter().setFileNumber(fileNumber); |
| New file |
| | |
| | | package de.micromata.borgbutler.server.rest; |
| | | |
| | | import de.micromata.borgbutler.cache.ButlerCache; |
| | | import de.micromata.borgbutler.config.BorgRepoConfig; |
| | | import de.micromata.borgbutler.config.ConfigurationHandler; |
| | | import de.micromata.borgbutler.json.JsonUtils; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import javax.ws.rs.*; |
| | | import javax.ws.rs.core.MediaType; |
| | | |
| | | @Path("/repoConfig") |
| | | public class BorgRepoConfigsRest { |
| | | private static Logger log = LoggerFactory.getLogger(BorgRepoConfigsRest.class); |
| | | |
| | | /** |
| | | * @param id id or name of repo. |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @return {@link BorgRepoConfig} as json string. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getRepoConfig(@QueryParam("id") String id, @QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(id); |
| | | return JsonUtils.toJson(repoConfig, prettyPrinter); |
| | | } |
| | | |
| | | @POST |
| | | @Produces(MediaType.TEXT_PLAIN) |
| | | public void setRepoConfig(String jsonConfig) { |
| | | BorgRepoConfig newRepoConfig = JsonUtils.fromJson(BorgRepoConfig.class, jsonConfig); |
| | | BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(newRepoConfig.getId()); |
| | | if (repoConfig == null) { |
| | | log.error("Can't find repo config '" + newRepoConfig.getId() + "'. Can't save new settings."); |
| | | return; |
| | | } |
| | | ButlerCache.getInstance().clearRepoCacheAccess(repoConfig.getRepo()); |
| | | ButlerCache.getInstance().clearRepoCacheAccess(newRepoConfig.getRepo()); |
| | | repoConfig.copyFrom(newRepoConfig); |
| | | ConfigurationHandler.getInstance().save(); |
| | | } |
| | | } |
| | |
| | | import de.micromata.borgbutler.cache.ButlerCache; |
| | | import de.micromata.borgbutler.config.ConfigurationHandler; |
| | | import de.micromata.borgbutler.json.JsonUtils; |
| | | import de.micromata.borgbutler.server.BorgInstallation; |
| | | import de.micromata.borgbutler.server.BorgVersion; |
| | | import de.micromata.borgbutler.server.ServerConfiguration; |
| | | import de.micromata.borgbutler.server.user.UserData; |
| | | import de.micromata.borgbutler.server.user.UserManager; |
| | |
| | | public class ConfigurationRest { |
| | | private Logger log = LoggerFactory.getLogger(ConfigurationRest.class); |
| | | |
| | | @GET |
| | | @Path("config") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("config") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getConfig(@QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | String json = JsonUtils.toJson(ServerConfiguration.get(), prettyPrinter); |
| | | return json; |
| | |
| | | ConfigurationHandler configurationHandler = ConfigurationHandler.getInstance(); |
| | | ServerConfiguration config = (ServerConfiguration)configurationHandler.getConfiguration(); |
| | | ServerConfiguration srcConfig = JsonUtils.fromJson(ServerConfiguration.class, jsonConfig); |
| | | BorgVersion oldBorgVersion = config.getBorgVersion(); |
| | | config.copyFrom(srcConfig); |
| | | BorgInstallation.getInstance().configure(oldBorgVersion); |
| | | configurationHandler.save(); |
| | | } |
| | | |
| | | @GET |
| | | @Path("user") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("user") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getUser(@QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | UserData user = RestUtils.getUser(); |
| | | String json = JsonUtils.toJson(user, prettyPrinter); |
| New file |
| | |
| | | package de.micromata.borgbutler.server.rest; |
| | | |
| | | import de.micromata.borgbutler.json.JsonUtils; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.swing.*; |
| | | import javax.ws.rs.GET; |
| | | import javax.ws.rs.Path; |
| | | import javax.ws.rs.Produces; |
| | | import javax.ws.rs.QueryParam; |
| | | import javax.ws.rs.core.Context; |
| | | import javax.ws.rs.core.MediaType; |
| | | import java.io.File; |
| | | |
| | | @Path("/files") |
| | | public class FilesystemBrowserRest { |
| | | private Logger log = LoggerFactory.getLogger(FilesystemBrowserRest.class); |
| | | |
| | | /** |
| | | * Opens a directory browser or file browser on the desktop app and returns the chosen dir/file. Works only if Browser and Desktop app are running |
| | | * on the same host. |
| | | * |
| | | * @param current The current path of file. If not given the directory/file browser starts with the last used directory or user.home. |
| | | * @return The chosen directory path (absolute path). |
| | | */ |
| | | @GET |
| | | @Path("/browse-local-filesystem") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String browseLocalFilesystem(@Context HttpServletRequest requestContext, @QueryParam("current") String current) { |
| | | String msg = RestUtils.checkLocalDesktopAvailable(requestContext); |
| | | if (msg != null) { |
| | | log.info(msg); |
| | | return msg; |
| | | } |
| | | JFileChooser chooser; |
| | | if (StringUtils.isNotBlank(current)) { |
| | | chooser = new JFileChooser(current); |
| | | } else { |
| | | chooser = new JFileChooser(); |
| | | } |
| | | chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); |
| | | synchronized (FilesystemBrowserRest.class) { |
| | | if (frame == null) { |
| | | frame = new JFrame("BorgButler"); |
| | | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); |
| | | JLabel label = new JLabel("Hello World"); |
| | | frame.getContentPane().add(label); |
| | | frame.pack(); |
| | | } |
| | | } |
| | | frame.setVisible(true); |
| | | frame.setAlwaysOnTop(true); |
| | | int returnCode = chooser.showDialog(frame, "Choose"); |
| | | frame.setVisible(false); |
| | | frame.setAlwaysOnTop(false); |
| | | File file = null; |
| | | if (returnCode == JFileChooser.APPROVE_OPTION) { |
| | | file = chooser.getSelectedFile(); |
| | | } |
| | | String filename = file != null ? JsonUtils.toJson(file.getAbsolutePath()) : ""; |
| | | String result = "{\"directory\":" + filename + "}"; |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * @return OK, if the local desktop services such as open file browser etc. are available. |
| | | */ |
| | | @GET |
| | | @Path("/local-fileservices-available") |
| | | @Produces(MediaType.TEXT_PLAIN) |
| | | public String browseLocalFilesystem(@Context HttpServletRequest requestContext) { |
| | | String msg = RestUtils.checkLocalDesktopAvailable(requestContext); |
| | | if (msg != null) { |
| | | log.info(msg); |
| | | return msg; |
| | | } |
| | | return "OK"; |
| | | } |
| | | |
| | | private static JFrame frame; |
| | | } |
| | |
| | | public class I18nRest { |
| | | private Logger log = LoggerFactory.getLogger(I18nRest.class); |
| | | |
| | | @GET |
| | | @Path("list") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param requestContext For detecting the user's client locale. |
| | |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("list") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getList(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter, |
| | | @QueryParam("keysOnly") boolean keysOnly, @QueryParam("locale") String locale) { |
| | | Locale localeObject; |
| | |
| | | public class JobsRest { |
| | | private static Logger log = LoggerFactory.getLogger(JobsRest.class); |
| | | |
| | | private static List<JsonJobQueue> testList; |
| | | private static List<JsonJobQueue> testList, oldJobsTestList; |
| | | |
| | | @GET |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * @param repo If given, only the job queue of the given repo will be returned. |
| | | * @param testMode If true, then a test job list is created. |
| | |
| | | * @return Job queues as json string. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getJobs(@QueryParam("repo") String repo, |
| | | @QueryParam("testMode") boolean testMode, |
| | | @QueryParam("oldJobs") boolean oldJobs, |
| | | @QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | log.debug("getJobs repo=" + repo + ", oldJobs=" + oldJobs); |
| | | if (testMode) { |
| | | // Return dynamic test queue: |
| | | return returnTestList(prettyPrinter); |
| | | return returnTestList(oldJobs, prettyPrinter); |
| | | } |
| | | boolean validRepo = false; |
| | | if (StringUtils.isNotBlank(repo) && !"null".equals(repo) && !"undefined".equals(repo)) { |
| | |
| | | BorgQueueExecutor borgQueueExecutor = BorgQueueExecutor.getInstance(); |
| | | List<JsonJobQueue> queueList = new ArrayList<>(); |
| | | if (validRepo) { // Get only the queue of the given repo: |
| | | JsonJobQueue queue = getQueue(repo); |
| | | JsonJobQueue queue = getQueue(repo, oldJobs); |
| | | if (queue != null) { |
| | | queueList.add(queue); |
| | | } |
| | | } else { // Get all the queues (of all repos). |
| | | for (String rep : borgQueueExecutor.getRepos()) { |
| | | JsonJobQueue queue = getQueue(rep); |
| | | JsonJobQueue queue = getQueue(rep, oldJobs); |
| | | if (queue != null) { |
| | | queueList.add(queue); |
| | | } |
| | |
| | | return JsonUtils.toJson(queueList, prettyPrinter); |
| | | } |
| | | |
| | | @GET |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | @Path("/statistics") |
| | | /** |
| | | * @return The total number of jobs queued or running (and other statistics): {@link de.micromata.borgbutler.BorgQueueStatistics}. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | public String getStatistics() { |
| | | return JsonUtils.toJson(BorgQueueExecutor.getInstance().getStatistics()); |
| | | } |
| | | |
| | | private JsonJobQueue getQueue(String repo) { |
| | | private JsonJobQueue getQueue(String repo, boolean oldJobs) { |
| | | BorgQueueExecutor borgQueueExecutor = BorgQueueExecutor.getInstance(); |
| | | BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(repo); |
| | | if (repoConfig == null) { |
| | | return null; |
| | | } |
| | | List<BorgJob<?>> borgJobList = borgQueueExecutor.getJobListCopy(repoConfig); |
| | | List<BorgJob<?>> borgJobList = borgQueueExecutor.getJobListCopy(repoConfig, oldJobs); |
| | | if (CollectionUtils.isEmpty(borgJobList)) |
| | | return null; |
| | | JsonJobQueue queue = new JsonJobQueue().setRepo(repoConfig.getDisplayName()); |
| | |
| | | return queue; |
| | | } |
| | | |
| | | @Path("/cancel") |
| | | @GET |
| | | /** |
| | | * @param uniqueJobNumberString The id of the job to cancel. |
| | | */ |
| | | @Path("/cancel") |
| | | @GET |
| | | public void cancelJob(@QueryParam("uniqueJobNumber") String uniqueJobNumberString) { |
| | | Long uniqueJobNumber = null; |
| | | try { |
| | |
| | | /** |
| | | * Only for test purposes and development. |
| | | * |
| | | * @param oldJobs |
| | | * @param prettyPrinter |
| | | * @return |
| | | */ |
| | | private String returnTestList(boolean prettyPrinter) { |
| | | if (testList == null) { |
| | | testList = new ArrayList<>(); |
| | | private String returnTestList(boolean oldJobs, boolean prettyPrinter) { |
| | | List<JsonJobQueue> list = oldJobs ? oldJobsTestList : testList; |
| | | if (list == null) { |
| | | list = new ArrayList<>(); |
| | | long uniqueJobNumber = 100000; |
| | | JsonJobQueue queue = new JsonJobQueue().setRepo("My Computer"); |
| | | addTestJob(queue, "info", "my-macbook", 0, 2342) |
| | | .setUniqueJobNumber(uniqueJobNumber++); |
| | | addTestJob(queue, "list", "my-macbook", -1, -1) |
| | | .setUniqueJobNumber(uniqueJobNumber++); |
| | | testList.add(queue); |
| | | addTestJob(queue, "info", "my-macbook", 0, 2342, uniqueJobNumber++, oldJobs); |
| | | addTestJob(queue, "list", "my-macbook", -1, -1, uniqueJobNumber++, oldJobs); |
| | | list.add(queue); |
| | | |
| | | queue = new JsonJobQueue().setRepo("My Server"); |
| | | addTestJob(queue, "list", "my-server", 0, 1135821) |
| | | .setUniqueJobNumber(uniqueJobNumber++); |
| | | addTestJob(queue, "info", "my-server", -1, -1) |
| | | .setUniqueJobNumber(uniqueJobNumber++); |
| | | testList.add(queue); |
| | | addTestJob(queue, "list", "my-server", 0, 1135821, uniqueJobNumber++, oldJobs); |
| | | addTestJob(queue, "info", "my-server", -1, -1, uniqueJobNumber++, oldJobs); |
| | | list.add(queue); |
| | | if (oldJobs) { |
| | | oldJobsTestList = list; |
| | | } else { |
| | | for (JsonJobQueue jobQueue : testList) { |
| | | testList = list; |
| | | } |
| | | } else if (!oldJobs) { |
| | | for (JsonJobQueue jobQueue : list) { |
| | | for (JsonJob job : jobQueue.getJobs()) { |
| | | if (job.getStatus() != AbstractJob.Status.RUNNING) continue; |
| | | long current = job.getProgressInfo().getCurrent(); |
| | |
| | | } |
| | | } |
| | | } |
| | | return JsonUtils.toJson(testList, prettyPrinter); |
| | | return JsonUtils.toJson(list, prettyPrinter); |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param total |
| | | * @return |
| | | */ |
| | | private JsonJob addTestJob(JsonJobQueue queue, String operation, String host, long current, long total) { |
| | | private JsonJob addTestJob(JsonJobQueue queue, String operation, String host, long current, long total, long uniqueNumber, boolean oldJobs) { |
| | | ProgressInfo progressInfo = new ProgressInfo() |
| | | .setCurrent(current) |
| | | .setTotal(total); |
| | |
| | | if (queue.getJobs() == null) { |
| | | queue.setJobs(new ArrayList<>()); |
| | | } |
| | | job.setUniqueJobNumber(uniqueNumber); |
| | | if (oldJobs) { |
| | | job.setStatus(uniqueNumber % 2 == 0 ? AbstractJob.Status.CANCELLED : AbstractJob.Status.DONE); |
| | | } |
| | | queue.getJobs().add(job); |
| | | return job; |
| | | } |
| | |
| | | public class ReposRest { |
| | | private static Logger log = LoggerFactory.getLogger(ReposRest.class); |
| | | |
| | | @GET |
| | | @Path("list") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @return A list of repositories of type {@link BorgRepository}. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("list") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getList(@QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | List<Repository> repositories = ButlerCache.getInstance().getAllRepositories(); |
| | | if (CollectionUtils.isEmpty(repositories)) { |
| | |
| | | return JsonUtils.toJson(repositories, prettyPrinter); |
| | | } |
| | | |
| | | @GET |
| | | @Path("repo") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param id id or name of repo. |
| | | * @param force If true, a reload of all repositories is forced. |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @return Repository (without list of archives) as json string. |
| | | * @return {@link Repository} (without list of archives) as json string. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("repo") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getRepo(@QueryParam("id") String id, @QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | Repository repository = ButlerCache.getInstance().getRepository(id); |
| | | return JsonUtils.toJson(repository, prettyPrinter); |
| | | } |
| | | |
| | | @GET |
| | | @Path("repoArchiveList") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param id id or name of repo. |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @return Repository (including list of archives) as json string. |
| | | * @return {@link Repository} (including list of archives) as json string. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("repoArchiveList") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getRepoArchiveList(@QueryParam("id") String id, @QueryParam("force") boolean force, |
| | | @QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | if (force) { |
| New file |
| | |
| | | package de.micromata.borgbutler.server.rest; |
| | | |
| | | import de.micromata.borgbutler.BorgQueueStatistics; |
| | | import de.micromata.borgbutler.server.BorgVersion; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | |
| | | /** |
| | | * Statistics of all the job queues, especially the number of total queued and running jobs. |
| | | * This is used e. g. by the client for showing a badge near to the menu entry "job monitor" with the number |
| | | * of Jobs in the queues. |
| | | */ |
| | | public class SystemInfo { |
| | | @Getter |
| | | @Setter |
| | | private BorgQueueStatistics queueStatistics; |
| | | |
| | | @Getter |
| | | @Setter |
| | | private boolean configurationOK; |
| | | |
| | | @Getter |
| | | @Setter |
| | | private BorgVersion borgVersion; |
| | | } |
| New file |
| | |
| | | package de.micromata.borgbutler.server.rest; |
| | | |
| | | import de.micromata.borgbutler.BorgQueueExecutor; |
| | | import de.micromata.borgbutler.json.JsonUtils; |
| | | import de.micromata.borgbutler.server.BorgVersion; |
| | | import de.micromata.borgbutler.server.ServerConfiguration; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import javax.ws.rs.GET; |
| | | import javax.ws.rs.Path; |
| | | import javax.ws.rs.Produces; |
| | | import javax.ws.rs.core.MediaType; |
| | | |
| | | @Path("/system") |
| | | public class SystemInfoRest { |
| | | private static Logger log = LoggerFactory.getLogger(SystemInfoRest.class); |
| | | |
| | | /** |
| | | * @return The total number of jobs queued or running (and other statistics): {@link de.micromata.borgbutler.BorgQueueStatistics}. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | @Path("info") |
| | | public String getStatistics() { |
| | | BorgVersion borgVersion = ServerConfiguration.get().getBorgVersion(); |
| | | SystemInfo systemInfonfo = new SystemInfo() |
| | | .setQueueStatistics(BorgQueueExecutor.getInstance().getStatistics()) |
| | | .setConfigurationOK(borgVersion.isVersionOK()) |
| | | .setBorgVersion(borgVersion); |
| | | return JsonUtils.toJson(systemInfonfo); |
| | | } |
| | | } |
| | |
| | | public class VersionRest { |
| | | private Logger log = LoggerFactory.getLogger(VersionRest.class); |
| | | |
| | | @GET |
| | | @Path("version") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | /** |
| | | * |
| | | * @param requestContext For detecting the user's client locale. |
| | | * @param prettyPrinter If true then the json output will be in pretty format. |
| | | * @see JsonUtils#toJson(Object, boolean) |
| | | */ |
| | | @GET |
| | | @Path("version") |
| | | @Produces(MediaType.APPLICATION_JSON) |
| | | public String getVersion(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter) { |
| | | UserData user = RestUtils.getUser(); |
| | | String language = Languages.asString(user.getLocale()); |
| | |
| | | @Getter |
| | | @Setter |
| | | private String[] environmentVariables; |
| | | @Getter |
| | | @Setter |
| | | private String createTime; |
| | | @Getter |
| | | @Setter |
| | | private String startTime; |
| | | @Getter |
| | | @Setter |
| | | private String stopTime; |
| | | |
| | | public JsonJob() { |
| | | } |
| | |
| | | this.commandLineAsString = borgJob.getCommandLineAsString(); |
| | | this.description = borgJob.getDescription(); |
| | | environmentVariables = borgJob.getCommand().getRepoConfig().getEnvironmentVariables(); |
| | | this.createTime = borgJob.getCreateTime(); |
| | | this.startTime = borgJob.getStartTime(); |
| | | this.stopTime = borgJob.getStopTime(); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | log4j.appender.file=org.apache.log4j.RollingFileAppender |
| | | |
| | | log4j.appender.file.File=merlin.log |
| | | log4j.appender.file.File=borgbutler.log |
| | | log4j.appender.file.MaxFileSize=10MB |
| | | log4j.appender.file.MaxBackupIndex=5 |
| | | log4j.appender.file.layout=org.apache.log4j.PatternLayout |
| New file |
| | |
| | | package de.micromata.borgbutler.server; |
| | | |
| | | import de.micromata.borgbutler.config.ConfigurationHandler; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import java.io.File; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | public class BorgInstallationTest { |
| | | private Logger log = LoggerFactory.getLogger(BorgInstallationTest.class); |
| | | |
| | | @Test |
| | | void foo() throws Exception { |
| | | ConfigurationHandler.getConfiguration().setBorgCommand(null); |
| | | BorgInstallation borgInstallation = BorgInstallation.getInstance(); |
| | | borgInstallation.initialize(); |
| | | ConfigurationHandler.getConfiguration().setBorgCommand("borg"); |
| | | borgInstallation.initialize(); |
| | | } |
| | | |
| | | @Test |
| | | void downloadTest() { |
| | | String version = new BorgVersion().getBinariesDownloadVersion(); |
| | | checkDownload(RunningMode.OSType.LINUX, "borg-linux64-" + version); |
| | | checkDownload(RunningMode.OSType.MAC_OS, "borg-macosx64-" + version); |
| | | checkDownload(RunningMode.OSType.FREEBSD, "borg-freebsd64-" + version); |
| | | assertNull(BorgInstallation.getInstance().download(RunningMode.OSType.WINDOWS)); |
| | | } |
| | | |
| | | private void checkDownload(RunningMode.OSType osType, String expectedName) { |
| | | File file = BorgInstallation.getInstance().download(osType); |
| | | assertEquals(expectedName, file.getName()); |
| | | assertTrue(file.canExecute()); |
| | | } |
| | | } |
| | |
| | | import classNames from 'classnames'; |
| | | import {FormFeedback, Input, UncontrolledTooltip} from 'reactstrap'; |
| | | import {FormCheckbox} from "./FormCheckbox"; |
| | | import {FormRadioButton} from "./FormRadioButton"; |
| | | import {FormButton} from "./FormButton"; |
| | | import {FormSelect, FormOption} from "./FormSelect"; |
| | | import {FormOption, FormSelect} from "./FormSelect"; |
| | | import {revisedRandId} from "../../../utilities/global"; |
| | | import I18n from "../translation/I18n"; |
| | | |
| | |
| | | </UncontrolledTooltip>; |
| | | } |
| | | const {fieldLength, className, hint, hintPlacement, id, ...other} = props; |
| | | let myClassName = className; |
| | | if (fieldLength > 0) { |
| | | myClassName = classNames(`col-sm-${props.fieldLength}`, className); |
| | | } |
| | | return ( |
| | | <React.Fragment> |
| | | <Input id={targetId} |
| | | className={classNames(`col-sm-${props.fieldLength}`, className)} |
| | | className={myClassName} |
| | | {...other} |
| | | /> |
| | | {tooltip} |
| | |
| | | name: '', |
| | | hint: null, |
| | | hintPlacement: 'top', |
| | | fieldLength: 10, |
| | | value: '', |
| | | min: null, |
| | | max: null, |
| | |
| | | |
| | | |
| | | const FormLabelField = (props) => { |
| | | const forId = props.children.props.id || props.htmlFor || revisedRandId(); |
| | | const forId = props.children.props.id || props.children.props.name || props.htmlFor || revisedRandId(); |
| | | return ( |
| | | <FormGroup> |
| | | <FormGroup className={props.className}> |
| | | <FormLabel length={props.labelLength} htmlFor={forId}> |
| | | {props.label} |
| | | </FormLabel> |
| | |
| | | |
| | | FormLabelField.propTypes = { |
| | | id: PropTypes.string, |
| | | className: PropTypes.string, |
| | | htmlFor: PropTypes.string, |
| | | validationMessage: PropTypes.string, |
| | | labelLength: PropTypes.number, |
| | |
| | | |
| | | FormLabelField.defaultProps = { |
| | | id: null, |
| | | className: null, |
| | | htmlFor: null, |
| | | validationMessage: null, |
| | | labelLength: 2, |
| | |
| | | label={props.label} |
| | | hint={props.hint} |
| | | validationState={props.validationState} |
| | | className={props.className} |
| | | > |
| | | <FormInput |
| | | id={props.id} |
| | |
| | | FormLabelInputField.propTypes = { |
| | | id: PropTypes.string, |
| | | label: PropTypes.node, |
| | | className: PropTypes.node, |
| | | labelLength: PropTypes.number, |
| | | fieldLength: PropTypes.number, |
| | | hint: PropTypes.string, |
| | |
| | | |
| | | FormLabelInputField.defaultProps = { |
| | | id: null, |
| | | className: null, |
| | | label: '', |
| | | labelLength: 2, |
| | | fieldLength: 10, |
| | |
| | | FormSelect, |
| | | FormOption, |
| | | FormCheckbox, |
| | | FormRadioButton, |
| | | FormLabelInputField, |
| | | FormFieldset, |
| | | FormButton, |
| New file |
| | |
| | | import React from 'react'; |
| | | import PropTypes from 'prop-types'; |
| | | import {UncontrolledTooltip} from 'reactstrap'; |
| | | import {revisedRandId} from "../../../utilities/global"; |
| | | import classNames from 'classnames'; |
| | | import I18n from "../translation/I18n"; |
| | | |
| | | class FormRadioButton extends React.Component { |
| | | |
| | | _id = this.props.id || revisedRandId(); |
| | | |
| | | render() { |
| | | const {id, className, labelKey, label, hint, hintKey, ...other} = this.props; |
| | | let tooltip = null; |
| | | let hintId = null; |
| | | if (hint || hintKey) { |
| | | hintId = `hint-${this._id}`; |
| | | tooltip = |
| | | <UncontrolledTooltip placement="right" target={hintId}> |
| | | {hintKey ? <I18n name={hintKey}/> : hint} |
| | | </UncontrolledTooltip>; |
| | | } |
| | | let labelNode = <label |
| | | className={'custom-control-label'} |
| | | htmlFor={this._id}> |
| | | {labelKey ? <I18n name={labelKey}/> : this.props.label} |
| | | </label>; |
| | | return ( |
| | | <React.Fragment> |
| | | <div className="custom-control custom-radio custom-control-inline" id={hintId}> |
| | | <input type="radio" |
| | | id={this._id} |
| | | className={classNames('custom-control-input', className)} |
| | | {...other} |
| | | /> |
| | | {labelNode} |
| | | </div> |
| | | {tooltip} |
| | | </React.Fragment> |
| | | ); |
| | | } |
| | | } |
| | | |
| | | FormRadioButton.propTypes = { |
| | | id: PropTypes.string, |
| | | name: PropTypes.string, |
| | | checked: PropTypes.bool, |
| | | onChange: PropTypes.func, |
| | | hint: PropTypes.string, |
| | | label: PropTypes.node, |
| | | labelKey: PropTypes.string |
| | | }; |
| | | |
| | | FormRadioButton.defaultProps = { |
| | | checked: false, |
| | | onChange: null |
| | | }; |
| | | |
| | | |
| | | export { |
| | | FormRadioButton |
| | | }; |
| | |
| | | {props.hint} |
| | | </UncontrolledTooltip>; |
| | | } |
| | | const {className, hint, hintPlacement, id, ...other} = props; |
| | | const {fieldLength, className, hint, hintPlacement, id, ...other} = props; |
| | | let myClassName = className; |
| | | if (fieldLength > 0) { |
| | | myClassName = classNames(`col-sm-${props.fieldLength}`, className); |
| | | } |
| | | return ( |
| | | <React.Fragment> |
| | | <select id={targetId} |
| | | className={classNames('custom-select form-control form-control-sm mr-1', className)} |
| | | className={classNames('custom-select form-control form-control-sm mr-1', myClassName)} |
| | | {...other} |
| | | > |
| | | {props.children} |
| | |
| | | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), |
| | | name: PropTypes.string, |
| | | onChange: PropTypes.func, |
| | | fieldLength: PropTypes.number, |
| | | hint: PropTypes.string, |
| | | hintPlacement: PropTypes.oneOf(['right', 'top']), |
| | | children: PropTypes.node, |
| | |
| | | />; |
| | | } else if (this.state.archive) { |
| | | pageHeader = <React.Fragment> |
| | | <Link to={`/repoArchives/${this.state.repoId}`}> {archive.repoDisplayName}</Link> - {archive.name} |
| | | <Link to={'/repos'}> Repositories</Link> - |
| | | <Link |
| | | to={`/repoArchives/${this.state.repoId}/${archive.repoDisplayName}`}> {archive.repoDisplayName}</Link> - {archive.name} |
| | | <div |
| | | className={'btn btn-outline-primary refresh-button-right'} |
| | | onClick={this.toggleModal} |
| | |
| | | import ConfigAccountTab from "./ConfigurationAccountTab"; |
| | | import ConfigServerTab from "./ConfigurationServerTab"; |
| | | import LoadingOverlay from '../../general/loading/LoadingOverlay'; |
| | | import ConfirmModal from '../../general/modal/ConfirmModal'; |
| | | |
| | | class ConfigurationPage |
| | | extends React |
| | |
| | | this.state = { |
| | | activeTab: '1', |
| | | loading: false, |
| | | reload: false |
| | | reload: false, |
| | | confirmModal: false |
| | | }; |
| | | |
| | | this.onSave = this.onSave.bind(this); |
| | | this.onCancel = this.onCancel.bind(this); |
| | | this.toggleModal = this.toggleModal.bind(this); |
| | | } |
| | | |
| | | toggleTab = tab => () => { |
| | |
| | | this.setReload(); |
| | | } |
| | | |
| | | toggleModal() { |
| | | this.setState({ |
| | | confirmModal: !this.state.confirmModal |
| | | }) |
| | | } |
| | | |
| | | render() { |
| | | // https://codepen.io/_arpy/pen/xYoyPW |
| | | if (this.state.reload) { |
| | |
| | | } |
| | | return ( |
| | | <React.Fragment> |
| | | <ConfirmModal |
| | | onConfirm={ConfigServerTab.clearAllCaches} |
| | | title={'Are you sure?'} |
| | | toggle={this.toggleModal} |
| | | open={this.state.confirmModal} |
| | | > |
| | | Do you really want to clear all caches? All Archive file lists and caches for repo and archive |
| | | information will be cleared. |
| | | <br/> |
| | | This is a safe option but it may take some time to re-fill the caches (on demand) again. |
| | | </ConfirmModal> |
| | | <PageHeader><I18n name={'configuration'}/></PageHeader> |
| | | <Nav tabs> |
| | | <NavLink |
| | |
| | | </TabContent> |
| | | <FormGroup> |
| | | <FormField length={12}> |
| | | <FormButton id={'clearAllCaches'} onClick={this.toggleModal}> Clear all caches |
| | | </FormButton> |
| | | <FormButton onClick={this.onCancel} |
| | | hintKey="configuration.cancel.hint"><I18n name={'common.cancel'}/> |
| | | </FormButton> |
| | |
| | | import React from 'react'; |
| | | import {FormButton, FormCheckbox, FormLabelField, FormLabelInputField} from '../../general/forms/FormComponents'; |
| | | import {Alert} from 'reactstrap'; |
| | | import { |
| | | FormCheckbox, |
| | | FormField, |
| | | FormGroup, |
| | | FormInput, |
| | | FormLabel, |
| | | FormLabelField, |
| | | FormLabelInputField, |
| | | FormOption, |
| | | FormSelect |
| | | } from '../../general/forms/FormComponents'; |
| | | import {getRestServiceUrl} from '../../../utilities/global'; |
| | | import I18n from '../../general/translation/I18n'; |
| | | import ErrorAlertGenericRestFailure from '../../general/ErrorAlertGenericRestFailure'; |
| | | import Loading from '../../general/Loading'; |
| | | import ConfirmModal from '../../general/modal/ConfirmModal'; |
| | | |
| | | class ConfigServerTab extends React.Component { |
| | | loadConfig = () => { |
| | |
| | | .then((data) => { |
| | | this.setState({ |
| | | loading: false, |
| | | borgBinary: data.borgVersion.borgBinary, |
| | | borgCommand: data.borgVersion.borgCommand, |
| | | ...data |
| | | }) |
| | | }) |
| | |
| | | showDemoRepos: true, |
| | | maxArchiveContentCacheCapacityMb: 100, |
| | | redirect: false, |
| | | confirmModal: false |
| | | borgCommand: null, |
| | | borgBinary: null |
| | | }; |
| | | |
| | | this.handleTextChange = this.handleTextChange.bind(this); |
| | | this.handleCheckboxChange = this.handleCheckboxChange.bind(this); |
| | | this.loadConfig = this.loadConfig.bind(this); |
| | | this.toggleModal = this.toggleModal.bind(this); |
| | | } |
| | | |
| | | componentDidMount() { |
| | |
| | | port: this.state.port, |
| | | maxArchiveContentCacheCapacityMb : this.state.maxArchiveContentCacheCapacityMb, |
| | | webDevelopmentMode: this.state.webDevelopmentMode, |
| | | showDemoRepos: this.state.showDemoRepos |
| | | showDemoRepos: this.state.showDemoRepos, |
| | | borgVersion: { |
| | | borgCommand: this.state.borgCommand, |
| | | borgBinary: this.state.borgBinary |
| | | } |
| | | }; |
| | | return fetch(getRestServiceUrl("configuration/config"), { |
| | | method: 'POST', |
| | |
| | | }) |
| | | } |
| | | |
| | | toggleModal() { |
| | | this.setState({ |
| | | confirmModal: !this.state.confirmModal |
| | | }) |
| | | } |
| | | |
| | | render() { |
| | | if (this.state.loading) { |
| | | return <Loading/>; |
| | |
| | | if (this.state.failed) { |
| | | return <ErrorAlertGenericRestFailure handleClick={this.loadConfig}/>; |
| | | } |
| | | |
| | | const borgVersion = this.state.borgVersion; |
| | | let borgInfoColor = 'success'; |
| | | let borgInfoMessage = `Borg version '${borgVersion.version}' is OK.`; |
| | | if (!borgVersion.versionOK) { |
| | | borgInfoColor = 'danger'; |
| | | borgInfoMessage = borgVersion.statusMessage; |
| | | } |
| | | return ( |
| | | <div> |
| | | <ConfirmModal |
| | | onConfirm={ConfigServerTab.clearAllCaches} |
| | | title={'Are you sure?'} |
| | | toggle={this.toggleModal} |
| | | open={this.state.confirmModal} |
| | | > |
| | | Do you really want to clear all caches? All Archive file lists and caches for repo and archive |
| | | information will be cleared. |
| | | <br/> |
| | | This is a safe option but it may take some time to re-fill the caches (on demand) again. |
| | | </ConfirmModal> |
| | | <React.Fragment> |
| | | <form> |
| | | <FormLabelField> |
| | | <FormButton id={'clearAllCaches'} onClick={this.toggleModal}> Clear all caches |
| | | </FormButton> |
| | | </FormLabelField> |
| | | <FormGroup> |
| | | <FormLabel>{'Borg command'}</FormLabel> |
| | | <FormField length={2}> |
| | | <FormSelect |
| | | value={this.state.borgBinary} |
| | | name={'borgBinary'} |
| | | onChange={this.handleTextChange} |
| | | hint={`Choose your OS and BorgButler will download and use a ready to run borg binary from ${borgVersion.binariesDownloadUrl} or choose a manual installed version.`} |
| | | > |
| | | {borgVersion.borgBinaries |
| | | .map((binary, index) => <FormOption label={binary[1]} value={binary[0]} |
| | | key={index}/>)} |
| | | <FormOption label={'Manual'} value={'manual'}/> |
| | | </FormSelect> |
| | | </FormField> |
| | | <FormInput fieldLength={8} name={'borgCommand'} value={this.state.borgCommand} |
| | | onChange={this.handleTextChange} |
| | | placeholder="Enter path of borg command" |
| | | disabled={this.state.borgBinary !== "manual"}/> |
| | | </FormGroup> |
| | | <FormGroup> |
| | | <FormField length={4}/> |
| | | <Alert className={'col-sm-8'} color={borgInfoColor}> |
| | | {borgInfoMessage} |
| | | </Alert> |
| | | </FormGroup> |
| | | <FormLabelInputField label={'Port'} fieldLength={2} type="number" min={0} max={65535} |
| | | step={1} |
| | | name={'port'} value={this.state.port} |
| | |
| | | onChange={this.handleCheckboxChange} /> |
| | | </FormLabelField> |
| | | </form> |
| | | </div> |
| | | </React.Fragment> |
| | | ); |
| | | } |
| | | } |
| | |
| | | width: calc(100% - 3em); |
| | | margin-top: -10px; |
| | | } |
| | | |
| | | .job-progress div.progress { |
| | | margin: 0 .75rem .375rem; |
| | | } |
| | | |
| | | .onclick { |
| | | cursor: pointer; |
| | | } |
| | |
| | | render() { |
| | | let content = undefined; |
| | | let job = this.props.job; |
| | | let cancelDisabled = undefined; |
| | | let times = ' (created: ' + job.createTime; |
| | | if (job.startTime) { |
| | | times += ', started: ' + job.startTime; |
| | | } |
| | | if (job.stopTime) { |
| | | times += ', ended: ' + job.stopTime; |
| | | } |
| | | times += ')'; |
| | | if ((job.status !== 'RUNNING' && job.status !== 'QUEUED') || job.cancellationRequested) { |
| | | cancelDisabled = true; |
| | | } |
| | | let cancelButton = <div className="job-cancel"><Button color={'danger'} |
| | | onClick={() => this.cancelJob(job.uniqueJobNumber)} |
| | | disabled={cancelDisabled}><IconCancel/></Button> |
| | | </div>; |
| | | if (job.status === 'RUNNING') { |
| | | let progressPercent = 100; |
| | | let color = 'success'; |
| | |
| | | progressPercent = job.progressPercent; |
| | | } |
| | | content = <Progress animated color={color} value={progressPercent}>{job.progressText}</Progress>; |
| | | } else if (job.status === 'CANCELLED') { |
| | | content = <Progress color={'warning'} value={100}>{job.status}</Progress> |
| | | cancelButton = ''; |
| | | } else if (job.status === 'FAILED') { |
| | | content = <Progress color={'danger'} value={100}>{job.status}</Progress> |
| | | cancelButton = ''; |
| | | } else if (job.status === 'DONE') { |
| | | content = <Progress color={'success'} value={100}>{job.status}</Progress> |
| | | cancelButton = ''; |
| | | } else { |
| | | content = <Progress color={'info'} value={100}>{job.status}</Progress> |
| | | } |
| | | let cancelDisabled = undefined; |
| | | if ((job.status !== 'RUNNING' && job.status !== 'QUEUED') || job.cancellationRequested) { |
| | | cancelDisabled = true; |
| | | } |
| | | let environmentVariables = ''; |
| | | if (job.environmentVariables && Array.isArray(job.environmentVariables)) { |
| | | environmentVariables = job.environmentVariables.map((variable, index) => <React.Fragment key={index}> |
| | |
| | | <Button color="link" onClick={this.toggle}>{job.description}</Button> |
| | | {content} |
| | | </div> |
| | | <div className="job-cancel"><Button color={'danger'} |
| | | onClick={() => this.cancelJob(job.uniqueJobNumber)} |
| | | disabled={cancelDisabled}><IconCancel/></Button> |
| | | </div> |
| | | {cancelButton} |
| | | </div> |
| | | <Collapse isOpen={this.state.collapse}> |
| | | <Card> |
| | |
| | | <tbody> |
| | | <tr> |
| | | <th>Status</th> |
| | | <td>{job.status}</td> |
| | | <td>{job.status} {times}</td> |
| | | </tr> |
| | | <tr> |
| | | <th>Command line</th> |
| | |
| | | import React from 'react'; |
| | | import {Button} from 'reactstrap'; |
| | | import {Button, Collapse} from 'reactstrap'; |
| | | import {getRestServiceUrl, isDevelopmentMode} from '../../../utilities/global'; |
| | | import JobQueue from './JobQueue'; |
| | | import ErrorAlert from '../archives/ArchiveView'; |
| | | import ErrorAlert from '../../general/ErrorAlert'; |
| | | import PropTypes from 'prop-types'; |
| | | |
| | | class JobMonitorPanel extends React.Component { |
| | | state = { |
| | | isFetching: false, |
| | | testMode: false |
| | | isFetchingOldJobs: false, |
| | | testMode: false, |
| | | collapseOldJobs: false |
| | | }; |
| | | |
| | | componentDidMount = () => { |
| | | this.fetchQueues(); |
| | | this.interval = setInterval(() => this.fetchQueues(), 2000); |
| | | this.fetchQueues(false); |
| | | this.interval = setInterval(() => this.fetchJobs(), 2000); |
| | | }; |
| | | |
| | | componentWillUnmount() { |
| | |
| | | }); |
| | | } |
| | | |
| | | fetchQueues = () => { |
| | | toggleOldJobs() { |
| | | if (!this.state.collapseOldJobs) { |
| | | this.fetchQueues(true); |
| | | } |
| | | this.setState({collapseOldJobs: !this.state.collapseOldJobs}); |
| | | } |
| | | |
| | | fetchJobs() { |
| | | if (!this.state.isFetching) { // Don't run twice at the same time |
| | | this.fetchQueues(false); |
| | | } |
| | | if (this.state.collapseOldJobs && !this.state.isFetchingOldJobs) { |
| | | this.fetchQueues(true); |
| | | } |
| | | } |
| | | |
| | | fetchQueues = (oldJobs) => { |
| | | let queuesVar = 'queues'; |
| | | let isFetchingVar = 'isFetching'; |
| | | let failedVar = 'failed'; |
| | | if (oldJobs) { |
| | | queuesVar = 'oldJobsQueues'; |
| | | isFetchingVar = 'isFetchingOldJobs'; |
| | | failedVar = 'oldJobsFailed'; |
| | | } |
| | | this.setState({ |
| | | isFetching: true, |
| | | failed: false |
| | | [isFetchingVar]: true, |
| | | [failedVar]: false |
| | | }); |
| | | fetch(getRestServiceUrl('jobs', { |
| | | repo: this.props.repo, |
| | | testMode: this.state.testMode |
| | | testMode: this.state.testMode, |
| | | oldJobs: oldJobs |
| | | }), { |
| | | method: 'GET', |
| | | headers: { |
| | |
| | | return queue; |
| | | }); |
| | | this.setState({ |
| | | isFetching: false, |
| | | queues |
| | | [isFetchingVar]: false, |
| | | [queuesVar]: queues |
| | | }); |
| | | }) |
| | | .catch(() => this.setState({isFetching: false, failed: true})); |
| | | .catch(() => this.setState({[isFetchingVar]: false, [failedVar]: true})); |
| | | }; |
| | | |
| | | render() { |
| | |
| | | content = <i>Loading...</i>; |
| | | } else if (this.state.failed) { |
| | | content = <ErrorAlert |
| | | title={'Cannot load Repositories'} |
| | | title={'Cannot load job queues'} |
| | | description={'Something went wrong during contacting the rest api.'} |
| | | action={{ |
| | | handleClick: this.fetchQueues, |
| | |
| | | key={queue.repo} |
| | | />)} |
| | | </React.Fragment>; |
| | | } else if (isDevelopmentMode() && !this.props.embedded) { |
| | | content = <React.Fragment>No jobs are running or queued.<br/><br/> |
| | | <Button color="primary" onClick={this.toggleTestMode}>Test mode</Button> |
| | | </React.Fragment> |
| | | } else { |
| | | content = <React.Fragment>No jobs are running or queued.</React.Fragment> |
| | | } |
| | | } |
| | | let oldJobs = ''; |
| | | |
| | | if (this.state.isFetchingOldJobs && !this.state.oldJobsQueues) { |
| | | oldJobs = <i>Loading...</i>; |
| | | } else if (this.state.oldJobsFailed) { |
| | | oldJobs = <ErrorAlert |
| | | title={'Cannot load old job queues'} |
| | | description={'Something went wrong during contacting the rest api.'} |
| | | action={{ |
| | | handleClick: this.fetchQueues, |
| | | title: 'Try again' |
| | | }} |
| | | />; |
| | | } else if (this.state.oldJobsQueues) { |
| | | if (this.state.oldJobsQueues.length > 0) { |
| | | oldJobs = <React.Fragment> |
| | | {this.state.oldJobsQueues |
| | | .map((queue) => <JobQueue |
| | | embedded={this.props.embedded} |
| | | queue={queue} |
| | | key={queue.repo} |
| | | />)} |
| | | </React.Fragment> |
| | | } else { |
| | | oldJobs = <React.Fragment>No old jobs available.</React.Fragment> |
| | | } |
| | | } |
| | | let testContent = ''; |
| | | if (isDevelopmentMode() && !this.props.embedded) { |
| | | testContent = <React.Fragment><br/><br/><br/><Button className="btn-outline-info" onClick={this.toggleTestMode}>Test mode</Button></React.Fragment>; |
| | | } |
| | | |
| | | return <React.Fragment> |
| | | {content} |
| | | <h5 className={'onclick'} onClick={this.toggleOldJobs}>Show old jobs |
| | | </h5> |
| | | <Collapse isOpen={this.state.collapseOldJobs}> |
| | | {oldJobs} |
| | | </Collapse> |
| | | {testContent} |
| | | </React.Fragment>; |
| | | } |
| | | |
| | |
| | | |
| | | this.fetchQueues = this.fetchQueues.bind(this); |
| | | this.toggleTestMode = this.toggleTestMode.bind(this); |
| | | this.toggleOldJobs = this.toggleOldJobs.bind(this); |
| | | } |
| | | } |
| | | |
| | | JobMonitorPanel.propTypes = { |
| | | JobMonitorPanel |
| | | .propTypes = { |
| | | embedded: PropTypes.bool, |
| | | repo: PropTypes.string |
| | | }; |
| | | |
| | | JobMonitorPanel.defaultProps = { |
| | | JobMonitorPanel |
| | | .defaultProps = { |
| | | embedded: true, |
| | | repo: null |
| | | }; |
| | |
| | | import React from 'react'; |
| | | import {Card, CardBody, CardHeader, ListGroup} from 'reactstrap'; |
| | | import {Card, CardBody, CardHeader, Collapse, ListGroup} from 'reactstrap'; |
| | | import Job from "./Job"; |
| | | |
| | | function JobQueue({queue, embedded}) { |
| | | class JobQueue extends React.Component { |
| | | state = { |
| | | collapsed: true |
| | | }; |
| | | |
| | | toggle() { |
| | | this.setState({collapsed: !this.state.collapsed}); |
| | | } |
| | | |
| | | |
| | | render() { |
| | | const queue = this.props.queue; |
| | | return ( |
| | | <div> |
| | | <Card> |
| | | <CardHeader>Job queue: {queue.repo}</CardHeader> |
| | | <CardHeader className={'onclick'} onClick={this.toggle}>Job queue: {queue.repo}</CardHeader> |
| | | <Collapse isOpen={this.state.collapsed}> |
| | | <CardBody> |
| | | <ListGroup> |
| | | {queue.jobs |
| | | .map((job, index) => <Job |
| | | embedded={embedded} |
| | | embedded={this.props.embedded} |
| | | job={job} |
| | | key={job.uniqueJobNumber} |
| | | />)} |
| | | </ListGroup> |
| | | </CardBody> |
| | | </Collapse> |
| | | </Card> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | | constructor(props) { |
| | | super(props); |
| | | this.toggle = this.toggle.bind(this); |
| | | |
| | | } |
| | | } |
| | | |
| | | export default JobQueue; |
| New file |
| | |
| | | import React from 'react'; |
| | | import {Link} from 'react-router-dom' |
| | | import { |
| | | FormButton, |
| | | FormField, |
| | | FormGroup, |
| | | FormInput, |
| | | FormLabel, |
| | | FormLabelInputField, |
| | | FormOption, |
| | | FormRadioButton, |
| | | FormSelect |
| | | } from '../../general/forms/FormComponents'; |
| | | import I18n from "../../general/translation/I18n"; |
| | | import {PageHeader} from "../../general/BootstrapComponents"; |
| | | |
| | | class ConfigureRepoPage extends React.Component { |
| | | |
| | | constructor(props) { |
| | | super(props); |
| | | this.handleTextChange = this.handleTextChange.bind(this); |
| | | this.handleRepoTextChange = this.handleRepoTextChange.bind(this); |
| | | this.handleCheckboxChange = this.handleCheckboxChange.bind(this); |
| | | this.state = { |
| | | repoConfig: {}, |
| | | mode: 'existingRepo', |
| | | localRemote: 'local', |
| | | encryption: 'repokey', |
| | | passwordMethod: 'passwordCommand', |
| | | passwordCreate: null |
| | | }; |
| | | } |
| | | |
| | | handleTextChange = event => { |
| | | event.preventDefault(); |
| | | this.setState({[event.target.name]: event.target.value}); |
| | | if (event.target.name === 'passwordMethod') { |
| | | const value = event.target.value; |
| | | var passwordCommand = null; |
| | | var passwordCreate = null; |
| | | if (value === 'passwordFile') { |
| | | passwordCommand = 'cat ~/.borg-passphrase'; |
| | | passwordCreate = <React.Fragment> |
| | | Create a file with a password in it in your home directory and use permissions to keep anyone else |
| | | from |
| | | reading it:<br/> |
| | | <div className="command-line">head -c 1024 /dev/urandom | base64 > ~/.borg-passphrase<br/> |
| | | chmod 400 ~/.borg-passphrase |
| | | </div> |
| | | </React.Fragment>; |
| | | } else if (value === 'macos-keychain') { |
| | | passwordCommand = 'security find-generic-password -a $USER -s borg-passphrase'; |
| | | passwordCreate = <React.Fragment> |
| | | Generate a passphrase and use security to save it to your login (default) keychain:<br/> |
| | | <div className="command-line">security add-generic-password -D secret -U -a $USER -s borg-passphrase |
| | | -w $(head -c 1024 /dev/urandom | base64) |
| | | </div> |
| | | </React.Fragment>; |
| | | } else if (value === 'gnome-keyring') { |
| | | passwordCommand = 'secret-tool lookup borg-repository repo-name'; |
| | | passwordCreate = <React.Fragment> |
| | | First ensure libsecret-tools, gnome-keyring and libpam-gnome-keyring are installed. If |
| | | libpam-gnome-keyring wasn’t already installed, ensure it runs on login:<br/> |
| | | <div className="command-line">sudo sh -c "echo session optional pam_gnome_keyring.so auto_start >> |
| | | /etc/pam.d/login"<br/> |
| | | sudo sh -c "echo password optional pam_gnome_keyring.so >> /etc/pam.d/passwd"<br/> |
| | | # you may need to relogin afterwards to activate the login keyring |
| | | </div> |
| | | Then add a secret to the login keyring: |
| | | <div className="command-line">head -c 1024 /dev/urandom | base64 | secret-tool store borg-repository |
| | | repo-name --label="Borg Passphrase"</div> |
| | | </React.Fragment>; |
| | | } else if (value === 'kwallet') { |
| | | passwordCommand = 'kwalletcli -e borg-passphrase -f Passwords'; |
| | | passwordCreate = <React.Fragment> |
| | | Ensure kwalletcli is installed, generate a passphrase, and store it in your “wallet”:<br/> |
| | | <div className="command-line">head -c 1024 /dev/urandom | base64 | kwalletcli -Pe borg-passphrase -f |
| | | Passwords |
| | | </div> |
| | | </React.Fragment>; |
| | | } |
| | | if (passwordCommand) { |
| | | this.setState({repoConfig: {...this.state.repoConfig, 'passwordCommand': passwordCommand}}) |
| | | } |
| | | this.setState({'passwordCreate': passwordCreate}); |
| | | } |
| | | } |
| | | |
| | | |
| | | handleRepoTextChange = event => { |
| | | event.preventDefault(); |
| | | this.setState({repoConfig: {...this.state.repoConfig, [event.target.name]: event.target.value}}); |
| | | } |
| | | |
| | | handleCheckboxChange = event => { |
| | | this.setState({[event.target.name]: event.target.value}); |
| | | /*if (event.target.name === 'mode' && event.target.value === 'existingRepo' |
| | | && this.state.passwordMethod !== 'passwordCommand' && this.state.passwordMethod !== 'passphrase') { |
| | | // Other options such as Mac OS key chain isn't available for existing repos: |
| | | this.setState({passwordMethod: 'passwordCommand'}); |
| | | }*/ |
| | | } |
| | | |
| | | render() { |
| | | const repoConfig = this.state.repoConfig; |
| | | var passwordMethods = [['password-command', 'Password command'], |
| | | ['passwordFile', 'Password file'], |
| | | ['macos-keychain', 'Mac OS X keychain'], |
| | | ['gnome-keyring', 'GNOME keyring'], |
| | | ['kwallet', 'KWallet'], |
| | | ['passphrase', 'Passphrase (not recommended)'] |
| | | ]; |
| | | let repoPlaceHolder = 'Enter the repo used by Borg.'; |
| | | if (this.state.mode === 'initNewRepo' && this.state.localRemote === 'remote') { |
| | | repoPlaceHolder = 'Enter the remote path of the repo, such as user@hostname:backup.'; |
| | | } |
| | | let repoFieldLength = 10; |
| | | let browseButton = null; |
| | | if (this.state.localRemote === 'local') { |
| | | repoFieldLength = 9; |
| | | browseButton = <FormButton onClick={null} |
| | | hint={'Browse local backup directory.'}>Browse</FormButton> |
| | | repoPlaceHolder = 'Enter or browse the local path of the repo home dir used by Borg.'; |
| | | } |
| | | return <React.Fragment> |
| | | <PageHeader> |
| | | Configure repository |
| | | </PageHeader> |
| | | <form> |
| | | <FormGroup> |
| | | <FormLabel length={2}>{'Mode'}</FormLabel> |
| | | <FormField length={10}> |
| | | <FormRadioButton name={'mode'} id={'mode1'} label={'Add existing repository'} |
| | | value={'existingRepo'} |
| | | checked={this.state.mode === 'existingRepo'} |
| | | onChange={this.handleCheckboxChange} |
| | | hint={'Do you want to add an already existing Borg repository?'}/> |
| | | <FormRadioButton name={'mode'} id={'mode2'} label={'Init new repository'} value={'initNewRepo'} |
| | | checked={this.state.mode === 'initNewRepo'} |
| | | onChange={this.handleCheckboxChange} |
| | | hint={'Do you want to create and init a new one?'}/> |
| | | </FormField> |
| | | </FormGroup> |
| | | <FormGroup> |
| | | <FormLabel length={2}>{'Local or remote'}</FormLabel> |
| | | <FormField length={10}> |
| | | <FormRadioButton name={'localRemote'} id={'localRemote1'} label={'Local repository'} |
| | | value={'local'} |
| | | checked={this.state.localRemote === 'local'} |
| | | onChange={this.handleCheckboxChange}/> |
| | | <FormRadioButton name={'localRemote'} id={'localRemote2'} label={'Remote repository'} |
| | | value={'remote'} |
| | | checked={this.state.localRemote === 'remote'} |
| | | onChange={this.handleCheckboxChange}/> |
| | | </FormField> |
| | | </FormGroup> |
| | | <FormLabelInputField label={'Display name'} fieldLength={12} |
| | | name={'displayName'} value={repoConfig.displayName} |
| | | onChange={this.handleRepoTextChange} |
| | | placeholder="Enter display name (only for displaying purposes)."/> |
| | | <FormGroup> |
| | | <FormLabel length={2}>{'Repo'}</FormLabel> |
| | | <FormField length={repoFieldLength}> |
| | | <FormInput |
| | | id={'repo'} |
| | | name={'repo'} |
| | | type={'text'} |
| | | value={repoConfig.repo} |
| | | onChange={this.handleRepoTextChange} |
| | | placeholder={repoPlaceHolder} |
| | | /> |
| | | </FormField> |
| | | {browseButton} |
| | | </FormGroup> |
| | | <FormLabelInputField label={'RSH'} fieldLength={12} |
| | | name={'rsh'} value={repoConfig.rsh} |
| | | onChange={this.handleRepoTextChange} |
| | | placeholder="Enter the rsh value (ssh command) for remote repository." |
| | | className={this.state.localRemote === 'local' ? 'hidden' : null}/> |
| | | <FormGroup className={this.state.mode === 'existingRepo' ? 'hidden' : null}> |
| | | <FormLabel length={2}>{'Encryption'}</FormLabel> |
| | | <FormField length={2}> |
| | | <FormSelect |
| | | value={repoConfig.encryption} |
| | | name={'encryption'} |
| | | onChange={this.handleRepoTextChange} |
| | | hint={'Encryption for the new repository (use repokey if you don\'t know what to choose).'} |
| | | > |
| | | <FormOption label={'repokey (SHA256)'} value={'repokey'}/> |
| | | <FormOption label={'repokey-blake2'} value={'repokey-blake2'}/> |
| | | <FormOption label={'keyfile (SHA256)'} value={'keyfile'}/> |
| | | <FormOption label={'none (not recommended)'} value={'none'}/> |
| | | </FormSelect> |
| | | </FormField> |
| | | </FormGroup> |
| | | <FormGroup> |
| | | <FormLabel length={2}>{'Password method'}</FormLabel> |
| | | <FormField length={3}> |
| | | <FormSelect |
| | | value={this.state.passwordMethod} |
| | | name={'passwordMethod'} |
| | | onChange={this.handleTextChange} |
| | | > |
| | | {passwordMethods |
| | | .map((entry) => <FormOption label={entry[1]} value={entry[0]} |
| | | key={entry[0]}/>)} |
| | | </FormSelect> |
| | | </FormField> |
| | | </FormGroup> |
| | | <FormGroup className={!this.state.passwordCreate ? 'hidden' : null}> |
| | | <FormLabel length={2}>{'Passphrase creation info'}</FormLabel> |
| | | <FormField length={10}> |
| | | {this.state.passwordCreate} |
| | | </FormField> |
| | | </FormGroup> |
| | | <FormLabelInputField label={'Password command'} fieldLength={12} |
| | | name={'passwordCommand'} value={repoConfig.passwordCommand} |
| | | onChange={this.handleRepoTextChange} |
| | | placeholder="Enter the password command to get the command from or choose a method above." |
| | | className={this.state.passwordMethod === 'passphrase' ? 'hidden' : null} |
| | | /> |
| | | <FormLabelInputField label={'Password'} fieldLength={6} type={'password'} |
| | | name={'passphrase'} value={repoConfig.passphrase} |
| | | onChange={this.handleRepoTextChange} |
| | | hint={"It's recommended to use password command instead."} |
| | | className={this.state.passwordMethod !== 'passphrase' ? 'hidden' : null} |
| | | /> |
| | | <FormField length={12}> |
| | | <Link to={'/repos'} className={'btn btn-outline-primary'}><I18n name={'common.cancel'}/> |
| | | </Link> |
| | | <FormButton onClick={this.onSave} bsStyle="primary" |
| | | hintKey="configuration.save.hint"><I18n name={'common.save'}/> |
| | | </FormButton> |
| | | </FormField> |
| | | </form> |
| | | <code> |
| | | <h2>Todo:</h2> |
| | | <ul> |
| | | <li>Implement 'Save' button ;-)</li> |
| | | <li>Add own environment variables.</li> |
| | | <li>Implement browse button for local repos.</li> |
| | | <li>Note (for new backups): Save your password, otherwise your backup will be lost without!</li> |
| | | <li>Note (hide password fields): Your backup will not be encrypted!</li> |
| | | <li>Remove and clone repo.</li> |
| | | </ul> |
| | | </code> |
| | | </React.Fragment>; |
| | | } |
| | | } |
| | | |
| | | export default ConfigureRepoPage; |
| | | |
| | |
| | | import React from 'react' |
| | | import {Nav, NavLink, TabContent, Table, TabPane} from 'reactstrap'; |
| | | import {Badge, Nav, NavLink, TabContent, Table, TabPane} from 'reactstrap'; |
| | | import { Link } from "react-router-dom"; |
| | | import classNames from 'classnames'; |
| | | import {PageHeader} from '../../general/BootstrapComponents'; |
| | |
| | | import ErrorAlert from '../../general/ErrorAlert'; |
| | | import {IconCheck, IconRefresh} from '../../general/IconComponents'; |
| | | import JobMonitorPanel from '../jobs/JobMonitorPanel'; |
| | | import RepoConfigPanel from "./RepoConfigPanel"; |
| | | |
| | | class RepoArchiveListView extends React.Component { |
| | | |
| | | state = { |
| | | id: this.props.match.params.id, |
| | | displayName: this.props.match.params.displayName, |
| | | isFetching: false, |
| | | activeTab: '1', |
| | | redirectOnError: true |
| | | }; |
| | | |
| | | componentDidMount = () => { |
| | |
| | | repo: json |
| | | }) |
| | | }) |
| | | .catch(() => this.setState({isFetching: false, failed: true})); |
| | | .catch(() => { |
| | | this.setState({isFetching: false, failed: true}) |
| | | if (this.state.redirectOnError && this.state.activeTab !== '3') { |
| | | this.setState({activeTab: '3', redirectOnError: false}); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | toggleTab = tab => () => { |
| | |
| | | }) |
| | | }; |
| | | |
| | | afterSave() { |
| | | if (!this.state.failed) { |
| | | this.setState({ |
| | | activeTab: '1' |
| | | }) |
| | | } |
| | | } |
| | | |
| | | afterCancel() { |
| | | if (!this.state.failed) { |
| | | this.setState({ |
| | | activeTab: '1' |
| | | }) |
| | | } |
| | | } |
| | | |
| | | render = () => { |
| | | let content = undefined; |
| | | let errorBadge = ''; |
| | | let content1 = undefined; |
| | | let content2 = undefined; |
| | | const repo = this.state.repo; |
| | | let pageHeader = ''; |
| | | const displayName = (this.state.displayName) ? this.state.displayName : `Error: id=${this.state.id}`; |
| | | let pageHeader = <React.Fragment> |
| | | {displayName} |
| | | </React.Fragment>; |
| | | |
| | | if (this.state.isFetching) { |
| | | content = <JobMonitorPanel repo={this.state.id} />; |
| | | content1 = <JobMonitorPanel repo={this.state.id}/>; |
| | | content2 = content1; |
| | | } else if (this.state.failed) { |
| | | content = <ErrorAlert |
| | | content1 = <ErrorAlert |
| | | title={'Cannot load Repositories'} |
| | | description={'Something went wrong during contacting the rest api.'} |
| | | description={'Something went wrong, may-be wrong configuration?'} |
| | | action={{ |
| | | handleClick: this.fetchRepo, |
| | | title: 'Try again' |
| | | }} |
| | | />; |
| | | content2 = content1; |
| | | errorBadge = <Badge color="danger" pill>!</Badge>; |
| | | } else if (this.state.repo) { |
| | | pageHeader = <React.Fragment> |
| | | {repo.displayName} |
| | |
| | | <td>{repo.cache.path}</td> |
| | | </tr> |
| | | } |
| | | content = <React.Fragment> |
| | | <Nav tabs> |
| | | <NavLink |
| | | className={classNames({active: this.state.activeTab === '1'})} |
| | | onClick={this.toggleTab('1')} |
| | | > |
| | | Archives |
| | | </NavLink> |
| | | <NavLink |
| | | className={classNames({active: this.state.activeTab === '2'})} |
| | | onClick={this.toggleTab('2')} |
| | | > |
| | | Information |
| | | </NavLink> |
| | | </Nav> |
| | | <TabContent activeTab={this.state.activeTab}> |
| | | <TabPane tabId={'1'}> |
| | | <Table hover> |
| | | |
| | | content1 = <Table hover> |
| | | <tbody> |
| | | <tr> |
| | | <th>Archive</th> |
| | |
| | | </tr>); |
| | | })} |
| | | </tbody> |
| | | </Table> |
| | | </TabPane> |
| | | <TabPane tabId={'2'}> |
| | | <Table striped bordered hover> |
| | | </Table>; |
| | | content2 = <Table striped bordered hover> |
| | | <tbody> |
| | | <tr> |
| | | <td>Id</td> |
| | |
| | | {encryption} |
| | | {cachePath} |
| | | </tbody> |
| | | </Table> |
| | | </TabPane> |
| | | </TabContent> |
| | | </React.Fragment>; |
| | | </Table>; |
| | | |
| | | } |
| | | return <React.Fragment> |
| | | <PageHeader> |
| | | {pageHeader} |
| | | <Link to={'/repos'}> Repositories</Link> - {pageHeader} |
| | | </PageHeader> |
| | | {content} |
| | | <Nav tabs> |
| | | <NavLink |
| | | className={classNames({active: this.state.activeTab === '1'})} |
| | | onClick={this.toggleTab('1')} |
| | | > |
| | | Archives {errorBadge} |
| | | </NavLink> |
| | | <NavLink |
| | | className={classNames({active: this.state.activeTab === '2'})} |
| | | onClick={this.toggleTab('2')} |
| | | > |
| | | Information {errorBadge} |
| | | </NavLink> |
| | | <NavLink |
| | | className={classNames({active: this.state.activeTab === '3'})} |
| | | onClick={this.toggleTab('3')} |
| | | > |
| | | Configuration |
| | | </NavLink> |
| | | </Nav> |
| | | <TabContent activeTab={this.state.activeTab}> |
| | | <TabPane tabId={'1'}> |
| | | {content1} |
| | | </TabPane> |
| | | <TabPane tabId={'2'}> |
| | | {content2} |
| | | </TabPane> |
| | | <TabPane tabId={'3'}> |
| | | <RepoConfigPanel id={this.state.id} afterCancel={this.afterCancel} afterSave={this.afterSave} |
| | | repoError={this.state.failed}/> |
| | | </TabPane> |
| | | </TabContent> |
| | | </React.Fragment>; |
| | | }; |
| | | |
| | |
| | | |
| | | this.fetchRepo = this.fetchRepo.bind(this); |
| | | this.toggleTab = this.toggleTab.bind(this); |
| | | this.afterCancel = this.afterCancel.bind(this); |
| | | this.afterSave = this.afterSave.bind(this); |
| | | } |
| | | } |
| | | |
| | |
| | | let repoText = this.buildItem(null, content); |
| | | |
| | | return <React.Fragment> |
| | | <Card tag={Link} to={`/repoArchives/${repo.id}`} outline color="success" className={'repo'} |
| | | <Card tag={Link} to={`/repoArchives/${repo.id}/${repo.displayName}`} outline color="success" className={'repo'} |
| | | style={{backgroundColor: '#fff'}}> |
| | | <CardHeader>{repo.displayName}</CardHeader> |
| | | <CardBody> |
| New file |
| | |
| | | import React from 'react'; |
| | | import {FormGroup} from 'reactstrap'; |
| | | import {FormButton, FormField, FormLabelInputField} from '../../general/forms/FormComponents'; |
| | | import {getRestServiceUrl} from '../../../utilities/global'; |
| | | import I18n from "../../general/translation/I18n"; |
| | | import LoadingOverlay from '../../general/loading/LoadingOverlay'; |
| | | import PropTypes from "prop-types"; |
| | | import ErrorAlert from "../../general/ErrorAlert"; |
| | | |
| | | class RepoConfigPanel extends React.Component { |
| | | |
| | | constructor(props) { |
| | | super(props); |
| | | this.state = { |
| | | loading: false, |
| | | repoConfig: undefined |
| | | }; |
| | | |
| | | this.fetch = this.fetch.bind(this); |
| | | this.handleTextChange = this.handleTextChange.bind(this); |
| | | this.onSave = this.onSave.bind(this); |
| | | this.onCancel = this.onCancel.bind(this); |
| | | } |
| | | |
| | | componentDidMount = () => this.fetch(); |
| | | |
| | | fetch = () => { |
| | | this.setState({ |
| | | isFetching: true, |
| | | failed: false, |
| | | repoConfig: undefined |
| | | }); |
| | | fetch(getRestServiceUrl('repoConfig', { |
| | | id: this.props.id |
| | | }), { |
| | | method: 'GET', |
| | | headers: { |
| | | 'Accept': 'application/json' |
| | | } |
| | | }) |
| | | .then(response => response.json()) |
| | | .then(json => { |
| | | this.setState({ |
| | | isFetching: false, |
| | | repoConfig: json |
| | | }) |
| | | }) |
| | | .catch((error) => { |
| | | console.log("error", error); |
| | | this.setState({ |
| | | isFetching: false, |
| | | failed: true |
| | | }); |
| | | }) |
| | | }; |
| | | |
| | | handleTextChange = event => { |
| | | event.preventDefault(); |
| | | this.setState({repoConfig: {...this.state.repoConfig, [event.target.name]: event.target.value}}); |
| | | } |
| | | |
| | | async onSave(event) { |
| | | const response = fetch(getRestServiceUrl("repoConfig"), { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json' |
| | | }, |
| | | body: JSON.stringify(this.state.repoConfig) |
| | | }); |
| | | if (response) await response; |
| | | if (this.props.afterSave) { |
| | | this.props.afterSave(); |
| | | } |
| | | } |
| | | |
| | | async onCancel() { |
| | | const response = this.fetch(); |
| | | if (response) await response; |
| | | if (this.props.afterCancel) { |
| | | this.props.afterCancel(); |
| | | } |
| | | } |
| | | |
| | | render() { |
| | | let content; |
| | | let repoError = ''; |
| | | if (this.props.repoError) { |
| | | repoError =<ErrorAlert |
| | | title={'Cannot access repository'} |
| | | description={'Repo not available or mis-configured (please refer the log files for more details).'} |
| | | /> |
| | | } |
| | | if (this.state.isFetching) { |
| | | content = <React.Fragment>Loading...</React.Fragment>; |
| | | } else if (this.state.failed) { |
| | | content = <ErrorAlert |
| | | title={'Cannot load config of repository'} |
| | | description={'Something went wrong during contacting the rest api.'} |
| | | action={{ |
| | | handleClick: this.fetchRepo, |
| | | title: 'Try again' |
| | | }} |
| | | />; |
| | | } else if (this.state.repoConfig) { |
| | | const repoConfig = this.state.repoConfig; |
| | | content = <React.Fragment> |
| | | <FormGroup> |
| | | <FormLabelInputField label={'Display name'} fieldLength={12} |
| | | name={'displayName'} value={repoConfig.displayName} |
| | | onChange={this.handleTextChange} |
| | | placeholder="Enter display name (only for displaying purposes)."/> |
| | | <FormLabelInputField label={'Repo'} fieldLength={12} |
| | | name={'repo'} value={repoConfig.repo} |
| | | onChange={this.handleTextChange} |
| | | placeholder="Enter the name of the repo, used by Borg."/> |
| | | <FormLabelInputField label={'RSH'} fieldLength={12} |
| | | name={'rsh'} value={repoConfig.rsh} |
| | | onChange={this.handleTextChange} |
| | | placeholder="Enter the rsh value (ssh command) for remote repository."/> |
| | | <FormLabelInputField label={'Password command'} fieldLength={12} |
| | | name={'passwordCommand'} value={repoConfig.passwordCommand} |
| | | onChange={this.handleTextChange} |
| | | placeholder="Enter the password command to get the command from."/> |
| | | <FormLabelInputField label={'Password'} fieldLength={6} type={'password'} |
| | | name={'passphrase'} value={repoConfig.passphrase} |
| | | onChange={this.handleTextChange} |
| | | hint={"It's recommended to use password command instead."} |
| | | /> |
| | | <FormField length={12}> |
| | | <FormButton onClick={this.onCancel} |
| | | hintKey="configuration.cancel.hint"><I18n name={'common.cancel'}/> |
| | | </FormButton> |
| | | <FormButton onClick={this.onSave} bsStyle="primary" |
| | | hintKey="configuration.save.hint"><I18n name={'common.save'}/> |
| | | </FormButton> |
| | | </FormField> |
| | | </FormGroup> |
| | | <LoadingOverlay active={this.state.loading}/> |
| | | </React.Fragment>; |
| | | } |
| | | return <React.Fragment>{content}{repoError}</React.Fragment>; |
| | | } |
| | | } |
| | | |
| | | RepoConfigPanel.propTypes = { |
| | | afterCancel: PropTypes.func.isRequired, |
| | | afterSave: PropTypes.func.isRequired, |
| | | id: PropTypes.string, |
| | | repoError: PropTypes.bool |
| | | }; |
| | | |
| | | export default RepoConfigPanel; |
| | | |
| | |
| | | import React from 'react' |
| | | import {Link} from 'react-router-dom' |
| | | import './RepoListView.css'; |
| | | import {CardDeck} from 'reactstrap'; |
| | | import {PageHeader} from '../../general/BootstrapComponents'; |
| | | import {getRestServiceUrl} from '../../../utilities/global'; |
| | | import ErrorAlert from '../../general/ErrorAlert'; |
| | | import RepoCard from './RepoCard'; |
| | | import {IconRefresh} from "../../general/IconComponents"; |
| | | import {IconAdd, IconRefresh} from "../../general/IconComponents"; |
| | | |
| | | class RepoListView extends React.Component { |
| | | |
| | |
| | | </div> |
| | | </PageHeader> |
| | | {content} |
| | | <br/> |
| | | <Link to={'/repo/configure'} |
| | | className={'btn btn-outline-primary'} |
| | | > |
| | | <IconAdd/> |
| | | </Link> |
| | | </React.Fragment>; |
| | | }; |
| | | |
| | |
| | | import Footer from '../components/views/footer/Footer'; |
| | | import {loadVersion} from '../actions'; |
| | | import {getTranslation} from '../utilities/i18n'; |
| | | import ConfigureRepoPage from '../components/views/repos/ConfigureRepoPage'; |
| | | |
| | | class WebApp extends React.Component { |
| | | |
| | |
| | | |
| | | componentDidMount = () => { |
| | | this.props.loadVersion(); |
| | | this.interval = setInterval(() => this.fetchJobStatistics(), 5000); |
| | | this.interval = setInterval(() => this.fetchSystemInfo(), 5000); |
| | | }; |
| | | |
| | | fetchJobStatistics = () => { |
| | | fetch(getRestServiceUrl('jobs/statistics'), { |
| | | fetchSystemInfo = () => { |
| | | fetch(getRestServiceUrl('system/info'), { |
| | | method: 'GET', |
| | | headers: { |
| | | 'Accept': 'application/json' |
| | |
| | | .then(response => response.json()) |
| | | .then(json => { |
| | | this.setState({ |
| | | statistics: json |
| | | systemInfo: json |
| | | }); |
| | | }) |
| | | .catch(); |
| | |
| | | |
| | | render() { |
| | | let jobsBadge = ''; |
| | | if (this.state && this.state.statistics && this.state.statistics.numberOfRunningAndQueuedJobs > 0) { |
| | | jobsBadge = <Badge color="danger" pill>{this.state.statistics.numberOfRunningAndQueuedJobs}</Badge>; |
| | | const statistics = (this.state && this.state.systemInfo) ? this.state.systemInfo.queueStatistics : null; |
| | | if (statistics && statistics.numberOfRunningAndQueuedJobs > 0) { |
| | | jobsBadge = <Badge color="danger" pill>{statistics.numberOfRunningAndQueuedJobs}</Badge>; |
| | | } |
| | | let configurationBadge = ''; |
| | | if (this.state && this.state.systemInfo && !this.state.systemInfo.configurationOK) { |
| | | configurationBadge = <Badge color="danger" pill>!</Badge>; |
| | | } |
| | | let routes = [ |
| | | ['Start', '/', Start], |
| | | ['Repositories', '/repos', RepoListView], |
| | | ['Job monitor', '/jobmonitor', JobMonitorView, {badge: jobsBadge}], |
| | | [getTranslation('logviewer'), '/logging', LogPage], |
| | | [getTranslation('configuration'), '/config', ConfigurationPage] |
| | | [getTranslation('configuration'), '/config', ConfigurationPage, {badge: configurationBadge}] |
| | | ]; |
| | | |
| | | if (isDevelopmentMode()) { |
| | |
| | | /> |
| | | )) |
| | | } |
| | | <Route path={'/repoArchives/:id'} component={RepoArchiveListView}/> |
| | | <Route path={'/repoArchives/:id/:displayName'} component={RepoArchiveListView} /> |
| | | <Route path={'/archives/:repoId/:archiveId/'} component={ArchiveView} /> |
| | | <Route path={'/repo/configure'} component={ConfigureRepoPage} /> |
| | | </Switch> |
| | | </div> |
| | | <Footer versionInfo={this.props.version}/> |
| | |
| | | color: #50555f; |
| | | } |
| | | |
| | | .hidden { |
| | | display: none; |
| | | } |
| | | |
| | | .command-line { |
| | | font-family: monospace; |
| | | white-space: pre; |
| | | border-style: solid; |
| | | background-color: black; |
| | | color: white; |
| | | padding-left: 5pt; |
| | | } |
| | | |
| | | a:hover { |
| | | color: #47a7eb; |
| | | } |
| | |
| | | color: #47a7eb; |
| | | } |
| | | |
| | | ul.nav-tabs a.active { |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .tab-pane.active { |
| | | padding: 20px; |
| | | } |