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.png)
# Planned features
- An actual name
- Some form of authentication
- Input validation (on client and server)
# Planned features for 1.0 (in order of importance
- Deleting links using API and frontend
- Code cleanup
- Better deduplication
- Code cleanup
- An actual name
- Official Docker Hub image
# Usage
@ -38,7 +36,7 @@ git clone https://github.com/draganczukp/url
```
## Building from source
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
```
@ -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
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
```
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
### `docker run` method
1. Build the image
```
docker build . -t url:1.0
docker build . -t url:latest
```
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
```
touch ./urls.csv
docker run -p 4567:4567 \
-e file.location=/urls.csv \
-e username="username"
-e password="password"
-v ./urls.csv:/urls.csv \
-d url:1.0
```

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package tk.draganczuk.url;
import spark.Filter;
import static spark.Spark.*;
public class App {
@ -17,13 +19,26 @@ public class App {
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) -> {
res.redirect("/index.html");
return "Redirect";
});
path("/api", () -> {
before("/*", authFilter);
get("/all", Routes::getAll);
post("/new", Routes::addUrl);
delete("/:shortUrl", Routes::delete);
});
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;
import org.eclipse.jetty.http.HttpStatus;
import spark.Request;
import spark.Response;
@ -29,8 +30,14 @@ public class Routes {
shortUrl = Utils.randomString();
}
if (Utils.validate(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) {
String shortUrl = req.params("shortUrl");
@ -47,4 +54,17 @@ public class Routes {
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.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Optional;
@ -55,4 +56,27 @@ public class UrlFile {
var split = line.split(",");
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;
import java.util.Random;
import java.util.regex.Pattern;
public class Utils {
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() {
int leftLimit = 48; // numeral '0'
int rightLimit = 122; // letter 'z'
int targetStringLength = 10;
String generatedString = random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
return random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append)
.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>
<div class="pure-control-group">
<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 class="pure-control-group">
<label for="shortUrl">Short URL (Optional)</label>
<input type="text" name="shortUrl" id="shortUrl" placeholder="Short URL (optional)"/>
<label for="shortUrl">Short URL (Optional). Only letters, number dashes and underscores
permitted</label>
<input type="text" name="shortUrl" id="shortUrl" placeholder="Short URL (optional)"
pattern="[a-z0-9_-]+"/>
</div>
<div class="pure-controls">
<button class="pure-button pure-button-primary">Submit</button>
@ -48,6 +50,7 @@
<tr>
<td>Long URL</td>
<td>Short url</td>
<td></td>
</tr>
</thead>
<tbody id="url-table">

View File

@ -1,5 +1,5 @@
const refreshData = async () => {
let data = await fetch("/all").then(res => res.text());
let data = await fetch("/api/all").then(res => res.text());
data = data
.split("\n")
.filter(line => line !== "")
@ -23,9 +23,11 @@ const TR = (row) => {
const tr = document.createElement("tr");
const longTD = TD(A(row.long));
const shortTD = TD(A_INT(row.short));
const btn = deleteButton(row.short);
tr.appendChild(longTD);
tr.appendChild(shortTD);
tr.appendChild(btn);
return tr;
};
@ -33,6 +35,21 @@ const TR = (row) => {
const A = (s) => `<a href='${s}'>${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 = document.createElement("td");
td.innerHTML = s;
@ -44,7 +61,7 @@ const submitForm = () => {
const longUrl = form.elements["longUrl"];
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, {
method: "POST"