mirror of
https://github.com/SinTan1729/chhoto-url
synced 2024-12-26 23:58:35 -06:00
Sqlite as storage backend (#1)
Some platforms has some problems with file locking, so I was forced to use an alternative. SQLite seems be the best option currently available * Migrated to an sqlite database * Removed unnecessary IOExceptions * Removed an util class not needed anymore * Updated README.md and docker-compose.yml to reflect new storage mechanism
This commit is contained in:
parent
25adf04903
commit
7f275bf6af
8 changed files with 137 additions and 148 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,3 +10,4 @@ url.iml
|
||||||
.settings/
|
.settings/
|
||||||
urls.csv
|
urls.csv
|
||||||
.env
|
.env
|
||||||
|
urls.sqlite
|
||||||
|
|
26
README.md
26
README.md
|
@ -13,7 +13,7 @@ unnecessary features, or they didn't have all the features I wanted.
|
||||||
to the correct long URL (you'd think that's a standard feature, but
|
to the correct long URL (you'd think that's a standard feature, but
|
||||||
apparently it's not)
|
apparently it's not)
|
||||||
- Provides a simple API for adding new short links
|
- Provides a simple API for adding new short links
|
||||||
- Links are stored in a plaintext CSV file
|
- Links are stored in an SQLite database
|
||||||
- Available as a Docker container (there is no image on docker hub _yet_)
|
- Available as a Docker container (there is no image on docker hub _yet_)
|
||||||
- Backend written in Java using [Spark Java](http://sparkjava.com/), frontend
|
- Backend written in Java using [Spark Java](http://sparkjava.com/), frontend
|
||||||
written in plain HTML and vanilla JS, using [Pure CSS](https://purecss.io/)
|
written in plain HTML and vanilla JS, using [Pure CSS](https://purecss.io/)
|
||||||
|
@ -45,9 +45,11 @@ in order to speed up future builds.
|
||||||
|
|
||||||
### 2. Set environment variables
|
### 2. Set environment variables
|
||||||
```bash
|
```bash
|
||||||
|
# Required for authentication
|
||||||
export username=<api username>
|
export username=<api username>
|
||||||
export password=<api password>
|
export password=<api password>
|
||||||
export file.location=<file location> # opitonal
|
# Sets where the database exists. Can be local or remote (optional)
|
||||||
|
export db.url=<url> # Default: './urls.sqlite'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Run it
|
### 3. Run it
|
||||||
|
@ -64,20 +66,24 @@ docker build . -t url:latest
|
||||||
```
|
```
|
||||||
2. Run the image
|
2. Run the image
|
||||||
```
|
```
|
||||||
docker run -p 4567:4567 -d url:latest
|
docker run -p 4567:4567
|
||||||
|
-d url:latest
|
||||||
|
-e username="username"
|
||||||
|
-e password="password"
|
||||||
|
-d url:latest
|
||||||
```
|
```
|
||||||
2.a Make the CSV file available to host
|
2.a Make the database file available to host (optional)
|
||||||
```
|
```
|
||||||
touch ./urls.csv
|
touch ./urls.csv
|
||||||
docker run -p 4567:4567 \
|
docker run -p 4567:4567 \
|
||||||
-e file.location=/urls.csv \
|
-e username="username" \
|
||||||
-e username="username"
|
-e password="password" \
|
||||||
-e password="password"
|
-v ./urls.sqlite:/urls.csv \
|
||||||
-v ./urls.csv:/urls.csv \
|
-e db.url=/urls.csv \
|
||||||
-d url:1.0
|
-d url:latest
|
||||||
```
|
```
|
||||||
## `docker-compose`
|
## `docker-compose`
|
||||||
There is a sample `docker-compose.yml` file in this repository. You can use it
|
There is a sample `docker-compose.yml` file in this repository configured for Traefik. You can use it
|
||||||
as a base, modifying it as needed. Run it with
|
as a base, modifying it as needed. Run it with
|
||||||
```
|
```
|
||||||
docker-compose up -d --build
|
docker-compose up -d --build
|
||||||
|
|
|
@ -24,6 +24,8 @@ jar {
|
||||||
dependencies {
|
dependencies {
|
||||||
compile "com.sparkjava:spark-core:2.8.0"
|
compile "com.sparkjava:spark-core:2.8.0"
|
||||||
compile 'com.qmetric:spark-authentication:1.4'
|
compile 'com.qmetric:spark-authentication:1.4'
|
||||||
|
compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
|
|
|
@ -6,11 +6,11 @@ services:
|
||||||
context: .
|
context: .
|
||||||
container_name: url
|
container_name: url
|
||||||
environment:
|
environment:
|
||||||
- file.location=/urls.csv
|
- db.url=/urls.sqlite
|
||||||
- username=${URL_LOGIN}
|
- username=${URL_LOGIN}
|
||||||
- password=${URL_PASSWORD}
|
- password=${URL_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- ./urls.csv:/urls.csv
|
- ./urls.sqlite:/urls.sqlite
|
||||||
networks:
|
networks:
|
||||||
- ${NETWORK}
|
- ${NETWORK}
|
||||||
labels:
|
labels:
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package tk.draganczuk.url;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pair
|
|
||||||
*/
|
|
||||||
public class Pair<T, U> {
|
|
||||||
|
|
||||||
private T left;
|
|
||||||
private U right;
|
|
||||||
|
|
||||||
|
|
||||||
public Pair(T left, U right) {
|
|
||||||
this.left = left;
|
|
||||||
this.right = right;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T, U> Pair<T, U> of(T left, U right){
|
|
||||||
return new Pair<T,U>(left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public T getLeft() {
|
|
||||||
return left;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLeft(T left) {
|
|
||||||
this.left = left;
|
|
||||||
}
|
|
||||||
|
|
||||||
public U getRight() {
|
|
||||||
return right;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRight(U right) {
|
|
||||||
this.right = right;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,18 +8,14 @@ import java.io.IOException;
|
||||||
|
|
||||||
public class Routes {
|
public class Routes {
|
||||||
|
|
||||||
private static UrlFile urlFile;
|
private static UrlRepository urlRepository;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
urlRepository = new UrlRepository();
|
||||||
urlFile = new UrlFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getAll(Request req, Response res) throws IOException {
|
public static String getAll(Request req, Response res) {
|
||||||
return String.join("\n", urlFile.getAll());
|
return String.join("\n", urlRepository.getAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String addUrl(Request req, Response res) {
|
public static String addUrl(Request req, Response res) {
|
||||||
|
@ -31,7 +27,7 @@ public class Routes {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.validate(shortUrl)) {
|
if (Utils.validate(shortUrl)) {
|
||||||
return urlFile.addUrl(longUrl, shortUrl);
|
return urlRepository.addUrl(longUrl, shortUrl);
|
||||||
} else {
|
} else {
|
||||||
res.status(HttpStatus.BAD_REQUEST_400);
|
res.status(HttpStatus.BAD_REQUEST_400);
|
||||||
return "shortUrl not valid ([a-z0-9]+)";
|
return "shortUrl not valid ([a-z0-9]+)";
|
||||||
|
@ -41,7 +37,7 @@ public class Routes {
|
||||||
|
|
||||||
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 = urlRepository
|
||||||
.findForShortUrl(shortUrl);
|
.findForShortUrl(shortUrl);
|
||||||
|
|
||||||
if (longUrlOpt.isEmpty()) {
|
if (longUrlOpt.isEmpty()) {
|
||||||
|
@ -56,15 +52,8 @@ public class Routes {
|
||||||
|
|
||||||
public static String delete(Request req, Response res) {
|
public static String delete(Request req, Response res) {
|
||||||
String shortUrl = req.params("shortUrl");
|
String shortUrl = req.params("shortUrl");
|
||||||
var longUrlOpt = urlFile
|
|
||||||
.findForShortUrl(shortUrl);
|
|
||||||
|
|
||||||
if (longUrlOpt.isEmpty()) {
|
urlRepository.deleteEntry(shortUrl);
|
||||||
res.status(404);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
urlFile.deleteEntry(String.format("%s,%s", shortUrl, longUrlOpt.get()));
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
|
|
||||||
public class UrlFile {
|
|
||||||
|
|
||||||
private File file;
|
|
||||||
|
|
||||||
public UrlFile() throws IOException{
|
|
||||||
String path = System.getenv().getOrDefault("file.location", "./urls.csv");
|
|
||||||
this.file = new File(path);
|
|
||||||
if (!file.exists()) {
|
|
||||||
file.createNewFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getAll() throws IOException{
|
|
||||||
return Files.readAllLines(file.toPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String addUrl(String longURL, String shortUrl){
|
|
||||||
String entry = String.format("%s,%s",shortUrl,longURL);
|
|
||||||
try {
|
|
||||||
var lineOpt = Files.lines(file.toPath())
|
|
||||||
.filter(line -> line.equals(entry))
|
|
||||||
.findAny();
|
|
||||||
if(lineOpt.isEmpty()){
|
|
||||||
Files.writeString(file.toPath(), entry + System.lineSeparator(), StandardOpenOption.APPEND);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
109
src/main/java/tk/draganczuk/url/UrlRepository.java
Normal file
109
src/main/java/tk/draganczuk/url/UrlRepository.java
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
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.sql.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
|
public class UrlRepository {
|
||||||
|
private static final String INSERT_ROW_SQL = "INSERT INTO urls (long_url, short_url) VALUES (?, ?)";
|
||||||
|
private static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS urls\n" +
|
||||||
|
"(\n" +
|
||||||
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
|
||||||
|
" long_url TEXT NOT NULL,\n" +
|
||||||
|
" short_url TEXT NOT NULL\n" +
|
||||||
|
");";
|
||||||
|
private static final String SELECT_FOR_SHORT_SQL = "SELECT long_url FROM urls WHERE short_url = ?";
|
||||||
|
private static final String DELETE_ROW_SQL = "DELETE FROM urls WHERE short_url = ?";
|
||||||
|
|
||||||
|
private String databaseUrl;
|
||||||
|
|
||||||
|
|
||||||
|
public UrlRepository() {
|
||||||
|
String path = System.getenv().getOrDefault("db.url", "./urls.sqlite");
|
||||||
|
|
||||||
|
databaseUrl = "jdbc:sqlite:" + path;
|
||||||
|
|
||||||
|
try (Connection conn = DriverManager.getConnection(databaseUrl)) {
|
||||||
|
if (conn != null) {
|
||||||
|
DatabaseMetaData meta = conn.getMetaData();
|
||||||
|
|
||||||
|
conn.createStatement()
|
||||||
|
.execute(CREATE_TABLE_SQL);
|
||||||
|
|
||||||
|
System.out.println("Database initialised");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.out.println(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAll() {
|
||||||
|
try (final var con = DriverManager.getConnection(databaseUrl)) {
|
||||||
|
var statement = con.createStatement();
|
||||||
|
|
||||||
|
statement.execute("SELECT * FROM urls");
|
||||||
|
ResultSet rs = statement.getResultSet();
|
||||||
|
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
result.add(String.format("%s,%s", rs.getString("short_url"), rs.getString("long_url")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String addUrl(String longURL, String shortUrl) {
|
||||||
|
try (final var con = DriverManager.getConnection(databaseUrl)) {
|
||||||
|
final var stmt = con.prepareStatement(INSERT_ROW_SQL);
|
||||||
|
stmt.setString(1, longURL);
|
||||||
|
stmt.setString(2, shortUrl);
|
||||||
|
if (stmt.execute()) {
|
||||||
|
return String.format("%s,%s", shortUrl, longURL);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> findForShortUrl(String shortUrl) {
|
||||||
|
try (final var con = DriverManager.getConnection(databaseUrl)) {
|
||||||
|
final var stmt = con.prepareStatement(SELECT_FOR_SHORT_SQL);
|
||||||
|
stmt.setString(1, shortUrl);
|
||||||
|
if (stmt.execute()) {
|
||||||
|
ResultSet rs = stmt.getResultSet();
|
||||||
|
if (rs.next()) {
|
||||||
|
return Optional.of(rs.getString("long_url"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteEntry(String shortUrl) {
|
||||||
|
try (final var con = DriverManager.getConnection(databaseUrl)) {
|
||||||
|
final var stmt = con.prepareStatement(DELETE_ROW_SQL);
|
||||||
|
stmt.setString(1, shortUrl);
|
||||||
|
stmt.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue