1
0
Fork 0
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:
Przemek Dragańczuk 2020-03-24 09:07:25 +01:00 committed by GitHub
parent 25adf04903
commit 7f275bf6af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 148 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@ url.iml
.settings/ .settings/
urls.csv urls.csv
.env .env
urls.sqlite

View file

@ -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

View file

@ -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 {

View file

@ -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:

View file

@ -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;
}
}

View file

@ -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 "";
} }
} }

View file

@ -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();
}
}
}

View 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();
}
}
}