mirror of
https://github.com/SinTan1729/chhoto-url
synced 2024-12-26 07:38:36 -06:00
Merge branch 'master' of github.com:draganczukp/url
This commit is contained in:
commit
1736a67a1c
10 changed files with 147 additions and 33 deletions
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](./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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
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;
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
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 "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -41,18 +42,41 @@ public class UrlFile {
|
||||||
|
|
||||||
public Optional<String> findForShortUrl(String shortUrl){
|
public Optional<String> findForShortUrl(String shortUrl){
|
||||||
try {
|
try {
|
||||||
return Files.lines(file.toPath())
|
return Files.lines(file.toPath())
|
||||||
.map(this::splitLine)
|
.map(this::splitLine)
|
||||||
.filter(pair -> pair.getLeft().equals(shortUrl))
|
.filter(pair -> pair.getLeft().equals(shortUrl))
|
||||||
.map(Pair::getRight)
|
.map(Pair::getRight)
|
||||||
.findAny();
|
.findAny();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 = "×";
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
Loading…
Reference in a new issue