Martin Kunz 5 years ago
commit
4fc004a8e5

+ 189 - 0
.gitignore

@@ -0,0 +1,189 @@
+
+# Created by https://www.gitignore.io/api/osx,java,maven,linux,windows,intellij
+# Edit at https://www.gitignore.io/?templates=osx,java,maven,linux,windows,intellij
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+### OSX ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.gitignore.io/api/osx,java,maven,linux,windows,intellij

+ 13 - 0
.idea/compiler.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <annotationProcessing>
+      <profile name="Maven default annotation processors profile" enabled="true">
+        <sourceOutputDir name="target/generated-sources/annotations" />
+        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
+        <outputRelativeToContentRoot value="true" />
+        <module name="transceivr" />
+      </profile>
+    </annotationProcessing>
+  </component>
+</project>

+ 4 - 0
.idea/encodings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" addBOMForNewFiles="with NO BOM" />
+</project>

+ 17 - 0
.idea/misc.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="JavaScriptSettings">
+    <option name="languageLevel" value="ES6" />
+  </component>
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="12" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>

+ 101 - 0
pom.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>at.acdp</groupId>
+    <artifactId>transceivr</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <name>transceivr</name>
+    <packaging>jar</packaging>
+    <properties>
+        <undertow.version>2.0.19.Final</undertow.version>
+    </properties>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.github.spotbugs</groupId>
+                <artifactId>spotbugs-maven-plugin</artifactId>
+                <version>3.1.8</version>
+                <dependencies>
+                    <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
+                    <dependency>
+                        <groupId>com.github.spotbugs</groupId>
+                        <artifactId>spotbugs</artifactId>
+                        <version>3.1.8</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.0</version>
+                <configuration>
+                    <source>11</source>
+                    <target>11</target>
+                    <excludes>
+                        <exclude>module-info.java</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.2.1</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <minimizeJar>true</minimizeJar>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <addClasspath>true</addClasspath>
+                            <mainClass>at.acdp.urweb.Main</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>com.eclipsesource.minimal-json</groupId>
+            <artifactId>minimal-json</artifactId>
+            <version>0.9.5</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>3.14.0</version>
+        </dependency>
+        <dependency>
+            <groupId>io.undertow</groupId>
+            <artifactId>undertow-core</artifactId>
+            <version>${undertow.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.25</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.2.3</version>
+        </dependency>
+    </dependencies>
+</project>

+ 60 - 0
src/main/java/com/acdp/transceivr/CountingInputStream.java

@@ -0,0 +1,60 @@
+package com.acdp.transceivr;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class CountingInputStream extends FilterInputStream {
+    private int count=0;
+
+    public CountingInputStream(InputStream is) {
+        super(is);
+    }
+
+    @Override
+    public int read() throws IOException {
+        int x=super.read();
+        if(x>0)
+            count++;
+        return x;
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        int bytes=super.read(b);
+        count+=bytes;
+        return bytes;
+    }
+
+    @Override
+    public byte[] readNBytes(int len) throws IOException {
+        byte[] ret= super.readNBytes(len);
+        count+=ret.length;
+        return ret;
+    }
+
+    @Override
+    public byte[] readAllBytes() throws IOException {
+        byte[] ret= super.readAllBytes();
+        count+=ret.length;
+        return ret;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        int c= super.read(b, off, len);
+        count+=c;
+        return c;
+    }
+
+    @Override
+    public int readNBytes(byte[] b, int off, int len) throws IOException {
+        int c= super.readNBytes(b, off, len);
+        count+=c;
+        return c;
+    }
+
+    public int getCount() {
+        return count;
+    }
+}

+ 111 - 0
src/main/java/com/acdp/transceivr/LoadTools.java

@@ -0,0 +1,111 @@
+package com.acdp.transceivr;
+
+import okhttp3.*;
+import okio.BufferedSink;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class LoadTools {
+    public static final MediaType MEDIA_TYPE_BINARY
+            = MediaType.parse("application/octet-stream");
+
+    private static void download(Transfer t, ProgressListener progressListener) throws IOException {
+        Request request = new Request.Builder()
+            .url(t.from)
+            .build();
+
+        OkHttpClient client = new OkHttpClient.Builder()
+            .addNetworkInterceptor(chain -> {
+                Response originalResponse = chain.proceed(chain.request());
+                return originalResponse.newBuilder()
+                        .body(new ProgressResponseBody(originalResponse.body(), progressListener))
+                        .build();
+            })
+            .build();
+
+        client.newCall(request).enqueue(new Callback() {
+            @Override
+            public void onFailure(Call call, IOException e) {
+                t.uploadError=e.toString();
+            }
+
+            @Override
+            public void onResponse(Call call, Response response) throws IOException {
+                if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+                try {
+                    upload(t, response.body().byteStream());
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+    }
+
+    private static void upload(Transfer t, InputStream is) throws IOException {
+        RequestBody requestBody = new RequestBody() {
+            @Override
+            public MediaType contentType() {
+                return MEDIA_TYPE_BINARY;
+            }
+
+            @Override
+            public void writeTo(BufferedSink sink) throws IOException {
+                is.transferTo(sink.outputStream());
+            }
+        };
+
+
+        OkHttpClient client = new OkHttpClient();
+        Request request = new Request.Builder()
+                .url(t.to)
+                .post(requestBody)
+                .build();
+
+        client.newCall(request).enqueue(
+            new Callback() {
+                @Override
+                public void onFailure(Call call, IOException e) {
+                    t.uploadError = e.toString();
+                }
+
+                @Override
+                public void onResponse(Call call, Response response) throws IOException {
+                    if (!response.isSuccessful()) {
+                        t.uploadError = response.toString();
+                        t.uploadCode = response.code();
+                    }
+                    t.uploadResponseBody = response.body().string();
+                    t.uploadDone = true;
+                }
+            }
+        );
+    }
+
+    public static void startTransfer(Transfer t) throws IOException {
+        final ProgressListener progressListener = new ProgressListener() {
+            boolean firstUpdate = true;
+
+            @Override
+            public void update(long bytesRead, long contentLength, boolean done) {
+                if (done) {
+                    t.downloadDone = true;
+                } else {
+                    if (firstUpdate) {
+                        firstUpdate = false;
+                        if (contentLength == -1) {
+                            t.contentLength = -1;
+                        } else {
+                            t.contentLength = contentLength;
+                        }
+                    }
+                    if (contentLength != -1) {
+                        t.bytesRead = bytesRead;
+                        t.currentTS = System.currentTimeMillis();
+                    }
+                }
+            }
+        };
+        download(t, progressListener);
+    }
+}

+ 8 - 0
src/main/java/com/acdp/transceivr/Main.java

@@ -0,0 +1,8 @@
+package com.acdp.transceivr;
+
+public class Main {
+
+    public static void main(String [ ] args) {
+        new WebServer(8082,true).start();
+    }
+}

+ 52 - 0
src/main/java/com/acdp/transceivr/ProgressResponseBody.java

@@ -0,0 +1,52 @@
+package com.acdp.transceivr;
+
+import okhttp3.MediaType;
+import okhttp3.ResponseBody;
+import okio.*;
+
+import java.io.IOException;
+
+public class ProgressResponseBody extends ResponseBody {
+
+    private final ResponseBody responseBody;
+    private final ProgressListener progressListener;
+    private BufferedSource bufferedSource;
+
+    ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
+        this.responseBody = responseBody;
+        this.progressListener = progressListener;
+    }
+
+    @Override public MediaType contentType() {
+        return responseBody.contentType();
+    }
+
+    @Override public long contentLength() {
+        return responseBody.contentLength();
+    }
+
+    @Override public BufferedSource source() {
+        if (bufferedSource == null) {
+            bufferedSource = Okio.buffer(source(responseBody.source()));
+        }
+        return bufferedSource;
+    }
+
+    private Source source(Source source) {
+        return new ForwardingSource(source) {
+            long totalBytesRead = 0L;
+
+            @Override public long read(Buffer sink, long byteCount) throws IOException {
+                long bytesRead = super.read(sink, byteCount);
+                // read() returns the number of bytes read, or -1 if this source is exhausted.
+                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
+                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
+                return bytesRead;
+            }
+        };
+    }
+}
+
+interface ProgressListener {
+    void update(long bytesRead, long contentLength, boolean done);
+}

+ 42 - 0
src/main/java/com/acdp/transceivr/Transfer.java

@@ -0,0 +1,42 @@
+package com.acdp.transceivr;
+
+import com.eclipsesource.json.JsonObject;
+
+public class Transfer {
+    public Transfer(int id) {
+        this.id = id;
+        startTS=System.currentTimeMillis();
+    }
+
+    public long bytesRead;
+    public long contentLength;
+    public String from;
+    public String to;
+    public String uploadError;
+    public int uploadCode;
+    public String uploadResponseBody;
+    public boolean downloadDone = false;
+    public boolean uploadDone = false;
+    public long startTS;
+    public long currentTS;
+
+    int id;
+
+    public JsonObject toJSON() {
+        JsonObject js = new JsonObject();
+        js.add("id", id);
+        js.add("bytesRead", bytesRead);
+        js.add("contentLength", contentLength);
+        js.add("from", from);
+        js.add("to", to);
+        js.add("uploadError", uploadError);
+        js.add("uploadCode", uploadCode);
+        js.add("uploadResponseBody", uploadResponseBody);
+        js.add("progress", contentLength>0?((100 * bytesRead) / contentLength):0);
+        js.add("downloadDone", downloadDone);
+        js.add("uploadDone", uploadDone);
+        var diff=currentTS-startTS;
+        js.add("rate",  diff>0?(bytesRead/diff):0);
+        return js;
+    }
+}

+ 78 - 0
src/main/java/com/acdp/transceivr/WebServer.java

@@ -0,0 +1,78 @@
+package com.acdp.transceivr;
+
+import java.net.http.HttpClient;
+import java.nio.file.Paths;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import io.undertow.Handlers;
+import io.undertow.Undertow;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.handlers.resource.PathResourceManager;
+import io.undertow.util.Headers;
+import org.slf4j.LoggerFactory;
+
+import static io.undertow.Handlers.resource;
+
+public class WebServer {
+    private final static org.slf4j.Logger logger = LoggerFactory.getLogger(WebServer.class);
+    private final int port;
+    private Undertow server;
+    private HttpClient client;
+    private int nextID=1;
+    private ConcurrentHashMap<Integer, Transfer> uploads=new ConcurrentHashMap();
+
+    public WebServer(int port, boolean debug) {
+        this.port = port;
+        client= HttpClient.newBuilder()
+                .followRedirects(HttpClient.Redirect.NORMAL)
+                .build();
+    }
+
+    public void start() {
+        Undertow.Builder builder = Undertow.builder();
+        builder.addHttpListener(port, "0.0.0.0");
+        builder.setHandler(Handlers.routing()
+            .post("/xfer", new HttpHandler() {
+                @Override
+                public void handleRequest(HttpServerExchange ex) throws Exception {
+                    if(!dispatch(ex,this)) return;
+                    JsonObject value = Json.parse(new String(ex.getInputStream().readAllBytes())).asObject();
+                    Transfer t=new Transfer(nextID++);
+                    t.from=value.get("from").asString();
+                    t.to=value.get("to").asString();
+                    LoadTools.startTransfer(t);
+                    uploads.put(t.id, t);
+                    ex.getResponseSender().send("OK: "+t.id);
+                }
+            })
+            .get("/status", new HttpHandler() {
+                @Override
+                public void handleRequest(HttpServerExchange ex) throws Exception {
+                    if (!dispatch(ex, this)) return;
+                    JsonArray ja=new JsonArray();
+                    for(Transfer t:uploads.values()) {
+                        ja.add(t.toJSON());
+                    }
+                    ex.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
+                    ex.getResponseSender().send(ja.toString());
+                }})
+            .get("/*", resource(new PathResourceManager(Paths.get("webroot"), 100))
+                    .setDirectoryListingEnabled(true))
+        );
+        server = builder.build();
+        server.start();
+    }
+
+    private boolean dispatch(HttpServerExchange ex, HttpHandler handler) {
+        if (ex.isInIoThread()) {
+            ex.dispatch(handler);
+            return false;
+        }
+        ex.startBlocking();
+        return true;
+    }
+}

+ 2 - 0
transceivr.iml

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4" />

+ 97 - 0
webroot/index.html

@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html lang="en">
+<style>
+    #myProgress {
+        width: 100%;
+        background-color: #ddd;
+    }
+
+    #myBar {
+        width: 1%;
+        height: 30px;
+        background-color: #4CAF50;
+    }
+</style>
+<head>
+    <meta charset="utf-8">
+    <title>Transloadr</title>
+    <script src="js/vue.js"></script>
+    <script src="js/moment.js"></script>
+
+</head>
+<body>
+
+<div id="app">
+    <table>
+        <template v-for="(item, index) in transfers">
+            <tr>
+                <td> {{ item.id }}</td>
+                <td style="width:200px;">
+                    <div id="myProgress">
+                        <div id="myBar" :style="{width: item.progress + '%'}">{{ item.progress }}</div>
+                    </div>
+                </td>
+                <td style="width:200px;">
+                    <pre></pre>
+                </td>
+            </tr>
+            <tr>
+                <td></td>
+                <td>
+<pre>id: {{ item.id}}
+from: {{ item.from}}
+to: {{ item.to}}
+bytesRead: {{ item.bytesRead}}
+uploadError: {{ item.uploadError}}
+uploadCode: {{ item.uploadCode}}
+uploadResponseBody: {{ item.uploadResponseBody}}
+uploadDone: {{ item.uploadDone}}
+downloadDone: {{ item.downloadDone}}
+rate: {{ item.rate}}k/s
+
+</pre>
+                </td>
+                <td></td>
+            </tr>
+        </template>
+    </table>
+</div>
+
+<script type="application/javascript">
+    new Vue({
+        el: '#app',
+        data: {
+            input: '',
+            transfers: [],
+        },
+        created: function () {
+            setInterval(this.update, 330);
+        },
+        watch: {},
+        methods: {
+            ts2txt: function (ts) {
+                return moment(ts).format('YYYY-MM-DD hh:mm ss.SSS ')
+            },
+            handleErrors: function (response) {
+                if (!response.ok) throw Error(response.statusText);
+                return response;
+            },
+            update: function (event) {
+                fetch('/status', {method: "GET"})
+                    .then(this.handleErrors)
+                    .then(response => {
+                        return response.json();
+                    })
+                    .then(myJson => {
+                        this.$data.transfers = myJson;
+                    })
+                    .catch(error => {
+                        this.$data.log = [];
+                        console.log(error)
+                    });
+            }
+        }
+    })
+</script>
+</body>
+</html>

File diff suppressed because it is too large
+ 4602 - 0
webroot/js/moment.js


File diff suppressed because it is too large
+ 11907 - 0
webroot/js/vue.js