From 7f275bf6af59c48b0e0d4df8583fa44dfc5e36a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Tue, 24 Mar 2020 09:07:25 +0100 Subject: [PATCH] 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 --- .gitignore | 1 + README.md | 26 +++-- build.gradle | 2 + docker-compose.yml | 4 +- src/main/java/tk/draganczuk/url/Pair.java | 36 ------ src/main/java/tk/draganczuk/url/Routes.java | 25 ++-- src/main/java/tk/draganczuk/url/UrlFile.java | 82 ------------- .../java/tk/draganczuk/url/UrlRepository.java | 109 ++++++++++++++++++ 8 files changed, 137 insertions(+), 148 deletions(-) delete mode 100644 src/main/java/tk/draganczuk/url/Pair.java delete mode 100644 src/main/java/tk/draganczuk/url/UrlFile.java create mode 100644 src/main/java/tk/draganczuk/url/UrlRepository.java diff --git a/.gitignore b/.gitignore index 2baf164..f7e5bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ url.iml .settings/ urls.csv .env +urls.sqlite diff --git a/README.md b/README.md index 24809c3..9dbea16 100644 --- a/README.md +++ b/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 apparently it's not) - 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_) - Backend written in Java using [Spark Java](http://sparkjava.com/), frontend 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 ```bash +# Required for authentication export username= export password= -export file.location= # opitonal +# Sets where the database exists. Can be local or remote (optional) +export db.url= # Default: './urls.sqlite' ``` ### 3. Run it @@ -64,20 +66,24 @@ docker build . -t url:latest ``` 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 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 + -e username="username" \ + -e password="password" \ + -v ./urls.sqlite:/urls.csv \ + -e db.url=/urls.csv \ + -d url:latest ``` ## `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 ``` docker-compose up -d --build diff --git a/build.gradle b/build.gradle index 2ed2c62..6106106 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,8 @@ jar { dependencies { compile "com.sparkjava:spark-core:2.8.0" compile 'com.qmetric:spark-authentication:1.4' + compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1' + } application { diff --git a/docker-compose.yml b/docker-compose.yml index eabed78..a118fba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,11 +6,11 @@ services: context: . container_name: url environment: - - file.location=/urls.csv + - db.url=/urls.sqlite - username=${URL_LOGIN} - password=${URL_PASSWORD} volumes: - - ./urls.csv:/urls.csv + - ./urls.sqlite:/urls.sqlite networks: - ${NETWORK} labels: diff --git a/src/main/java/tk/draganczuk/url/Pair.java b/src/main/java/tk/draganczuk/url/Pair.java deleted file mode 100644 index 04a93b1..0000000 --- a/src/main/java/tk/draganczuk/url/Pair.java +++ /dev/null @@ -1,36 +0,0 @@ -package tk.draganczuk.url; - -/** -* Pair -*/ -public class Pair { - - private T left; - private U right; - - - public Pair(T left, U right) { - this.left = left; - this.right = right; - } - - public static Pair of(T left, U right){ - return new Pair(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; - } -} diff --git a/src/main/java/tk/draganczuk/url/Routes.java b/src/main/java/tk/draganczuk/url/Routes.java index 2e9b590..e775660 100644 --- a/src/main/java/tk/draganczuk/url/Routes.java +++ b/src/main/java/tk/draganczuk/url/Routes.java @@ -8,18 +8,14 @@ import java.io.IOException; public class Routes { - private static UrlFile urlFile; + private static UrlRepository urlRepository; static { - try { - urlFile = new UrlFile(); - } catch (IOException e) { - e.printStackTrace(); - } + urlRepository = new UrlRepository(); } - public static String getAll(Request req, Response res) throws IOException { - return String.join("\n", urlFile.getAll()); + public static String getAll(Request req, Response res) { + return String.join("\n", urlRepository.getAll()); } public static String addUrl(Request req, Response res) { @@ -31,7 +27,7 @@ public class Routes { } if (Utils.validate(shortUrl)) { - return urlFile.addUrl(longUrl, shortUrl); + return urlRepository.addUrl(longUrl, shortUrl); } else { res.status(HttpStatus.BAD_REQUEST_400); return "shortUrl not valid ([a-z0-9]+)"; @@ -41,7 +37,7 @@ public class Routes { public static String goToLongUrl(Request req, Response res) { String shortUrl = req.params("shortUrl"); - var longUrlOpt = urlFile + var longUrlOpt = urlRepository .findForShortUrl(shortUrl); if (longUrlOpt.isEmpty()) { @@ -56,15 +52,8 @@ public class Routes { 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())); + urlRepository.deleteEntry(shortUrl); return ""; } } diff --git a/src/main/java/tk/draganczuk/url/UrlFile.java b/src/main/java/tk/draganczuk/url/UrlFile.java deleted file mode 100644 index f666fb2..0000000 --- a/src/main/java/tk/draganczuk/url/UrlFile.java +++ /dev/null @@ -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 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 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 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(); - } - } -} diff --git a/src/main/java/tk/draganczuk/url/UrlRepository.java b/src/main/java/tk/draganczuk/url/UrlRepository.java new file mode 100644 index 0000000..456e3c2 --- /dev/null +++ b/src/main/java/tk/draganczuk/url/UrlRepository.java @@ -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 getAll() { + try (final var con = DriverManager.getConnection(databaseUrl)) { + var statement = con.createStatement(); + + statement.execute("SELECT * FROM urls"); + ResultSet rs = statement.getResultSet(); + + List 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 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(); + } + } +}