diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/README.md b/README.md
index e69de29..c1be26d 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,9 @@
+# Project Pie Cannon
+Pie Cannon is an easy way to upload files to the internet and get a shareable link. File goes in, link comes out.
+
+It doesn't come with a backend because you're expected to bring your own. A plain old FTP/SFTP+HTTP server will work fine, but support for more backends is on the way.
+
+This project is separated into a library and a desktop application (targeting mainly GNU/Linux), with an Android application in the works.
+
+## How to use
+TODO
diff --git a/desktop/piecannon b/desktop/piecannon
new file mode 100755
index 0000000..9b502c5
--- /dev/null
+++ b/desktop/piecannon
@@ -0,0 +1,3 @@
+#!/bin/sh
+RESULT=$(mvn -B exec:java -Dexec.mainClass=net.monarchpass.piecannon.App -Dexec.arguments=$1 -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN | tail -n 1)
+xdg-open $RESULT
diff --git a/desktop/pom.xml b/desktop/pom.xml
new file mode 100644
index 0000000..3f8aea0
--- /dev/null
+++ b/desktop/pom.xml
@@ -0,0 +1,67 @@
+
+ 4.0.0
+
+ net.monarchpass
+ piecannon-desktop-app
+ 0.0.1-SNAPSHOT
+
+
+ 1.8
+ 1.8
+
+
+
+
+ net.monarchpass
+ libpiecannon
+ 0.0.1-SNAPSHOT
+
+
+ com.google.guava
+ guava
+ 30.0-jre
+
+
+ net.kothar
+ xdg-java
+ 0.1.1
+
+
+ com.google.code.gson
+ gson
+ 2.8.6
+
+
+ org.projectlombok
+ lombok
+ 1.18.16
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.7.0
+ test
+
+
+ com.google.truth
+ truth
+ 1.1
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+ 3.8.1
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+
+
diff --git a/desktop/src/main/java/net/monarchpass/piecannon/App.java b/desktop/src/main/java/net/monarchpass/piecannon/App.java
new file mode 100644
index 0000000..0eea3a7
--- /dev/null
+++ b/desktop/src/main/java/net/monarchpass/piecannon/App.java
@@ -0,0 +1,84 @@
+package net.monarchpass.piecannon;
+
+import java.net.URI;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+
+import java.util.Collections;
+import java.util.Random;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
+import org.freedesktop.BaseDirectory;
+
+import com.google.common.collect.Streams;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import lombok.extern.java.Log;
+import java.util.logging.Level;
+
+@Log
+public class App {
+ public static void main (final String... args) throws Exception {
+ final File serversJson = getServersJson();
+ final List servers = loadServersFrom(serversJson);
+ log.log(Level.INFO, "{0} servers loaded from {1}", new Object[] {
+ servers.size(), serversJson
+ });
+
+ if (servers.isEmpty()) {
+ log.log(Level.SEVERE, "No servers defined, please define at least one in {0}", serversJson);
+ System.exit(1);
+ }
+
+ final Server server = servers.get(new Random().nextInt(servers.size()));
+ log.log(Level.INFO, "Randomly selected server: {0}", server.getLabel());
+
+ if (args.length == 0) {
+ log.log(Level.SEVERE, "No filename provided");
+ System.exit(1);
+ }
+
+ final File source = new File(args[0]);
+ if (!source.exists()) {
+ log.log(Level.SEVERE, "No such file: {0}", source);
+ System.exit(1);
+ }
+
+ final URI result = server.upload(source);
+ System.out.println(result);
+ }
+
+ public static List loadServersFrom (final File serversJson) throws IOException {
+ if (!serversJson.exists()) {
+ return Collections.EMPTY_LIST;
+ }
+
+ final ServerFactory factory = new ServerFactory();
+ try (final InputStream in = new FileInputStream(serversJson)) {
+ final JsonParser parser = new JsonParser();
+ return Streams.stream(parser.parse(new InputStreamReader(in)).getAsJsonArray())
+ .filter(JsonElement::isJsonObject)
+ .map(JsonObject.class::cast)
+ .map(factory)
+ .collect(Collectors.toList());
+ }
+ }
+
+ public static File getServersJson () {
+ return new File(getDataDirectory(), "servers.json");
+ }
+
+ public static File getDataDirectory () {
+ return new File(BaseDirectory.get(BaseDirectory.XDG_DATA_HOME), "piecannon");
+ }
+}
diff --git a/desktop/src/main/java/net/monarchpass/piecannon/ServerFactory.java b/desktop/src/main/java/net/monarchpass/piecannon/ServerFactory.java
new file mode 100644
index 0000000..a84e51b
--- /dev/null
+++ b/desktop/src/main/java/net/monarchpass/piecannon/ServerFactory.java
@@ -0,0 +1,31 @@
+package net.monarchpass.piecannon;
+
+import java.net.URI;
+
+import java.util.function.Function;
+
+import com.google.gson.JsonObject;
+import net.monarchpass.piecannon.impl.SftpServer;
+
+import com.google.common.base.MoreObjects;
+
+public class ServerFactory implements Function {
+ public Server apply (final JsonObject object) {
+ return makeSftpServer(object);
+ }
+
+ private Server makeSftpServer (final JsonObject object) {
+ final String host = object.getAsJsonPrimitive("host").getAsString();
+ final String label = object.has("label") ? object.getAsJsonPrimitive("label").getAsString() : host;
+ final int port = object.has("port") ? object.getAsJsonPrimitive("port").getAsInt() : 22;
+ final String username = object.getAsJsonPrimitive("username").getAsString();
+ final String password = object.getAsJsonPrimitive("password").getAsString();
+ final String path = object.getAsJsonPrimitive("path").getAsString();
+ final String url = object.getAsJsonPrimitive("url").getAsString();
+
+ return new SftpServer(
+ label, host, port, username, password,
+ path, URI.create(url)
+ );
+ }
+}
diff --git a/desktop/src/test/java/net/monarchpass/piecannon/AppTest.java b/desktop/src/test/java/net/monarchpass/piecannon/AppTest.java
new file mode 100644
index 0000000..bf6f223
--- /dev/null
+++ b/desktop/src/test/java/net/monarchpass/piecannon/AppTest.java
@@ -0,0 +1,10 @@
+package net.monarchpass.piecannon;
+
+import org.junit.jupiter.api.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class AppTest {
+ @Test
+ public void test () throws Exception {}
+}
diff --git a/lib/pom.xml b/lib/pom.xml
new file mode 100644
index 0000000..e096e10
--- /dev/null
+++ b/lib/pom.xml
@@ -0,0 +1,57 @@
+
+ 4.0.0
+
+ net.monarchpass
+ libpiecannon
+ 0.0.1-SNAPSHOT
+
+
+ 1.8
+ 1.8
+
+
+
+
+ com.github.mwiede
+ jsch
+ 0.1.60
+
+
+ com.google.guava
+ guava
+ 30.0-android
+
+
+ org.projectlombok
+ lombok
+ 1.18.16
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.7.0
+ test
+
+
+ com.google.truth
+ truth
+ 1.1
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+ 3.8.1
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+
+
diff --git a/lib/src/main/java/net/monarchpass/piecannon/PieCannon.java b/lib/src/main/java/net/monarchpass/piecannon/PieCannon.java
new file mode 100644
index 0000000..04a0fc7
--- /dev/null
+++ b/lib/src/main/java/net/monarchpass/piecannon/PieCannon.java
@@ -0,0 +1,39 @@
+package net.monarchpass.piecannon;
+
+import java.net.URI;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharSource;
+import com.google.common.io.ByteStreams;
+
+public class PieCannon {
+ private final List servers = new ArrayList<>();
+
+ public void addServer (final Server server) {
+ servers.add(server);
+ }
+
+ public List getServers () {
+ return Collections.unmodifiableList(servers);
+ }
+
+ public static boolean testServer (final Server server) {
+ final String testString = "piecannon-test-" + System.currentTimeMillis();
+ final URI result = server.upload("piecannon.test", CharSource.wrap(testString).asByteSource(Charsets.UTF_8));
+
+ try (final InputStream in = result.toURL().openStream()) {
+ return new String(ByteStreams.toByteArray(in), Charsets.UTF_8).equals(testString);
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+
+ return false;
+ }
+}
diff --git a/lib/src/main/java/net/monarchpass/piecannon/Server.java b/lib/src/main/java/net/monarchpass/piecannon/Server.java
new file mode 100644
index 0000000..29cb451
--- /dev/null
+++ b/lib/src/main/java/net/monarchpass/piecannon/Server.java
@@ -0,0 +1,12 @@
+package net.monarchpass.piecannon;
+
+import java.io.File;
+import java.net.URI;
+import com.google.common.io.Files;
+import com.google.common.io.ByteSource;
+
+public interface Server {
+ public String getLabel ();
+ public URI upload (String name, ByteSource source);
+ public default URI upload (File file) {return upload(file.getName(), Files.asByteSource(file));}
+}
diff --git a/lib/src/main/java/net/monarchpass/piecannon/impl/SftpServer.java b/lib/src/main/java/net/monarchpass/piecannon/impl/SftpServer.java
new file mode 100644
index 0000000..f1e6cc0
--- /dev/null
+++ b/lib/src/main/java/net/monarchpass/piecannon/impl/SftpServer.java
@@ -0,0 +1,53 @@
+package net.monarchpass.piecannon.impl;
+
+import net.monarchpass.piecannon.Server;
+import com.google.common.io.ByteSource;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.net.URI;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.Session;
+import com.jcraft.jsch.Channel;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.SftpException;
+
+import lombok.Data;
+
+@Data
+public class SftpServer implements Server {
+ private final String label;
+ private final String host;
+ private final int port;
+ private final String username;
+ private final String password;
+ private final String path;
+ private final URI uri;
+
+ public URI upload (String name, ByteSource source) {
+ try {
+ final JSch jsch = new JSch();
+ final Session session = jsch.getSession(username, host, port);
+ session.setPassword(password);
+ session.setConfig("StrictHostKeyChecking", "no");
+ session.connect();
+
+ final Channel channel = session.openChannel("sftp");
+ channel.connect();
+
+ final ChannelSftp sftp = (ChannelSftp) channel;
+ sftp.cd(path);
+
+ try (InputStream in = source.openStream()) {
+ sftp.put(in, name);
+ }
+
+ session.disconnect();
+ return URI.create(uri.toString() + "/" + name);
+ } catch (final JSchException | IOException | SftpException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+}
diff --git a/lib/src/test/java/net/monarchpass/piecannon/SftpTest.java b/lib/src/test/java/net/monarchpass/piecannon/SftpTest.java
new file mode 100644
index 0000000..c9cb1f8
--- /dev/null
+++ b/lib/src/test/java/net/monarchpass/piecannon/SftpTest.java
@@ -0,0 +1,30 @@
+package net.monarchpass.piecannon;
+
+import java.net.URI;
+import org.junit.jupiter.api.Test;
+import net.monarchpass.piecannon.impl.SftpServer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class SftpTest {
+ @Test
+ public void test () throws Exception {
+ final String testServerHost = System.getProperty("piecannon.test.host");
+ if (testServerHost == null) {
+ return;
+ }
+
+ final String testServerUser = System.getProperty("piecannon.test.user");
+ final String testServerPassword = System.getProperty("piecannon.test.password");
+ final String testServerPath = System.getProperty("piecannon.test.path");
+ final String testServerURL = System.getProperty("piecannon.test.url");
+
+ final Server server = new SftpServer(
+ "Test Server Instance", testServerHost, 22,
+ testServerUser, testServerPassword, testServerPath,
+ URI.create(testServerURL)
+ );
+
+ assertThat(PieCannon.testServer(server)).isTrue();
+ }
+}