mirror of
https://github.com/minoplhy/chhoto-url.git
synced 2024-11-22 09:16:46 +00:00
Merge branch 'master' of github.com:draganczukp/url
This commit is contained in:
commit
1736a67a1c
27
README.md
27
README.md
@ -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
|
||||
```
|
||||
|
@ -23,6 +23,7 @@ jar {
|
||||
|
||||
dependencies {
|
||||
compile "com.sparkjava:spark-core:2.8.0"
|
||||
implementation 'com.qmetric:spark-authentication:1.4'
|
||||
}
|
||||
|
||||
application {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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";
|
||||
});
|
||||
|
||||
get("/all", Routes::getAll);
|
||||
post("/new", Routes::addUrl);
|
||||
|
||||
path("/api", () -> {
|
||||
before("/*", authFilter);
|
||||
get("/all", Routes::getAll);
|
||||
post("/new", Routes::addUrl);
|
||||
delete("/:shortUrl", Routes::delete);
|
||||
});
|
||||
|
||||
get("/:shortUrl", Routes::goToLongUrl);
|
||||
}
|
||||
}
|
||||
|
20
src/main/java/tk/draganczuk/url/Filters.java
Normal file
20
src/main/java/tk/draganczuk/url/Filters.java
Normal 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));
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package tk.draganczuk.url;
|
||||
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
|
||||
@ -29,15 +30,21 @@ public class Routes {
|
||||
shortUrl = Utils.randomString();
|
||||
}
|
||||
|
||||
return urlFile.addUrl(longUrl, shortUrl);
|
||||
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){
|
||||
|
||||
public static String goToLongUrl(Request req, Response res) {
|
||||
String shortUrl = req.params("shortUrl");
|
||||
var longUrlOpt = urlFile
|
||||
.findForShortUrl(shortUrl);
|
||||
.findForShortUrl(shortUrl);
|
||||
|
||||
if(longUrlOpt.isEmpty()){
|
||||
if (longUrlOpt.isEmpty()) {
|
||||
res.status(404);
|
||||
return "";
|
||||
}
|
||||
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -41,18 +42,41 @@ public class UrlFile {
|
||||
|
||||
public Optional<String> findForShortUrl(String shortUrl){
|
||||
try {
|
||||
return Files.lines(file.toPath())
|
||||
.map(this::splitLine)
|
||||
.filter(pair -> pair.getLeft().equals(shortUrl))
|
||||
.map(Pair::getRight)
|
||||
.findAny();
|
||||
return Files.lines(file.toPath())
|
||||
.map(this::splitLine)
|
||||
.filter(pair -> pair.getLeft().equals(shortUrl))
|
||||
.map(Pair::getRight)
|
||||
.findAny();
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Pair<String, String> splitLine(String line){
|
||||
public Pair<String, String> splitLine(String line) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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 = "×";
|
||||
|
||||
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"
|
||||
|
Loading…
Reference in New Issue
Block a user