ds-server.2.1.2.source-code.SDS Maven / Gradle / Ivy
Show all versions of sds-server Show documentation
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.zip.CRC32;
/**
* Synchronizes a local directory against a SDS server.
*
* This client can be started as command line tool and will synchronize a local directory against an
* artifact stored in a SDS server.
*
* To make this as lightweight as possible, no external libraries are referenced. Therefore only this
* class file is required and can be launched using java SDS ....
*/
public class SDS {
//------------------------------------------------------------------------
// Command Line Handling
//------------------------------------------------------------------------
public static void main(String[] args) {
System.out.println("SDS - Software Distribution System");
System.out.println("-----------------------------------------------");
SDS instance = parseCommandLineAndCreateInstance(args);
instance.verifyParameters();
instance.execute();
}
private static SDS parseCommandLineAndCreateInstance(String[] args) {
SDS result = new SDS();
result.applyParameter("server", System.getenv("SDS_SERVER"));
result.applyParameter("identity", System.getenv("SDS_IDENTITY"));
result.applyParameter("key", System.getenv("SDS_KEY"));
List argsList = Arrays.asList(args);
Iterator iter = argsList.iterator();
while (iter.hasNext()) {
String current = iter.next();
if (current.startsWith("-")) {
String key = current.substring(1);
if (!result.applySwitch(key)) {
if (!iter.hasNext()) {
fail("Missing value for parameter '%s'", key);
}
String value = iter.next();
result.applyParameter(key, value);
}
} else {
result.applyParameter("command", current);
break;
}
}
if (iter.hasNext()) {
result.applyParameter("artifact", iter.next());
}
if (iter.hasNext()) {
result.applyParameter("version", iter.next());
}
return result;
}
private boolean applySwitch(String key) {
try {
Field field = getClass().getDeclaredField(key);
if (!boolean.class.equals(field.getType())) {
return false;
}
field.set(this, true);
return true;
} catch (Exception e) {
verbose(e);
return false;
}
}
private void applyParameter(String key, String value) {
try {
Field field = getClass().getDeclaredField(key);
field.set(this, value);
} catch (Exception e) {
verbose(e);
fail("Unknown parameter: %s", key);
}
}
private void verifyParameters() {
System.out.println();
if (empty(server)) {
fail("Please provide a server.");
}
if (empty(command)) {
fail("Please specify which command to execute.");
}
System.out.printf(" Server: %s%n", server);
System.out.printf(" Identity: %s%n", identity);
System.out.printf(" Key present: %s%n", !empty(key));
System.out.println();
}
//------------------------------------------------------------------------
// Helper methods
//------------------------------------------------------------------------
private boolean empty(String value) {
return value == null || "".equals(value);
}
private String urlEncode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("UTF-8", e);
}
}
private String hashMD5(String value) {
try {
byte[] bytesOfMessage = value.getBytes("UTF-8");
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(bytesOfMessage);
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
} catch (Throwable e) {
verbose(e);
fail("Cannot compute MD5 hashes...: %s", e.getMessage());
return null;
}
}
private long crc(File file) {
try {
CRC32 crc = new CRC32();
try (FileInputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int read = in.read(buffer);
while (read > 0) {
crc.update(buffer, 0, read);
read = in.read(buffer);
}
return crc.getValue();
}
} catch (IOException e) {
fail("Failed to compute the CRC of %s", file.getAbsolutePath());
return 0;
}
}
private File getExpectedFile(String path) {
File file = new File(".");
for (String pathElement : path.split("/")) {
file = new File(file, pathElement);
}
return file;
}
//------------------------------------------------------------------------
// Instance variables
//------------------------------------------------------------------------
private String server;
private String identity;
private String key;
private String command;
private String artifact;
private String version;
private boolean debug;
private String timestamp =
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()).replaceAll("[^0-9]", "_");
//------------------------------------------------------------------------
// Logging and error handling
//------------------------------------------------------------------------
private void verbose(Object e) {
if (debug) {
if (e instanceof Throwable) {
((Throwable) e).printStackTrace();
} else {
System.err.println(e);
}
}
}
private static void fail(String msg, Object... params) {
System.err.println();
System.err.println();
if (params.length > 0) {
System.err.println(String.format(msg, params));
} else {
System.err.println(msg);
}
System.err.println();
System.err.println();
System.err.println("Usage: sds -server -identity -key COMMAND [ARTIFACT] [VERSION]");
System.err.println();
System.err.println("You can omit the parameters if the appropriate environment variables "
+ "(SDS_SERVER, SDS_IDENTITY, SDS_KEY) are filled.");
System.err.println();
System.err.println("Commands");
System.err.println("--------");
System.err.println();
System.err.println("list - Lists all versions of the given artifact, "
+ "or all known artifacts if no artifact name is given");
System.err.println("pull - Synchronizes the given artifact against the current directory.");
System.err.println(" WARNING: This will delete all files in the current directory "
+ "if they are not part of the artifact distribution.");
System.err.println("verify - Verifies the local directory against the given artifact on the server. "
+ "Show all changes that would be performed.");
System.err.println("monkey - Synchronizes the given artifact against the current directory, "
+ "but ask if a change should be performed or not.");
System.err.println();
System.exit(-1);
}
//------------------------------------------------------------------------
// Server communication
//------------------------------------------------------------------------
private void download(String uri, OutputStream target, boolean showProgress) {
try {
URL url = makeURL(uri);
verbose(url);
byte[] buffer = new byte[8192];
long bytesSoFar = 0;
long lastBytesReported = 0;
long lastTimeReported = System.currentTimeMillis();
try (InputStream in = url.openConnection().getInputStream()) {
int read = in.read(buffer);
while (read > 0) {
target.write(buffer, 0, read);
if (showProgress) {
bytesSoFar += read;
long now = System.currentTimeMillis();
if (now - lastTimeReported > 5000) {
long timeDiff = now - lastTimeReported;
long bytesDiff = bytesSoFar - lastBytesReported;
System.out.println(String.format("Downloaded %s kB (%s kB/s)",
bytesSoFar / 1024,
(bytesDiff / 1024) / (timeDiff / 1000)));
lastTimeReported = now;
lastBytesReported = bytesSoFar;
}
}
read = in.read(buffer);
}
if (showProgress) {
long timeDiff = (System.currentTimeMillis() - lastTimeReported) / 1000;
if (timeDiff == 0) {
timeDiff = 1;
}
long bytesDiff = bytesSoFar - lastBytesReported;
System.out.println(String.format("Downloaded %s kB (%s kB/s)",
bytesSoFar / 1024,
(bytesDiff / 1024) / timeDiff));
}
}
} catch (IOException e) {
fail("An IO error occurred while calling '%s': %s", uri, e.getMessage());
}
}
private URL makeURL(String uri) {
try {
if (!server.startsWith("http")) {
server = "http://" + server;
}
if (empty(identity)) {
return new URL(server + uri);
}
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String input = identity + timestamp + key;
String hash = hashMD5(input);
if (uri.contains("?")) {
return new URL(server
+ uri
+ "&user="
+ urlEncode(identity)
+ "×tamp="
+ urlEncode(timestamp)
+ "&hash="
+ urlEncode(hash));
} else {
return new URL(server
+ uri
+ "?user="
+ urlEncode(identity)
+ "×tamp="
+ urlEncode(timestamp)
+ "&hash="
+ urlEncode(hash));
}
} catch (MalformedURLException e) {
verbose(e);
fail("Cannot create a valid url. Please specify the server without a leading '/': %s", e.getMessage());
return null;
}
}
private void downloadAndVerify(String baseURI, File file, Object expectedFile) {
int retries = 3;
while (retries-- > 0) {
try {
doDownloadFile(baseURI, expectedFile, file);
break;
} catch (Throwable e) {
verbose(e);
if (retries <= 0) {
fail(e.getMessage());
}
}
}
}
private void doDownloadFile(String baseURI, Object expectedFile, File target) throws IOException {
File buffer = File.createTempFile("sds-", ".sds");
try {
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(buffer))) {
download(baseURI + "/" + get(expectedFile, "name"), out, true);
}
if (buffer.length() != (long) get(expectedFile, "size")) {
throw new IllegalStateException("Length of downloaded file '"
+ get(expectedFile, "name")
+ "' does not match!");
}
if (crc(buffer) != (long) get(expectedFile, "crc")) {
throw new IllegalStateException("CRC of downloaded file '"
+ get(expectedFile, "name")
+ "' does not match!");
}
Files.move(buffer.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
} finally {
buffer.delete();
}
}
private Object jsonCall(String uri) {
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
download(uri, buffer, debug);
return parseJSON(new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buffer.toByteArray()),
"UTF-8")));
} catch (Throwable e) {
verbose(e);
fail("Cannot download '%s' as JSON", uri);
return null;
}
}
//------------------------------------------------------------------------
// Built-in JSON parser...
//------------------------------------------------------------------------
private Object parseJSON(Reader input) throws IOException {
char next = readFirstNonWhiteSpace(input);
if (next == '{') {
return parseMap(input);
} else if (next == '"') {
return parseString(input);
} else if (next == '[') {
return parseArray(input);
} else if (Character.isLetterOrDigit(next)) {
StringBuilder sb = new StringBuilder();
while (Character.isLetterOrDigit(next) || next == '-') {
sb.append(next);
input.mark(1);
next = (char) input.read();
}
input.reset();
String str = sb.toString();
if ("true".equals(str)) {
return true;
} else if ("false".equals(str)) {
return false;
} else if ("null".equals(str)) {
return null;
} else if (str.contains("-")) {
return LocalDate.parse(str);
} else {
return Long.valueOf(str);
}
} else {
throw new IllegalArgumentException("Unexpected JSON character: " + (char) next);
}
}
private Object parseArray(Reader input) throws IOException {
List