Merge branch 'master' of github.com:draganczukp/url

This commit is contained in:
draganczukp 2020-02-16 16:55:16 +01:00
commit 1736a67a1c
10 changed files with 147 additions and 33 deletions

View File

@ -22,13 +22,11 @@ unnecessary features, or they didn't have all the features I wanted.
# Screenshot # Screenshot
![Screenshot](./screenshot.png) ![Screenshot](./screenshot.png)
# Planned features # Planned features for 1.0 (in order of importance
- An actual name
- Some form of authentication
- Input validation (on client and server)
- Deleting links using API and frontend - Deleting links using API and frontend
- Code cleanup
- Better deduplication - Better deduplication
- Code cleanup
- An actual name
- Official Docker Hub image - Official Docker Hub image
# Usage # Usage
@ -38,7 +36,7 @@ git clone https://github.com/draganczukp/url
``` ```
## Building from source ## Building from source
Gradle 6.x.x and JDK 11 are required. Other versions are not tested Gradle 6.x.x and JDK 11 are required. Other versions are not tested
1. Build the `.jar` file ### 1. Build the `.jar` file
``` ```
gradle build --no-daemon gradle build --no-daemon
``` ```
@ -46,27 +44,36 @@ The `--no-daemon` option means that gradle should exit as soon as the build is
finished. Without it, gradle would still be running in the background finished. Without it, gradle would still be running in the background
in order to speed up future builds. in order to speed up future builds.
2. Run it ### 2. Set environment variables
```bash
export username=<api username>
export password=<api password>
export file.location=<file location> # opitonal
```
### 3. Run it
``` ```
java -jar build/libs/url.jar java -jar build/libs/url.jar
``` ```
3. Navigate to `http://localhost:4567` in your browser, add links as you wish. ### 4. Navigate to `http://localhost:4567` in your browser, add links as you wish.
## Running with docker ## Running with docker
### `docker run` method ### `docker run` method
1. Build the image 1. Build the image
``` ```
docker build . -t url:1.0 docker build . -t url:latest
``` ```
2. Run the image 2. Run the image
``` ```
docker run -p 4567:4567 -d url:1.0 docker run -p 4567:4567 -d url:latest
``` ```
2.a Make the CSV file available to host 2.a Make the CSV file available to host
``` ```
touch ./urls.csv touch ./urls.csv
docker run -p 4567:4567 \ docker run -p 4567:4567 \
-e file.location=/urls.csv \ -e file.location=/urls.csv \
-e username="username"
-e password="password"
-v ./urls.csv:/urls.csv \ -v ./urls.csv:/urls.csv \
-d url:1.0 -d url:1.0
``` ```

View File

@ -23,6 +23,7 @@ jar {
dependencies { dependencies {
compile "com.sparkjava:spark-core:2.8.0" compile "com.sparkjava:spark-core:2.8.0"
implementation 'com.qmetric:spark-authentication:1.4'
} }
application { application {

View File

@ -4,10 +4,11 @@ services:
# TODO: Publish to docker hub # TODO: Publish to docker hub
build: build:
context: . context: .
# ports: container_name: url
# - 4567:4567
environment: environment:
- file.location=/urls.csv - file.location=/urls.csv
- username=${URL_LOGIN}
- password=${URL_PASSWORD}
volumes: volumes:
- ./urls.csv:/urls.csv - ./urls.csv:/urls.csv
networks: networks:
@ -30,4 +31,3 @@ services:
networks: networks:
proxy: proxy:
external: true external: true

View File

@ -1,5 +1,7 @@
package tk.draganczuk.url; package tk.draganczuk.url;
import spark.Filter;
import static spark.Spark.*; import static spark.Spark.*;
public class App { public class App {
@ -17,13 +19,26 @@ public class App {
port(Integer.parseInt(System.getProperty("port", "4567"))); port(Integer.parseInt(System.getProperty("port", "4567")));
// Add GZIP compression
after(Filters::addGZIP);
// Authenticate
Filter authFilter = Filters.createAuthFilter();
before("/index.html", authFilter);
get("/", (req, res) -> { get("/", (req, res) -> {
res.redirect("/index.html"); res.redirect("/index.html");
return "Redirect"; return "Redirect";
}); });
path("/api", () -> {
before("/*", authFilter);
get("/all", Routes::getAll); get("/all", Routes::getAll);
post("/new", Routes::addUrl); post("/new", Routes::addUrl);
delete("/:shortUrl", Routes::delete);
});
get("/:shortUrl", Routes::goToLongUrl); get("/:shortUrl", Routes::goToLongUrl);
} }
} }

View File

@ -0,0 +1,20 @@
package tk.draganczuk.url;
import com.qmetric.spark.authentication.AuthenticationDetails;
import com.qmetric.spark.authentication.BasicAuthenticationFilter;
import spark.Filter;
import spark.Request;
import spark.Response;
public class Filters {
public static void addGZIP(Request request, Response response) {
response.header("Content-Encoding", "gzip");
}
public static Filter createAuthFilter() {
String username = System.getenv("username");
String password = System.getenv("password");
return new BasicAuthenticationFilter(new AuthenticationDetails(username, password));
}
}

View File

@ -1,5 +1,6 @@
package tk.draganczuk.url; package tk.draganczuk.url;
import org.eclipse.jetty.http.HttpStatus;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
@ -29,15 +30,21 @@ public class Routes {
shortUrl = Utils.randomString(); shortUrl = Utils.randomString();
} }
if (Utils.validate(shortUrl)) {
return urlFile.addUrl(longUrl, shortUrl); return urlFile.addUrl(longUrl, shortUrl);
} else {
res.status(HttpStatus.BAD_REQUEST_400);
return "shortUrl not valid ([a-z0-9]+)";
}
} }
public static String goToLongUrl(Request req, Response res){
public static String goToLongUrl(Request req, Response res) {
String shortUrl = req.params("shortUrl"); String shortUrl = req.params("shortUrl");
var longUrlOpt = urlFile var longUrlOpt = urlFile
.findForShortUrl(shortUrl); .findForShortUrl(shortUrl);
if(longUrlOpt.isEmpty()){ if (longUrlOpt.isEmpty()) {
res.status(404); res.status(404);
return ""; return "";
} }
@ -47,4 +54,17 @@ public class Routes {
return ""; return "";
} }
public static String delete(Request req, Response res) {
String shortUrl = req.params("shortUrl");
var longUrlOpt = urlFile
.findForShortUrl(shortUrl);
if (longUrlOpt.isEmpty()) {
res.status(404);
return "";
}
urlFile.deleteEntry(String.format("%s,%s", shortUrl, longUrlOpt.get()));
return "";
}
} }

View File

@ -3,6 +3,7 @@ package tk.draganczuk.url;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -51,8 +52,31 @@ public class UrlFile {
} }
} }
public Pair<String, String> splitLine(String line){ public Pair<String, String> splitLine(String line) {
var split = line.split(","); var split = line.split(",");
return new Pair<>(split[0], split[1]); return new Pair<>(split[0], split[1]);
} }
public void deleteEntry(String entry) {
try {
File tmp = File.createTempFile(file.getName(), ".tmp");
if (!tmp.exists()) {
tmp.createNewFile();
}
Files.lines(file.toPath())
.filter(line -> !line.equals(entry))
.forEach(line -> {
try {
Files.writeString(tmp.toPath(), line + "\n", StandardOpenOption.APPEND);
} catch (IOException e) {
e.printStackTrace();
}
});
Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
} }

View File

@ -1,23 +1,30 @@
package tk.draganczuk.url; package tk.draganczuk.url;
import java.util.Random; import java.util.Random;
import java.util.regex.Pattern;
public class Utils { public class Utils {
private static final Random random = new Random(System.currentTimeMillis()); private static final Random random = new Random(System.currentTimeMillis());
private static final String SHORT_URL_PATTERN = "[a-z0-9_-]+";
private static final Pattern PATTERN = Pattern.compile(SHORT_URL_PATTERN);
public static String randomString() { public static String randomString() {
int leftLimit = 48; // numeral '0' int leftLimit = 48; // numeral '0'
int rightLimit = 122; // letter 'z' int rightLimit = 122; // letter 'z'
int targetStringLength = 10; int targetStringLength = 10;
String generatedString = random.ints(leftLimit, rightLimit + 1) return random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) .filter(i -> (i <= 57 || i >= 97))
.limit(targetStringLength) .limit(targetStringLength)
.collect(StringBuilder::new, .collect(StringBuilder::new,
StringBuilder::appendCodePoint, StringBuilder::appendCodePoint,
StringBuilder::append) StringBuilder::append)
.toString(); .toString();
}
return generatedString; public static boolean validate(String shortUrl) {
return PATTERN.matcher(shortUrl)
.matches();
} }
} }

View File

@ -30,11 +30,13 @@
<legend>Add new URL</legend> <legend>Add new URL</legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="longUrl">Long URL</label> <label for="longUrl">Long URL</label>
<input type="text" name="longUrl" id="longUrl" placeholder="Long URL" required/> <input type="url" name="longUrl" id="longUrl" placeholder="Long URL" required/>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="shortUrl">Short URL (Optional)</label> <label for="shortUrl">Short URL (Optional). Only letters, number dashes and underscores
<input type="text" name="shortUrl" id="shortUrl" placeholder="Short URL (optional)"/> permitted</label>
<input type="text" name="shortUrl" id="shortUrl" placeholder="Short URL (optional)"
pattern="[a-z0-9_-]+"/>
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<button class="pure-button pure-button-primary">Submit</button> <button class="pure-button pure-button-primary">Submit</button>
@ -48,6 +50,7 @@
<tr> <tr>
<td>Long URL</td> <td>Long URL</td>
<td>Short url</td> <td>Short url</td>
<td></td>
</tr> </tr>
</thead> </thead>
<tbody id="url-table"> <tbody id="url-table">

View File

@ -1,5 +1,5 @@
const refreshData = async () => { const refreshData = async () => {
let data = await fetch("/all").then(res => res.text()); let data = await fetch("/api/all").then(res => res.text());
data = data data = data
.split("\n") .split("\n")
.filter(line => line !== "") .filter(line => line !== "")
@ -23,9 +23,11 @@ const TR = (row) => {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
const longTD = TD(A(row.long)); const longTD = TD(A(row.long));
const shortTD = TD(A_INT(row.short)); const shortTD = TD(A_INT(row.short));
const btn = deleteButton(row.short);
tr.appendChild(longTD); tr.appendChild(longTD);
tr.appendChild(shortTD); tr.appendChild(shortTD);
tr.appendChild(btn);
return tr; return tr;
}; };
@ -33,6 +35,21 @@ const TR = (row) => {
const A = (s) => `<a href='${s}'>${s}</a>`; const A = (s) => `<a href='${s}'>${s}</a>`;
const A_INT = (s) => `<a href='/${s}'>${window.location.host}/${s}</a>`; const A_INT = (s) => `<a href='/${s}'>${window.location.host}/${s}</a>`;
const deleteButton = (shortUrl) => {
const btn = document.createElement("button");
btn.innerHTML = "&times;";
btn.onclick = e => {
e.preventDefault();
fetch(`/api/${shortUrl}`, {
method: "DELETE"
}).then(_ => refreshData());
};
return btn;
};
const TD = (s) => { const TD = (s) => {
const td = document.createElement("td"); const td = document.createElement("td");
td.innerHTML = s; td.innerHTML = s;
@ -44,7 +61,7 @@ const submitForm = () => {
const longUrl = form.elements["longUrl"]; const longUrl = form.elements["longUrl"];
const shortUrl = form.elements["shortUrl"]; const shortUrl = form.elements["shortUrl"];
const url = `/new?long=${longUrl.value}&short=${shortUrl.value}`; const url = `/api/new?long=${longUrl.value}&short=${shortUrl.value}`;
fetch(url, { fetch(url, {
method: "POST" method: "POST"